add files to lib

This commit is contained in:
techies 2022-08-21 08:20:08 +07:00
parent e2862b9019
commit de22a550ad
5 changed files with 1313 additions and 0 deletions

132
lib/editor.js Normal file
View File

@ -0,0 +1,132 @@
/**
* Implements a simple select-replace file editor in Telegram.
* It works as follows:
*
* 1. The user invokes the editor with a non-empty file.
* 2. The contents of the file are posted as a message.
* 3. The user replies to that message with (part of) the text.
* The bot will locate that substring in the file contents and track the message.
* 4. The user edits his message.
* The bot will then replace the original substring, save the file and edit its message.
* If there are any problems with saving the file, the editor may detach.
*
* NOTE: sync I/O is used for simplicity; be careful! (TODO)
**/
var fs = require("fs");
var escapeHtml = require("escape-html");
var utils = require("./utils");
function ChunkedString(text) {
this.text = text;
this.chunks = [];
}
ChunkedString.prototype.findAcquire = function findAcquire(text) {
if (text.length == 0) throw Error("Empty find text not allowed");
var index = this.text.indexOf(text);
if (index == -1)
throw Error("The substring was not found. Wrapping in tildes may be necessary.");
if (index != this.text.lastIndexOf(text))
throw Error("There are multiple instances of the passed substring");
return this.acquire(index, text.length);
};
ChunkedString.prototype.acquire = function acquire(offset, length) {
if (offset < 0 || length <= 0 || offset + length > this.text.length)
throw Error("Invalid coordinates");
for (var i = 0; i < this.chunks.length; i++) {
var c = this.chunks[i];
if (offset + length > c.offset || c.offset + c.text.length > offset)
throw Error("Chunk overlaps");
}
var chunk = { offset: offset, text: this.text.substring(offset, offset + length) };
this.chunks.push(chunk);
return chunk;
};
ChunkedString.prototype.release = function release(chunk) {
if (this.chunks.indexOf(chunk) == -1) throw Error("Invalid chunk given");
this.chunks.splice(index, 1);
};
ChunkedString.prototype.modify = function modify(chunk, text) {
if (this.chunks.indexOf(chunk) == -1) throw Error("Invalid chunk given");
if (text.length == 0) throw Error("Empty replacement not allowed");
var end = chunk.offset + chunk.text.length;
this.text = this.text.substring(0, chunk.offset) + text + this.text.substring(end);
var diff = text.length - chunk.text.length;
chunk.text = text;
this.chunks.forEach(function (c) {
if (c.offset > chunk.offset) c.offset += diff;
});
};
function Editor(reply, file, encoding) {
if (!encoding) encoding = "utf-8";
this.reply = reply;
this.file = file;
this.encoding = encoding;
// TODO: support for longer files (paginated, etc.)
// FIXME: do it correctly using fd, keeping it open
var contents = fs.readFileSync(file, encoding);
if (contents.length > 1500 || contents.split("\n") > 50)
throw Error("The file is too long");
this.contents = new ChunkedString(contents);
this.chunks = {}; // associates each message ID to an active chunk
this.message = new utils.EditedMessage(reply, this._render(), "HTML");
this.fileTouched = false;
}
Editor.prototype._render = function _render() {
if (!this.contents.text.trim()) return "<em>(empty file)</em>";
return "<pre>" + escapeHtml(this.contents.text) + "</pre>";
};
Editor.prototype.handleReply = function handleReply(msg) {
this.message.idPromise.then(function (id) {
if (this.detached) return;
if (msg.reply.id != id) return;
try {
this.chunks[msg.id] = this.contents.findAcquire(msg.text);
} catch (e) {
this.reply.html("%s", e.message);
}
}.bind(this));
};
Editor.prototype.handleEdit = function handleEdit(msg) {
if (this.detached) return false;
if (!Object.hasOwnProperty.call(this.chunks, msg.id)) return false;
this.contents.modify(this.chunks[msg.id], msg.text);
this.attemptSave();
return true;
};
Editor.prototype.attemptSave = function attemptSave() {
this.fileTouched = true;
process.nextTick(function () {
if (!this.fileTouched) return;
if (this.detached) return;
this.fileTouched = false;
// TODO: check for file external modification, fail then
try {
fs.writeFileSync(this.file, this.contents.text, this.encoding);
} catch (e) {
this.reply.html("Couldn't save file: %s", e.message);
return;
}
this.message.edit(this._render());
}.bind(this));
};
Editor.prototype.detach = function detach() {
this.detached = true;
};
module.exports.Editor = Editor;

267
lib/renderer.js Normal file
View File

