telebot
Version:
Easy way to write Telegram bots.
464 lines (355 loc) • 11.9 kB
JavaScript
'use strict';
const
request = require('request'),
standardUpdates = require('./updates.js'),
standardMethods = require('./methods.js');
/* Telegram Bot */
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.limit = Number(cfg.limit) || 100;
this.sleep = Number(cfg.sleep) || 1000;
this.timeout = cfg.timeout >= 0 ? cfg.timeout : 0;
this.retryTimeout = cfg.retryTimeout >= 0 ? cfg.retryTimeout : 5000;
this.updateId = 0;
this.loopFn = null;
this.flags = {
pool: true,
retry: false,
looping: false
};
this.modList = {};
this.eventList = {};
this.updateTypes = standardUpdates;
this.processUpdate = (update, props) => {
for (let name in this.updateTypes) {
if (name in update) {
update = update[name];
return this.updateTypes[name].call(this, update, props);
}
}
};
}
/* Modules */
use(fn) {
return fn.call(this, this, this.cfg.modules);
}
/* Connection */
connect() {
const f = this.flags;
f.looping = true;
console.log('[bot.info] bot started');
this.event('connect');
// Global loop function
this.loopFn = setInterval(x => {
// Stop on false looping flag
if (!f.looping) clearInterval(this.loopFn);
// Skip processing on false pool flag
if (!f.pool) return;
f.pool = false;
// Get updates
this.getUpdates().then(x => {
// 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(x => {
// Seems okay for the next pool
f.pool = 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(x => {
const seconds = this.retryTimeout / 1000;
console.log(`[bot.info.update] reconnecting in ${ seconds } seconds...`);
this.event('reconnecting');
// Set reconnecting timeout
setTimeout(x => (f.pool = true), this.retryTimeout);
});
}, this.sleep);
}
/* Stop looping */
disconnect(message) {
this.flags.looping = false;
console.log(`[bot.info] bot disconnected ${ message ? ': ' + message : '' }`);
this.event('disconnect', message);
}
/* Fetch updates */
getUpdates(offset=this.updateId, limit=this.limit, timeout=this.timeout) {
// Request updates from Telegram server
return this.request('/getUpdates', {
offset, limit, timeout
}).then(body =>
this.receiveUpdates(body)
);
}
/* Recive updates */
receiveUpdates(body) {
// Globals
var
mod,
props = {},
updateList = body.result,
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', {
list: updateList,
props: extendProps(props, eventProps)
});
updateList = mod.list;
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(x => 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();
});
}
/* Send request to server */
request(url, form, data) {
const options = { url: this.api + url, json: true };
if (form) { options.form = form; } else { options.formData = data; };
return new Promise((resolve, reject) => {
request.post(options, (error, response, body) => {
if (error || !body.ok || response.statusCode == 404) {
return reject(error || body || 404);
}
return resolve(body);
});
});
}
/* Modifications */
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;
}
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;
}
/* Events */
on(types, fn, opt) {
if (!opt) opt = {};
if (typeof types == 'string') types = [types];
for (let type of types) {
let event = this.eventList[type];
if (!event) {
this.eventList[type] = { fired: null, list: [fn] };
} else {
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) this.eventList[type].fired = null;
}
}
}
}
event(types, data, self) {
let promises = [];
if (typeof types == 'string') types = [types];
for (let type of types) {
let event = this.eventList[type];
let details = { type, time: Date.now() };
let fired = { self, data, details };
if (!event) {
this.eventList[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 x => 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) {
let events = this.eventList;
if (!events.hasOwnProperty(type)) return false;
events[type].fired = null;
return true;
}
removeEvent(type, fn) {
let events = this.eventList;
if (!events.hasOwnProperty(type)) return false;
let event = events[type].list;
let index = event.indexOf(fn);
if (index == -1) return false;
event.splice(index, 1);
return true;
}
destroyEvent(type) {
let events = this.eventList;
if (!events.hasOwnProperty(type)) return false;
delete events[type];
return true;
}
/* Process global properties */
properties(form={}, opt={}) {
// Reply to message
if (opt.reply) form.reply_to_message_id = opt.reply;
// Markdown/HTML support for message
if (opt.parse) form.parse_mode = opt.parse;
// User notification
if (opt.notify === false) form.disable_notification = true;
// Web preview
if (opt.preview === false) form.disable_web_page_preview = true;
// Markup object
if (opt.markup !== undefined) {
if (opt.markup == 'hide' || opt.markup === false) {
// Hide keyboard
form.reply_markup = JSON.stringify({ hide_keyboard: true });
} else if (opt.markup == 'reply') {
// Fore reply
form.reply_markup = JSON.stringify({ force_reply: true });
} else {
// JSON keyboard
form.reply_markup = opt.markup;
}
}
return (this.modRun('property', { form, options: opt })).form;
}
/* Method adder */
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[name] = function() {
this.event(name, arguments);
let form = {}, args = [].slice.call(arguments);
let options = args[args.length - 1];
if (typeof options != 'object') options = {};
if (argFn) form = argFn.apply(this, args);
if (optFn) options = optFn.apply(this, [].concat(form, options));
form = this.properties(form, options);
let request = this.request(`/${id}`, form);
if (method.then) request = request.then(module.then);
return request;
};
}
console.log(`[bot.info] new methods: ${ Object.keys(methods).join(', ') }`);
}
};
/* 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;