599 lines
17 KiB
JavaScript
599 lines
17 KiB
JavaScript
const request = require('request');
|
|
const webhook = require('./webhook.js');
|
|
const standardUpdates = require('./updates.js');
|
|
const standardMethods = require('./methods.js');
|
|
|
|
const BUILDIN_PLUGINS_FOLDER = '../plugins/';
|
|
const BUILDIN_PLUGINS = ['regExpMessage', 'shortReply'];
|
|
|
|
const USER_PLUGIN_FOLDER = '../plugins/';
|
|
|
|
class TeleBot {
|
|
|
|
constructor(cfg) {
|
|
|
|
if (typeof cfg !== 'object') cfg = {token: cfg};
|
|
|
|
if (!cfg.token || cfg.token.split(':').length !== 2) {
|
|
throw Error('[bot.error] invalid bot token');
|
|
}
|
|
|
|
this.cfg = cfg;
|
|
this.token = cfg.token;
|
|
this.id = this.token.split(':')[0];
|
|
this.api = `https://api.telegram.org/bot${this.token}`;
|
|
this.fileLink = `https://api.telegram.org/file/bot${this.token}/`;
|
|
|
|
this.pluginConfig = cfg.pluginConfig || {};
|
|
|
|
this.usePlugins = Array.isArray(cfg.usePlugins) ? cfg.usePlugins : [];
|
|
this.pluginFolder = cfg.pluginFolder || USER_PLUGIN_FOLDER;
|
|
|
|
this.buildInPlugins = cfg.buildInPlugins !== undefined ? (cfg.buildInPlugins || []) : BUILDIN_PLUGINS;
|
|
this.buildInPluginsFolder = cfg.buildInPluginsFolder || BUILDIN_PLUGINS_FOLDER;
|
|
|
|
const poll = cfg.polling || {};
|
|
|
|
this.proxy = poll.proxy;
|
|
this.limit = poll.limit > 0 && poll.limit <= 100 ? poll.limit : 100;
|
|
this.interval = poll.interval >= 0 ? poll.interval : 300;
|
|
this.timeout = poll.timeout >= 0 ? poll.timeout : 0;
|
|
this.retryTimeout = poll.retryTimeout >= 0 ? poll.retryTimeout : 5000;
|
|
|
|
this.webhook = cfg.webhook;
|
|
|
|
this.allowedUpdates = typeof cfg.allowedUpdates === 'string' || Array.isArray(cfg.allowedUpdates) ? cfg.allowedUpdates : [];
|
|
this.maxConnections = this.webhook && Number.isInteger(this.webhook.maxConnections) ? this.webhook.maxConnections : 40;
|
|
|
|
this.updateId = 0;
|
|
this.loopFn = null;
|
|
|
|
this.flags = {
|
|
poll: false,
|
|
retry: false,
|
|
looping: false
|
|
};
|
|
|
|
this.modList = {};
|
|
this.eventList = new Map();
|
|
|
|
this.updateTypes = standardUpdates;
|
|
|
|
this.processUpdate = (update, props) => {
|
|
if (update) {
|
|
for (let name in this.updateTypes) {
|
|
if (name in update) {
|
|
update = update[name];
|
|
return this.updateTypes[name].call(this, update, props);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Load build-in plugins
|
|
this.buildInPlugins.map(buildInPluginName => this.plug(require(`${this.buildInPluginsFolder}${buildInPluginName}`)));
|
|
|
|
// Load user plugins
|
|
this.usePlugins.map(userPluginName => this.plug(require(`${this.pluginFolder}${userPluginName}`)));
|
|
|
|
}
|
|
|
|
/* Plugins */
|
|
|
|
static addMethods(methods) {
|
|
|
|
for (let id in methods) {
|
|
|
|
const method = methods[id];
|
|
|
|
// If method is a function
|
|
if (typeof method === 'function') {
|
|
this.prototype[id] = method;
|
|
continue;
|
|
}
|
|
|
|
// Set method name
|
|
const name = method.short || id;
|
|
|
|
// Argument function
|
|
let argFn = method.arguments;
|
|
if (argFn && typeof argFn !== 'function') {
|
|
if (typeof argFn === 'string') argFn = [argFn];
|
|
let args = argFn;
|
|
argFn = function () {
|
|
const form = {};
|
|
args.forEach((v, i) => form[v] = arguments[i]);
|
|
return form;
|
|
};
|
|
}
|
|
|
|
// Options function
|
|
let optFn = method.options;
|
|
|
|
// Create method
|
|
this.prototype[id] = this.prototype[name] = function () {
|
|
this.event([id, name], arguments);
|
|
let form = {}, args = [].slice.call(arguments);
|
|
let options = args[args.length - 1], fnOptions = {};
|
|
if (typeof options !== 'object') options = {};
|
|
if (argFn) form = argFn.apply(this, args);
|
|
if (optFn) fnOptions = optFn.apply(this, [].concat(form, options));
|
|
form = this.properties(form, Object.assign(options, fnOptions));
|
|
return this.request(`/${id}`, form).then(method.then || (re => re && re.result));
|
|
};
|
|
|
|
}
|
|
}
|
|
|
|
/* Connection */
|
|
|
|
plug(module) {
|
|
|
|
const {id, defaultConfig, plugin} = module;
|
|
|
|
if (id) {
|
|
|
|
const userConfig = this.pluginConfig[id];
|
|
const isConfigObject = Object.prototype.toString.call(defaultConfig) === '[object Object]';
|
|
|
|
let config;
|
|
if (isConfigObject) {
|
|
config = Object.assign(defaultConfig, userConfig);
|
|
} else {
|
|
config = userConfig || defaultConfig;
|
|
}
|
|
|
|
plugin.call(this, this, config || {});
|
|
|
|
console.log(`[bot.plugin] loaded '${id}' plugin`);
|
|
|
|
} else {
|
|
console.log('[bot.plugin] skip plugin without id');
|
|
}
|
|
|
|
}
|
|
|
|
start() {
|
|
|
|
const f = this.flags;
|
|
|
|
// Set webhook
|
|
if (this.webhook) {
|
|
|
|
let {url, cert} = this.webhook;
|
|
if (url) url = `${url}/${this.token}`;
|
|
|
|
return this.setWebhook(url, cert, this.allowedUpdates, this.maxConnections).then(() => {
|
|
|
|
console.log(`[bot.webhook] set to "${url}"`);
|
|
return webhook.call(this, this, this.webhook);
|
|
|
|
}).catch((error) => {
|
|
|
|
console.error('[bot.error.webhook]', error);
|
|
this.event('error', {error});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// Delete webhook
|
|
this.setWebhook().then((response) => {
|
|
f.poll = true;
|
|
|
|
if (response.description === 'Webhook was deleted') {
|
|
console.log('[bot.webhook] webhook was deleted');
|
|
}
|
|
|
|
console.log('[bot.info] bot started');
|
|
|
|
}).catch((error) => {
|
|
|
|
console.error('[bot.error.webhook]', error);
|
|
this.event('error', {error});
|
|
|
|
});
|
|
|
|
f.looping = true;
|
|
|
|
this.event('start');
|
|
|
|
// Global loop function
|
|
this.loopFn = setInterval(() => {
|
|
|
|
// Stop on false looping flag
|
|
if (!f.looping) clearInterval(this.loopFn);
|
|
|
|
// Skip processing on false poll flag
|
|
if (!f.poll) return;
|
|
|
|
f.poll = false;
|
|
|
|
// Get updates
|
|
this.getUpdates().then(() => {
|
|
|
|
// Retry connecting
|
|
if (f.retry) {
|
|
|
|
const now = Date.now();
|
|
const diff = (now - f.retry) / 1000;
|
|
|
|
console.log(`[bot.info.update] reconnected after ${diff} seconds`);
|
|
this.event('reconnected', {
|
|
startTime: f.retry, endTime: now, diffTime: diff
|
|
});
|
|
|
|
f.retry = false;
|
|
|
|
}
|
|
|
|
// Tick
|
|
return this.event('tick');
|
|
|
|
}).then(() => {
|
|
|
|
// Seems okay for the next poll
|
|
f.poll = true;
|
|
|
|
}).catch(error => {
|
|
|
|
// Set retry flag as current date (for timeout calculations)
|
|
if (f.retry === false) f.retry = Date.now();
|
|
|
|
console.error(`[bot.error.update]`, error.stack || error);
|
|
this.event(['error', 'error.update'], {error});
|
|
|
|
return Promise.reject();
|
|
|
|
}).catch(() => {
|
|
|
|
const seconds = this.retryTimeout / 1000;
|
|
console.log(`[bot.info.update] reconnecting in ${seconds} seconds...`);
|
|
this.event('reconnecting');
|
|
|
|
// Set reconnecting timeout
|
|
setTimeout(() => (f.poll = true), this.retryTimeout);
|
|
|
|
});
|
|
|
|
}, this.interval);
|
|
|
|
}
|
|
|
|
/* Stop looping */
|
|
|
|
connect(...args) {
|
|
return this.start(...args);
|
|
}
|
|
|
|
/* Fetch updates */
|
|
|
|
stop(message) {
|
|
this.flags.looping = false;
|
|
console.log(`[bot.info] bot stopped ${message ? ': ' + message : ''}`);
|
|
this.event('stop', message);
|
|
}
|
|
|
|
/* Recive updates */
|
|
|
|
getUpdates(offset = this.updateId, limit = this.limit, timeout = this.timeout, allowed_updates = this.allowedUpdates) {
|
|
|
|
// Request updates from Telegram server
|
|
return this.request('/getUpdates', {
|
|
offset, limit, timeout, allowed_updates
|
|
}).then(body =>
|
|
this.receiveUpdates(body.result)
|
|
);
|
|
|
|
}
|
|
|
|
/* Send request to server */
|
|
|
|
receiveUpdates(updateList) {
|
|
|
|
// Globals
|
|
var mod, props = {};
|
|
var promise = Promise.resolve();
|
|
|
|
// No updates
|
|
if (!updateList.length) return promise;
|
|
|
|
// We have updates
|
|
return this.event('update', updateList).then(eventProps => {
|
|
|
|
// Run update list modifiers
|
|
mod = this.modRun('updateList', {
|
|
updateList, props: extendProps(props, eventProps)
|
|
});
|
|
|
|
updateList = mod.updateList;
|
|
props = mod.props;
|
|
|
|
// Every Telegram update
|
|
for (let update of updateList) {
|
|
|
|
// Update ID
|
|
const nextId = ++update.update_id;
|
|
if (this.updateId < nextId) this.updateId = nextId;
|
|
|
|
// Run update modifiers
|
|
mod = this.modRun('update', {update, props});
|
|
|
|
update = mod.update;
|
|
props = mod.props;
|
|
|
|
// Process update
|
|
promise = promise.then(() => this.processUpdate(update, props));
|
|
|
|
}
|
|
|
|
return promise;
|
|
|
|
}).catch(error => {
|
|
|
|
console.log('[bot.error]', error.stack || error);
|
|
this.event('error', {error});
|
|
|
|
// Don't trigger server reconnect
|
|
return Promise.resolve();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/* Modifications */
|
|
|
|
request(url, form, data) {
|
|
|
|
const options = {
|
|
url: this.api + url,
|
|
json: true
|
|
};
|
|
|
|
if (this.proxy) options.proxy = this.proxy;
|
|
|
|
if (form) {
|
|
options.form = form;
|
|
} else {
|
|
for (let item in data) {
|
|
const type = typeof data[item];
|
|
if (type === 'string' || type === 'object') continue;
|
|
data[item] = JSON.stringify(data[item]);
|
|
}
|
|
options.formData = data;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
request.post(options, (error, response, body) => {
|
|
if (error || !body || !body.ok || response.statusCode === 404) {
|
|
return reject(error || body || 404);
|
|
}
|
|
return resolve(body);
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
mod(names, fn) {
|
|
if (typeof names === 'string') names = [names];
|
|
const mods = this.modList;
|
|
for (let name of names) {
|
|
if (!mods[name]) mods[name] = [];
|
|
if (mods[name].includes(fn)) return;
|
|
mods[name].push(fn);
|
|
}
|
|
return fn;
|
|
}
|
|
|
|
modRun(name, data) {
|
|
const list = this.modList[name];
|
|
if (!list || !list.length) return data;
|
|
for (let fn of list) data = fn.call(this, data);
|
|
return data;
|
|
}
|
|
|
|
/* Events */
|
|
|
|
removeMod(name, fn) {
|
|
let list = this.modList[name];
|
|
if (!list) return false;
|
|
let index = list.indexOf(fn);
|
|
if (index === -1) return false;
|
|
list.splice(index, 1);
|
|
return true;
|
|
}
|
|
|
|
on(types, fn, opt) {
|
|
|
|
if (!opt) opt = {};
|
|
if (!Array.isArray(types)) types = [types];
|
|
|
|
const eventList = this.eventList;
|
|
|
|
for (let type of types) {
|
|
|
|
if (!eventList.has(type)) {
|
|
|
|
eventList.set(type, {fired: null, list: [fn]});
|
|
|
|
} else {
|
|
|
|
const event = eventList.get(type);
|
|
|
|
if (event.list.includes(fn)) continue;
|
|
event.list.push(fn);
|
|
|
|
if (opt.fired && event.fired) {
|
|
|
|
let fired = event.fired;
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
let output = fn.call(fired.self, fired.data, fired.self, fired.details);
|
|
|
|
if (output instanceof Promise) {
|
|
output.then(resolve).catch(reject);
|
|
} else {
|
|
resolve(output);
|
|
}
|
|
|
|
}).catch(error => {
|
|
eventPromiseError.call(this, type, fired, error);
|
|
});
|
|
|
|
if (opt.cleanFired) {
|
|
eventList.set(type, event.fired = null);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
event(types, data, self) {
|
|
|
|
let promises = [];
|
|
|
|
if (!Array.isArray(types)) types = [types];
|
|
|
|
for (let type of types) {
|
|
|
|
let event = this.eventList.get(type);
|
|
let details = {type, time: Date.now()};
|
|
let fired = {self, data, details};
|
|
|
|
if (!event) {
|
|
this.eventList.set(type, {fired, list: []});
|
|
continue;
|
|
}
|
|
|
|
event.fired = fired;
|
|
event = event.list;
|
|
|
|
for (let fn of event) {
|
|
|
|
promises.push((new Promise((resolve, reject) => {
|
|
let that = this;
|
|
|
|
details.remove = (function (fn) {
|
|
return () => that.removeEvent(type, fn);
|
|
}(fn));
|
|
|
|
fn = fn.call(self, data, self, details);
|
|
|
|
if (fn instanceof Promise) {
|
|
fn.then(resolve).catch(reject);
|
|
} else {
|
|
resolve(fn);
|
|
}
|
|
|
|
})).catch(error => {
|
|
eventPromiseError.call(this, type, fired, error);
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
cleanEvent(type) {
|
|
const eventList = this.eventList;
|
|
if (!eventList.has(type)) return false;
|
|
eventList.set(type, eventList.get(type).fired = null);
|
|
return true;
|
|
}
|
|
|
|
removeEvent(type, fn) {
|
|
const eventList = this.eventList;
|
|
if (!eventList.has(type)) return false;
|
|
let event = eventList.get(type).list;
|
|
let index = event.indexOf(fn);
|
|
if (index === -1) return false;
|
|
event.splice(index, 1);
|
|
return true;
|
|
}
|
|
|
|
/* Process global properties */
|
|
|
|
destroyEvent(type) {
|
|
let eventList = this.eventList;
|
|
if (!eventList.has(type)) return false;
|
|
eventList.delete(type);
|
|
return true;
|
|
}
|
|
|
|
/* Method adder */
|
|
|
|
properties(form = {}, opt = {}) {
|
|
|
|
const parseMode = opt.parseMode || opt.parse;
|
|
const replyToMessage = opt.replyToMessage || opt.reply;
|
|
const replyMarkup = opt.replyMarkup || opt.markup;
|
|
const notification = opt.notification === false || opt.notify === false;
|
|
const webPreview = opt.webPreview === false || opt.preview === false;
|
|
|
|
if (replyToMessage) form.reply_to_message_id = replyToMessage;
|
|
if (parseMode) form.parse_mode = parseMode;
|
|
if (notification) form.disable_notification = true;
|
|
if (webPreview) form.disable_web_page_preview = true;
|
|
|
|
// Markup object
|
|
if (replyMarkup !== undefined) {
|
|
if (replyMarkup === 'hide' || replyMarkup === false) {
|
|
// Hide keyboard
|
|
form.reply_markup = JSON.stringify({hide_keyboard: true});
|
|
} else if (replyMarkup === 'reply') {
|
|
// Fore reply
|
|
form.reply_markup = JSON.stringify({force_reply: true});
|
|
} else {
|
|
// JSON keyboard
|
|
form.reply_markup = JSON.stringify(replyMarkup);
|
|
}
|
|
}
|
|
|
|
return (this.modRun('property', {form, options: opt})).form;
|
|
|
|
}
|
|
}
|
|
|
|
/* Add standard methods */
|
|
|
|
TeleBot.addMethods(standardMethods);
|
|
|
|
/* Functions */
|
|
|
|
function eventPromiseError(type, fired, error) {
|
|
return new Promise((resolve, reject) => {
|
|
console.error('[bot.error.event]', error.stack || error);
|
|
if (type !== 'error' && type !== 'error.event') {
|
|
this.event(['error', 'error.event'], {error, data: fired.data})
|
|
.then(resolve).catch(reject);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function extendProps(props, input) {
|
|
for (let obj of input) {
|
|
for (let naprops in obj) {
|
|
const key = props[naprops], value = obj[naprops];
|
|
if (key !== undefined) {
|
|
if (!Array.isArray(key)) props[naprops] = [key];
|
|
props[naprops].push(value);
|
|
continue;
|
|
}
|
|
props[naprops] = value;
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
/* Exports */
|
|
|
|
module.exports = TeleBot;
|