@ -0,0 +1,267 @@
/**
* This class keeps a logical mapping of lines to messages.
* It doesn't actually render or send messages, it delegates
* that task to the renderer.
*
* FIXME: do something to prevent extremely long messages to be
* sent (and rejected) when too many lines are inserted in between
* a message.
**/
var escapeHtml = require("escape-html");
var utils = require("./utils");
function Renderer(reply, state, options) {
if (!options) options = {};
this.reply = reply;
this.state = state;
this.options = options;
this.offset = 0;
this.messages = [];
this.orphanLines = [];
this.unfinishedLine = null;
this.totalLines = 0;
state.on("lineChanged", this._lineChanged.bind(this));
state.on("linesRemoving", this._linesRemoving.bind(this));
state.on("linesScrolling", this._linesScrolling.bind(this));
state.on("linesInserted", this._linesInserted.bind(this));
this.initTimers();
}
/** MESSAGE MAPPING **/
Renderer.prototype.ensureLinesCreated = function ensureLinesCreated(y) {
if (this.totalLines < y) {
this.orphanLines = this.orphanLines.concat(this.state.lines.slice(this.totalLines, y));
this.totalLines = y;
this.newLinesChanged = true;
}
};
Renderer.prototype._lineChanged = function _lineChanged(y) {
if (this.state.length - y <= this.orphanLines.length)
this.newLinesChanged = true;
};
Renderer.prototype._linesRemoving = function _linesRemoving(y, n) {
this.ensureLinesCreated(this.state.lines.length);
// Seek until we arrive at the wanted line
y += this.offset;
var idx = 0, lineIdx = 0;
while (y) {
var lines = (idx === this.messages.length) ? this.orphanLines : this.messages[idx].lines;
if (lineIdx < lines.length) { lineIdx++; y--; }
else { idx++; lineIdx = 0; }
}
// Remove following lines
this.totalLines -= n;
while (n) {
var lines = (idx === this.messages.length) ? this.orphanLines : this.messages[idx].lines;
if (lines.splice(lineIdx, 1).length) n--;
else { idx++; lineIdx = 0; }
}
if (idx >= this.messages.length) this.newLinesChanged = true;
};
Renderer.prototype._linesScrolling = function _linesScrolling(n) {
this.ensureLinesCreated(this.state.lines.length);
if (n > 0) {
// Scrolling up: increment offset, discarding message if necessary
this.offset += n;
this.totalLines -= n;
while (this.messages.length) {
var message = this.messages[0];
if (message.lines.length > this.offset) break;
if (message.rendered !== message.ref.lastText) break;
this.offset -= message.lines.length;
this.messages.shift();
}
} else {
// Scrolling down: just delete bottom lines (leaving them would complicate everything)
n = -n;
this._linesRemoving(this.state.lines.length - n, n);
}
};
Renderer.prototype._linesInserted = function _linesInserted(y, n) {
this.ensureLinesCreated(y);
var pos = y;
// Seek until we arrive at the wanted line, *just before the next one*
y += this.offset;
var idx = 0, lineIdx = 0;
while (true) {
var lines = (idx === this.messages.length) ? this.orphanLines : this.messages[idx].lines;
if (lineIdx < lines.length) {
if (!y) break;
lineIdx++; y--;
} else { idx++; lineIdx = 0; }
}
// Insert lines
this.totalLines += n;
while (n) {
var lines = (idx === this.messages.length) ? this.orphanLines : this.messages[idx].lines;
lines.splice(lineIdx, 0, this.state.lines[pos]);
n--, lineIdx++, pos++;
}
if (idx === this.messages.length) this.newLinesChanged = true;
};
Renderer.prototype.update = function update() {
this.ensureLinesCreated(this.state.lines.length);
// Rerender messages, scheduling flush if some changed
var linesChanged = false;
this.messages.forEach(function (message) {
var rendered = this.render(message);
if (rendered !== message.rendered) {
message.rendered = rendered;
linesChanged = true;
}
}.bind(this));
if (linesChanged) this.editedLineTimer.set();
if (this.newLinesChanged) this.newLineTimer.reset();
this.newLinesChanged = false;
// Make sure orphan lines are processed
this.orphanLinesUpdated();
};
Renderer.prototype.emitMessage = function emitMessage(count, silent, disablePreview) {
if (count < 0 || count > this.orphanLines.length) throw new Error("Should not happen.");
if (count > this.options.maxLinesEmitted)
count = this.options.maxLinesEmitted;
var lines = this.orphanLines.splice(0, count);
var message = { lines: lines };
this.messages.push(message);
message.rendered = this.render(message);
var reply = this.reply.silent(silent).disablePreview(disablePreview);
message.ref = new utils.EditedMessage(reply, message.rendered, "HTML");
this.orphanLinesUpdated();
};
/** HTML RENDERING **/
/* Given a line, return true if potentially monospaced */
Renderer.prototype.evaluateCode = function evaluateCode(str) {
//FIXME: line just between two code lines should be come code
if (str.indexOf(" ") !== -1 || /[-_,:;<>()/\\~|'"=^]{4}/.exec(str))
return true;
return false;
};
/* Given a message object, render to an HTML snippet */
Renderer.prototype.render = function render(message) {
var cursorString = this.state.getMode("cursorBlink") ? this.options.cursorBlinkString : this.options.cursorString;
var isWhitespace = true, x = this.state.cursor[0];
var html = message.lines.map(function (line, idx) {
var hasCursor = (this.state.getMode("cursor")) && (this.state.getLine() === line);
if (!line.code && this.evaluateCode(line.str)) line.code = true;
var content = line.str;
if (hasCursor || line.str.trim().length) isWhitespace = false;
if (idx === 0 && !content.substring(0, this.options.startFill.length).trim()) {
// The message would start with spaces, which would get trimmed by telegram
if (!(hasCursor && x < this.options.startFill.length))
content = this.options.startFill + content.substring(this.options.startFill.length);
}
if (hasCursor)
content = escapeHtml(content.substring(0, x)) + cursorString + escapeHtml(content.substring(x));
else
content = escapeHtml(content);
if (line.code) content = "<code>" + content + "</code>";
return content;
}.bind(this)).join("\n");
if (isWhitespace) return "<em>(empty)</em>";
return html;
};
/** FLUSH SCHEDULING **/
Renderer.prototype.initTimers = function initTimers() {
// Set when an existent line changes, cancelled when edited lines flushed
this.editedLineTimer = new utils.Timer(this.options.editTime).on("fire", this.flushEdited.bind(this));
// Set when a new line is added or changed, cancelled on new lines flush
this.newChunkTimer = new utils.Timer(this.options.chunkTime).on("fire", this.flushNew.bind(this));
// Reset when a new line is added or changed, cancelled on new lines flush
this.newLineTimer = new utils.Timer(this.options.lineTime).on("fire", this.flushNew.bind(this));
// Set when there is an unfinished nonempty line, cancelled when reference changes or line becomes empty
this.unfinishedLineTimer = new utils.Timer(this.options.unfinishedTime).on("fire", this.flushUnfinished.bind(this));
this.newChunkTimer.on("active", function () {
this.reply.action("typing");
}.bind(this));
//FIXME: should we emit actions on edits?
};
Renderer.prototype.orphanLinesUpdated = function orphanLinesUpdated() {
var newLines = this.orphanLines.length - 1;
if (newLines >= this.options.maxLinesWait) {
// Flush immediately
this.flushNew();
} else if (newLines > 0) {
this.newChunkTimer.set();
} else {
this.newChunkTimer.cancel();
this.newLineTimer.cancel();
}
// Update unfinished line
var unfinishedLine = this.orphanLines[this.orphanLines.length - 1];
if (unfinishedLine && this.totalLines === this.state.rows && unfinishedLine.str.length === this.state.columns)
unfinishedLine = null;
if (this.unfinishedLine !== unfinishedLine) {
this.unfinishedLine = unfinishedLine;
this.unfinishedLineTimer.cancel();
}
if (unfinishedLine && unfinishedLine.str.length) this.unfinishedLineTimer.set();
else this.unfinishedLineTimer.cancel();
};
Renderer.prototype.flushEdited = function flushEdited() {
this.messages.forEach(function (message) {
if (message.rendered !== message.ref.lastText)
message.ref.edit(message.rendered);
});
this.editedLineTimer.cancel();
};
Renderer.prototype.flushNew = function flushNew() {
this.flushEdited();
var count = this.orphanLines.length;
if (this.unfinishedLine) count--;
if (count <= 0) return;
this.emitMessage(count, !!this.options.silent, !!this.options.hidePreview);
};
Renderer.prototype.flushUnfinished = function flushUnfinished() {
do this.flushNew(); while (this.orphanLines.length > 1);
if (this.orphanLines.length < 1 || this.orphanLines[0].str.length === 0) return;
this.emitMessage(1, !!this.options.unfinishedSilent, !!this.options.unfinishedHidePreview);
};
exports.Renderer = Renderer;

539
lib/terminal.js Normal file
View File

@ -0,0 +1,539 @@
/**
* Implements a terminal emulator. We use terminal.js for the
* dirty work of parsing the escape sequences, but implement our
* own TermState, with some quirks when compared to a standard
* terminal emulator:
*
* - Lines and characters are created on demand. The terminal
* starts out with no content. The reason being, you don't want
* empty lines to be immediately pushed to your Telegram chat
* after starting a command.
*
* - Allows lines of length higher than the column size. The extra
* characters are appended but the cursor keeps right at the edge.
* Telegram already wraps long lines, having them wrapped by the
* terminal would be ugly.
*
* - Graphic attributes not implemented for now (would not be used
* for Telegram anyways).
*
* - Doesn't have an alternate buffer for now (wouldn't make much
* sense for Telegram rendering...) FIXME
*
* The terminal emulator emits events when lines get inserted, changed,
* removed or go out of view, similar to the original TermState.
**/
var util = require("util");
var Terminal = require("terminal.js");
//FIXME: investigate using a patched palette for better support on android
var GRAPHICS = {
'`': '\u25C6',
'a': '\u2592',
'b': '\u2409',
'c': '\u240C',
'd': '\u240D',
'e': '\u240A',
'f': '\u00B0',
'g': '\u00B1',
'h': '\u2424',
'i': '\u240B',
'j': '\u2518',
'k': '\u2510',
'l': '\u250C',
'm': '\u2514',
'n': '\u253C',
'o': '\u23BA',
'p': '\u23BB',
'q': '\u2500',
'r': '\u23BC',
's': '\u23BD',
't': '\u251C',
'u': '\u2524',
'v': '\u2534',
'w': '\u252C',
'x': '\u2502',
'y': '\u2264',
'z': '\u2265',
'{': '\u03C0',
'|': '\u2260',
'}': '\u00A3',
'~': '\u00B7'
};
/** INITIALIZATION & ACCESSORS **/
function TermState(options) {
if (!options) options = {};
this.rows = options.rows || 24;
this.columns = options.columns || 80;
this.defaultAttributes = {
fg: null,
bg: null,
bold: false,
underline: false,
italic: false,
blink: false,
inverse: false,
};
this.reset();
}
util.inherits(TermState, require("events").EventEmitter);
TermState.prototype.reset = function reset() {
this.lines = [];
this.cursor = [0,0];
this.savedCursor = [0,0];
this.modes = {
cursor: true,
cursorBlink: false,
appKeypad: false,
wrap: true,
insert: false,
crlf: false,
mousebtn: false,
mousemtn: false,
reverse: false,
graphic: false,
mousesgr: false,
};
this.attributes = Object.create(this.defaultAttributes);
this._charsets = {
"G0": "unicode",
"G1": "unicode",
"G2": "unicode",
"G3": "unicode",
};
this._mappedCharset = "G0";
this._mappedCharsetNext = "G0";
this.metas = {
title: '',
icon: ''
};
this.leds = {};
this._tabs = [];
this.emit("reset");
};
function getGenericSetter(field) {
return function genericSetter(name, value) {
this[field + "s"][name] = value;
this.emit(field, name);
};
}
TermState.prototype.setMode = getGenericSetter("mode");
TermState.prototype.setMeta = getGenericSetter("meta");
TermState.prototype.setAttribute = getGenericSetter("attribute");
TermState.prototype.setLed = getGenericSetter("led");
TermState.prototype.getMode = function getMode(mode) {
return this.modes[mode];
};
TermState.prototype.getLed = function getLed(led) {
return !!this.leds[led];
};
TermState.prototype.ledOn = function ledOn(led) {
this.setLed(led, true);
return this;
};
TermState.prototype.resetLeds = function resetLeds() {
this.leds = {};
return this;
};
TermState.prototype.resetAttribute = function resetAttribute(name) {
this.attributes[name] = this.defaultAttributes[name];
return this;
};
TermState.prototype.mapCharset = function(target, nextOnly) {
this._mappedCharset = target;
if (!nextOnly) this._mappedCharsetNext = target;
this.modes.graphic = this._charsets[this._mappedCharset] === "graphics"; // backwards compatibility
};
TermState.prototype.selectCharset = function(charset, target) {
if (!target) target = this._mappedCharset;
this._charsets[target] = charset;
this.modes.graphic = this._charsets[this._mappedCharset] === "graphics"; // backwards compatibility
};
/** CORE METHODS **/
/* Move the cursor */
TermState.prototype.setCursor = function setCursor(x, y) {
if (typeof x === 'number')
this.cursor[0] = x;
if (typeof y === 'number')
this.cursor[1] = y;
this.cursor = this.getCursor();
this.emit("cursor");
return this;
};
/* Get the real cursor position (the logical one may be off-bounds) */
TermState.prototype.getCursor = function getCursor() {
var x = this.cursor[0], y = this.cursor[1];
if (x >= this.columns) x = this.columns - 1;
else if (x < 0) x = 0;
if (y >= this.rows) y = this.rows - 1;
else if (y < 0) y = 0;
return [x,y];
};
/* Get the line at specified position, allocating it if necessary */
TermState.prototype.getLine = function getLine(y) {
if (typeof y !== "number") y = this.getCursor()[1];
if (y < 0) throw new Error("Invalid position to write to");
// Insert lines until the line at this position is available
while (!(y < this.lines.length))
this.lines.push({ str: "", attr: null });
return this.lines[y];
};
/* Replace the line at specified position, allocating it if necessary */
TermState.prototype.setLine = function setLine(y, line) {
if (typeof y !== "number") line = y, y = this.getCursor()[1];
this.getLine(y);
this.lines[y] = line;
return this;
};
/* Write chunk of text (single-line assumed) beginning at position */
TermState.prototype._writeChunk = function _writeChunk(position, chunk, insert) {
var x = position[0], line = this.getLine(position[1]);
if (x < 0) throw new Error("Invalid position to write to");
// Insert spaces until the wanted column is available
while (line.str.length < x)
line.str += " ";
// Write the chunk at position
line.str = line.str.substring(0, x) + chunk + line.str.substring(x + (insert ? 0 : chunk.length));
//TODO: add attribute
this.emit("lineChanged", position[1]);
return this;
};
/* Remove characters beginning at position */
TermState.prototype.removeChar = function removeChar(n) {
var x = this.cursor[0], line = this.getLine();
if (x < 0) throw new Error("Invalid position to delete from");
// Insert spaces until the wanted column is available
while (line.str.length < x)
line.str += " ";
// Remove characters
line.str = line.str.substring(0, x) + line.str.substring(x + n);
this.emit("lineChanged", this.cursor[1]);
return this;
};
TermState.prototype.eraseInLine = function eraseInLine(n) {
var x = this.cursor[0], line = this.getLine();
switch (n || 0) {
case "after":
case 0:
line.str = line.str.substring(0, x);
break;
case "before":
case 1:
var str = "";
while (str.length < x) str += " ";
line.str = str + line.str.substring(x);
break;
case "all":
case 2:
line.str = "";
break;
}
this.emit("lineChanged", this.cursor[1]);
return this;
};
TermState.prototype.eraseInDisplay = function eraseInDisplay(n) {
switch (n || 0) {
case "below":
case "after":
case 0:
this.eraseInLine(n);
this.removeLine(this.lines.length - (this.cursor[1]+1), this.cursor[1]+1);
break;
case "above":
case "before":
case 1:
for (var y = 0; y < this.cursor[1]; y++) {
this.lines[y].str = "";
this.emit("lineChanged", y);
}
this.eraseInLine(n);
break;
case "all":
case 2:
this.removeLine(this.lines.length, 0);
break;
}
return this;
};
TermState.prototype.removeLine = function removeLine(n, y) {
if (typeof y !== "number") y = this.cursor[1];
if (n <= 0) return this;
if (y + n > this.lines.length)
n = this.lines.length - y;
if (n <= 0) return this;
this.emit("linesRemoving", y, n);
this.lines.splice(y, n);
return this;
};
TermState.prototype.insertLine = function insertLine(n, y) {
if (typeof y !== "number") y = this.cursor[1];
if (n <= 0) return this;
if (y + n > this.rows)
n = this.rows - y;
if (n <= 0) return this;
this.getLine(y);
this.removeLine((this.lines.length + n) - this.rows, this.rows - n);
for (var i = 0; i < n; i++)
this.lines.splice(y, 0, { str: "", attr: null });
this.emit("linesInserted", y, n);
return this;
};
TermState.prototype.scroll = function scroll(n) {
if (n > 0) { // up
if (n > this.lines.length) n = this.lines.length; //FIXME: is this okay?
if (n > 0) this.emit("linesScrolling", n);
this.lines = this.lines.slice(n);
} else if (n < 0) { // down
n = -n;
if (n > this.rows) n = this.rows; //FIXME: is this okay?
var extraLines = (this.lines.length + n) - this.rows;
if (extraLines > 0) this.emit("linesScrolling", -extraLines);
this.lines = this.lines.slice(0, this.rows - n);
this.insertLine(n, 0);
}
return this;
};
/** HIGH LEVEL **/
TermState.prototype._graphConvert = function(content) {
// optimization for 99% of the time
if(this._mappedCharset === this._mappedCharsetNext && !this.modes.graphic) {
return content;
}
var result = "", i;
for(i = 0; i < content.length; i++) {
result += (this.modes.graphic && content[i] in GRAPHICS) ?
GRAPHICS[content[i]] :
content[i];
this._mappedCharset = this._mappedCharsetNext;
this.modes.graphic = this._charsets[this._mappedCharset] === "graphics"; // backwards compatibility
}
return result;
};
TermState.prototype.write = function write(chunk) {
chunk.split("\n").forEach(function (line, i) {
if (i > 0) {
// Begin new line
if (this.cursor[1] + 1 >= this.rows)
this.scroll(1);
this.mvCursor(0, 1);
this.getLine();
}
if (!line.length) return;
if (this.getMode("graphic")) this.getLine().code = true;
line = this._graphConvert(line);
this._writeChunk(this.cursor, line, this.getMode("insert"));
this.cursor[0] += line.length;
}.bind(this));
this.emit("cursor");
return this;
};
TermState.prototype.resize = function resize(size) {
if (this.lines.length > size.rows)
this.scroll(this.lines.length - size.rows);
this.rows = size.rows;
this.columns = size.columns;
this.setCursor();
this.emit("resize", size);
return this;
};
TermState.prototype.mvCursor = function mvCursor(x, y) {
var cursor = this.getCursor();
return this.setCursor(cursor[0] + x, cursor[1] + y);
};
TermState.prototype.toString = function toString() {
return this.lines.map(function (line) { return line.str; }).join("\n");
};
TermState.prototype.prevLine = function prevLine() {
if (this.cursor[1] > 0) this.mvCursor(0, -1);
else this.scroll(-1);
return this;
};
TermState.prototype.nextLine = function nextLine() {
if (this.cursor[1] < this.rows - 1) this.mvCursor(0, +1);
else this.scroll(+1);
return this;
};
TermState.prototype.saveCursor = function saveCursor() {
this.savedCursor = this.getCursor();
return this;
};
TermState.prototype.restoreCursor = function restoreCursor() {
this.cursor = this.savedCursor;
return this.setCursor();
};
TermState.prototype.insertBlank = function insertBlank(n) {
var str = "";
while (str.length < n) str += " ";
return this._writeChunk(this.cursor, str, true);
};
TermState.prototype.eraseCharacters = function eraseCharacters(n) {
var str = "";
while (str.length < n) str += " ";
return this._writeChunk(this.cursor, str, false);
};
TermState.prototype.setScrollRegion = function setScrollRegion(n, m) {
//TODO
return this;
};
TermState.prototype.switchBuffer = function switchBuffer(alt) {
if (this.alt !== alt) {
this.scroll(this.lines.length);
this.alt = alt;
}
return this;
};
TermState.prototype.getBufferRowCount = function getBufferRowCount() {
return this.lines.length;
};
/**
* moves Cursor forward or backward a specified amount of tabs
* @param n {number} - number of tabs to move. <0 moves backward, >0 moves
* forward
*/
TermState.prototype.mvTab = function(n) {
var x = this.getCursor()[0];
var tabMax = this._tabs[this._tabs.length - 1] || 0;
var positive = n > 0;
n = Math.abs(n);
while(n !== 0 && x > 0 && x < this.columns-1) {
x += positive ? 1 : -1;
if(this._tabs.indexOf(x) != -1 || (x > tabMax && x % 8 === 0))
n--;
}
this.setCursor(x);
};
/**
* set tab at specified position
* @param pos {number} - position to set a tab at
*/
TermState.prototype.setTab = function(pos) {
// Set the default to current cursor if no tab position is specified
if(pos === undefined) {
pos = this.getCursor()[0];
}
// Only add the tab position if it is not there already
if (this._tabs.indexOf(pos) != -1) {
this._tabs.push(pos);
this._tabs.sort();
}
};
/**
* remove a tab
* @param pos {number} - position to remove a tab. Do nothing if the tab isn't
* set at this position
*/
TermState.prototype.removeTab = function(pos) {
var i, tabs = this._tabs;
for(i = 0; i < tabs.length && tabs[i] !== pos; i++);
tabs.splice(i, 1);
};
/**
* removes a tab at a given index
* @params n {number} - can be one of the following
* <ul>
* <li>"current" or 0: searches tab at current position. no tab is at current
* position delete the next tab</li>
* <li>"all" or 3: deletes all tabs</li>
*/
TermState.prototype.tabClear = function(n) {
switch(n || "current") {
case "current":
case 0:
for(var i = this._tabs.length - 1; i >= 0; i--) {
if(this._tabs[i] < this.getCursor()[0]) {
this._tabs.splice(i, 1);
break;
}
}
break;
case "all":
case 3:
this._tabs = [];
break;
}
};
function createTerminal(options) {
var state = new TermState(options);
var term = new Terminal({});
term.state = state;
return term;
}
exports.TermState = TermState;
exports.createTerminal = createTerminal;

251
lib/utils.js Normal file
View File

@ -0,0 +1,251 @@
/**
* Miscellaneous utilities.
**/
var fs = require("fs");
var util = require("util");
var mime = require("mime");
var crypto = require("crypto");
var url = require("url");
/** TIMER **/
function Timer(delay) {
this.delay = delay;
}
util.inherits(Timer, require("events").EventEmitter);
/* Starts the timer, does nothing if started already. */
Timer.prototype.set = function set() {
if (this.timeout) return;
this.timeout = setTimeout(function () {
this.timeout = null;
this.emit("fire");
}.bind(this), this.delay);
this.emit("active");
};
/* Cancels the timer if set. */
Timer.prototype.cancel = function cancel() {
if (!this.timeout) return;
clearTimeout(this.timeout);
delete this.timeout;
};
/* Starts the timer, cancelling first if set. */
Timer.prototype.reset = function reset() {
this.cancel();
this.set();
};
/** EDITED MESSAGE **/
function EditedMessage(reply, text, mode) {
this.reply = reply;
this.mode = mode;
this.lastText = text;
this.markup = reply.parameters["reply_markup"];
this.disablePreview = reply.parameters["disable_web_page_preview"];
this.text = text;
this.callbacks = [];
this.pendingText = null;
this.pendingCallbacks = [];
this.idPromise = new Promise(function (resolve, reject) {
reply.text(this.text, this.mode).then(function (err, msg) {
if (err) reject(err);
else resolve(msg.id);
this._whenEdited(err, msg);
}.bind(this));
}.bind(this));
}
util.inherits(EditedMessage, require("events").EventEmitter);
EditedMessage.prototype.refresh = function refresh(callback) {
if (callback) this.pendingCallbacks.push(callback);
this.pendingText = this.lastText;
if (this.callbacks === undefined) this._flushEdit();
};
EditedMessage.prototype.edit = function edit(text, callback) {
this.lastText = text;
var idle = this.callbacks === undefined;
if (callback) this.pendingCallbacks.push(callback);
if (text === this.text) {
this.callbacks = (this.callbacks || []).concat(this.pendingCallbacks);
this.pendingText = null;
this.pendingCallbacks = [];
if (idle) this._whenEdited();
} else {
this.pendingText = text;
if (idle) this._flushEdit();
}
};
EditedMessage.prototype._flushEdit = function _flushEdit() {
this.text = this.pendingText;
this.callbacks = this.pendingCallbacks;
this.pendingText = null;
this.pendingCallbacks = [];
this.reply.parameters["reply_markup"] = this.markup;
this.reply.parameters["disable_web_page_preview"] = this.disablePreview;
this.reply.editText(this.id, this.text, this.mode).then(this._whenEdited.bind(this));
};
EditedMessage.prototype._whenEdited = function _whenEdited(err, msg) {
if (err) this.emit(this.id === undefined ? "error" : "editError", err);
if (this.id === undefined) this.id = msg.id;
var callbacks = this.callbacks;
delete this.callbacks;
callbacks.forEach(function (callback) { callback(); });
if (this.pendingText !== null) this._flushEdit();
};
/** SANITIZED ENV **/
function getSanitizedEnv() {
// Adapted from pty.js source
var env = {};
Object.keys(process.env).forEach(function (key) {
env[key] = process.env[key];
});
// Make sure we didn't start our
// server from inside tmux.
delete env.TMUX;
delete env.TMUX_PANE;
// Make sure we didn't start
// our server from inside screen.
// http://web.mit.edu/gnu/doc/html/screen_20.html
delete env.STY;
delete env.WINDOW;
// Delete some variables that
// might confuse our terminal.
delete env.WINDOWID;
delete env.TERMCAP;
delete env.COLUMNS;
delete env.LINES;
// Set $TERM to screen. This disables multiplexers
// that have login hooks, such as byobu.
env.TERM = "screen";
return env;
}
/** RESOLVE SIGNAL **/
var SIGNALS = "HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS".split(" ");
function formatSignal(signal) {
signal--;
if (signal in SIGNALS) return "SIG" + SIGNALS[signal];
return "unknown signal " + signal;
}
/** SHELLS **/
function getShells() {
var lines = fs.readFileSync("/etc/shells", "utf-8").split("\n")
var shells = lines.map(function (line) { return line.split("#")[0]; })
.filter(function (line) { return line.trim().length; });
// Add process.env.SHELL at #1 position
var shell = process.env.SHELL;
if (shell) {
var idx = shells.indexOf(shell);
if (idx !== -1) shells.splice(idx, 1);
shells.unshift(shell);
}
return shells;
}
var shells = getShells();
/** RESOLVE SHELLS **/
function resolveShell(shell) {
return shell; //TODO: if found in list, otherwise resolve with which & verify access
}
/** TOKEN GENERATION **/
function generateToken() {
return crypto.randomBytes(12).toString("hex");
}
/** RESOLVE BOOLEAN **/
var BOOLEANS = {
"yes": true, "no": false,
"y": true, "n": false,
"on": true, "off": false,
"enable": true, "disable": false,
"enabled": true, "disabled": false,
"active": true, "inactive": false,
"true": true, "false": false,
};
function resolveBoolean(arg) {
arg = arg.trim().toLowerCase();
if (!Object.hasOwnProperty.call(BOOLEANS, arg)) return null;
return BOOLEANS[arg];
}
/** GENERATE FILENAME WHEN NOT AVAILABLE **/
function constructFilename(msg) {
return "upload." + mime.extension(msg.file.mime);
}
/** AGENT **/
function createAgent() {
var proxy = process.env["https_proxy"] || process.env["all_proxy"];
if (!proxy) return;
try {
proxy = url.parse(proxy);
} catch (e) {
console.error("Error parsing proxy URL:", e, "Ignoring proxy.");
return;
}
if ([ "socks:", "socks4:", "socks4a:", "socks5:", "socks5h:" ].indexOf(proxy.protocol) !== -1) {
try {
var SocksProxyAgent = require('socks-proxy-agent');
} catch (e) {
console.error("Error loading SOCKS proxy support, verify socks-proxy-agent is correctly installed. Ignoring proxy.");
return;
}
return new SocksProxyAgent(proxy);
}
if ([ "http:", "https:" ].indexOf(proxy.protocol) !== -1) {
try {
var HttpsProxyAgent = require('https-proxy-agent');
} catch (e) {
console.error("Error loading HTTPS proxy support, verify https-proxy-agent is correctly installed. Ignoring proxy.");
return;
}
return new HttpsProxyAgent(proxy);
}
console.error("Unknown proxy protocol:", util.inspect(proxy.protocol), "Ignoring proxy.");
}
exports.Timer = Timer;
exports.EditedMessage = EditedMessage;
exports.getSanitizedEnv = getSanitizedEnv;
exports.formatSignal = formatSignal;
exports.shells = shells;
exports.resolveShell = resolveShell;
exports.generateToken = generateToken;
exports.resolveBoolean = resolveBoolean;
exports.constructFilename = constructFilename;
exports.createAgent = createAgent;

124
lib/wizard.js Normal file
View File

@ -0,0 +1,124 @@
var readline = require("readline");
var botgram = require("botgram");
var fs = require("fs");
var util = require("util");
var utils = require("./utils");
// Wizard functions
function stepAuthToken(rl, config) {
return question(rl, "First, enter your bot API token: ")
.then(function (token) {
token = token.trim();
//if (!/^\d{5,}:[a-zA-Z0-9_+/-]{20,}$/.test(token))
// throw new Error();
config.authToken = token;
return createBot(token);
}).catch(function (err) {
console.error("Invalid token was entered, please try again.\n%s\n", err);
return stepAuthToken(rl, config);
});
}
function stepOwner(rl, config, getNextMessage) {
console.log("Waiting for a message...");
return getNextMessage().then(function (msg) {
var prompt = util.format("Should %s «%s» (%s) be the bot's owner? [y/n]: ", msg.chat.type, msg.chat.name, msg.chat.id);
return question(rl, prompt)
.then(function (answer) {
console.log();
answer = answer.trim().toLowerCase();
if (answer === "y" || answer === "yes")
config.owner = msg.chat.id;
else
return stepOwner(rl, config, getNextMessage);
});
});
}
function configWizard(options) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
var config = {};
var bot = null;
return Promise.resolve()
.then(function () {
return stepAuthToken(rl, config);
})
.then(function (bot_) {
bot = bot_;
console.log("\nNow, talk to me so I can discover your Telegram user:\n%s\n", bot.link());
})
.then(function () {
var getNextMessage = getPromiseFactory(bot);
return stepOwner(rl, config, getNextMessage);
})
.then(function () {
console.log("All done, writing the configuration...");
var contents = JSON.stringify(config, null, 4) + "\n";
return writeFile(options.configFile, contents);
})
.catch(function (err) {
console.error("Error, wizard crashed:\n%s", err.stack);
process.exit(1);
})
.then(function () {
rl.close();
if (bot) bot.stop();
process.exit(0);
});
}
// Promise utilities
function question(interface, query) {
return new Promise(function (resolve, reject) {
interface.question(query, resolve);
});
}
function writeFile(file, contents) {
return new Promise(function (resolve, reject) {
fs.writeFile(file, contents, "utf-8", function (err) {
if (err) reject(err);
else resolve();
});
});
}
function createBot(token) {
return new Promise(function (resolve, reject) {
var bot = botgram(token, { agent: utils.createAgent() });
bot.on("error", function (err) {
bot.stop();
reject(err);
});
bot.on("ready", resolve.bind(this, bot));
});
}
function getPromiseFactory(bot) {
var resolveCbs = [];
bot.message(function (msg, reply, next) {
if (!msg.queued) {
resolveCbs.forEach(function (resolve) {
resolve(msg);
});
resolveCbs = [];
}
next();
});
return function () {
return new Promise(function (resolve, reject) {
resolveCbs.push(resolve);
});
};
}
exports.configWizard = configWizard;