tkd-api/node_modules/telebot/lib/telebot.js

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;