telegraf-ts
Version:
Well-Typed fork of telegraf
248 lines (230 loc) • 7.03 kB
JavaScript
const debug = require('debug')('telegraf:core');
const Telegram = require('./telegram');
const Extra = require('./extra');
const Composer = require('./composer');
const Markup = require('./markup');
const session = require('./session');
const Router = require('./router');
const Stage = require('./stage');
const BaseScene = require('./scenes/base');
const Context = require('./context');
const generateCallback = require('./core/network/webhook');
const crypto = require('crypto');
const { URL } = require('url');
const DEFAULT_OPTIONS = {
retryAfter: 1,
handlerTimeout: 0,
contextType: Context,
};
const noop = () => {};
class Telegraf extends Composer {
constructor(token, options) {
super();
this.options = {
...DEFAULT_OPTIONS,
...options,
};
this.token = token;
this.handleError = err => {
console.error();
console.error((err.stack || err.toString()).replace(/^/gm, ' '));
console.error();
throw err;
};
this.context = {};
this.polling = {
offset: 0,
started: false,
};
}
set token(token) {
this.telegram = new Telegram(
token,
this.telegram ? this.telegram.options : this.options.telegram,
);
}
get token() {
return this.telegram.token;
}
set webhookReply(webhookReply) {
this.telegram.webhookReply = webhookReply;
}
get webhookReply() {
return this.telegram.webhookReply;
}
catch(handler) {
this.handleError = handler;
return this;
}
webhookCallback(path = '/') {
return generateCallback(
path,
(update, res) => this.handleUpdate(update, res),
debug,
);
}
startPolling(timeout = 30, limit = 100, allowedUpdates, stopCallback = noop) {
this.polling.timeout = timeout;
this.polling.limit = limit;
this.polling.allowedUpdates = allowedUpdates
? Array.isArray(allowedUpdates)
? allowedUpdates
: [`${allowedUpdates}`]
: null;
this.polling.stopCallback = stopCallback;
if (!this.polling.started) {
this.polling.started = true;
this.fetchUpdates();
}
return this;
}
startWebhook(hookPath, tlsOptions, port, host, cb) {
const webhookCb = this.webhookCallback(hookPath);
const callback =
cb && typeof cb === 'function'
? (req, res) => webhookCb(req, res, () => cb(req, res))
: webhookCb;
this.webhookServer = tlsOptions
? require('https').createServer(tlsOptions, callback)
: require('http').createServer(callback);
this.webhookServer.listen(port, host, () => {
debug('Webhook listening on port: %s', port);
});
return this;
}
launch(config = {}) {
debug('Connecting to Telegram');
return this.telegram
.getMe()
.then(botInfo => {
debug(`Launching @${botInfo.username}`);
this.options.username = botInfo.username;
this.context.botInfo = botInfo;
if (!config.webhook) {
const { timeout, limit, allowedUpdates, stopCallback } =
config.polling || {};
return this.telegram
.deleteWebhook()
.then(() =>
this.startPolling(timeout, limit, allowedUpdates, stopCallback),
)
.then(() => debug('Bot started with long-polling'));
}
if (
typeof config.webhook.domain !== 'string' &&
typeof config.webhook.hookPath !== 'string'
) {
throw new Error('Webhook domain or webhook path is required');
}
let domain = config.webhook.domain || '';
if (domain.startsWith('https://') || domain.startsWith('http://')) {
domain = new URL(domain).host;
}
const hookPath =
config.webhook.hookPath ||
`/telegraf/${crypto.randomBytes(32).toString('hex')}`;
const { port, host, tlsOptions, cb } = config.webhook;
this.startWebhook(hookPath, tlsOptions, port, host, cb);
if (!domain) {
debug('Bot started with webhook');
return;
}
return this.telegram
.setWebhook(`https://${domain}${hookPath}`)
.then(() => debug(`Bot started with webhook @ https://${domain}`));
})
.catch(err => {
console.error('Launch failed');
console.error(err.stack || err.toString());
});
}
stop(cb = noop) {
debug('Stopping bot...');
return new Promise(resolve => {
const done = () => resolve() & cb();
if (this.webhookServer) {
return this.webhookServer.close(done);
} else if (!this.polling.started) {
return done();
}
this.polling.stopCallback = done;
this.polling.started = false;
});
}
handleUpdates(updates) {
if (!Array.isArray(updates)) {
return Promise.reject(new Error('Updates must be an array'));
}
const processAll = Promise.all(
updates.map(update => this.handleUpdate(update)),
);
if (this.options.handlerTimeout === 0) {
return processAll;
}
return Promise.race([
processAll,
new Promise(resolve => setTimeout(resolve, this.options.handlerTimeout)),
]);
}
handleUpdate(update, webhookResponse) {
debug('Processing update', update.update_id);
const tg = new Telegram(this.token, this.telegram.options, webhookResponse);
const TelegrafContext = this.options.contextType;
const ctx = new TelegrafContext(update, tg, this.options);
Object.assign(ctx, this.context);
return this.middleware()(ctx).catch(err => this.handleError(err, ctx));
}
fetchUpdates() {
if (!this.polling.started) {
this.polling.stopCallback && this.polling.stopCallback();
return;
}
const { timeout, limit, offset, allowedUpdates } = this.polling;
this.telegram
.getUpdates(timeout, limit, offset, allowedUpdates)
.catch(err => {
if (err.code === 401 || err.code === 409) {
throw err;
}
const wait =
(err.parameters && err.parameters.retry_after) ||
this.options.retryAfter;
console.error(
`Failed to fetch updates. Waiting: ${wait}s`,
err.message,
);
return new Promise(resolve => setTimeout(resolve, wait * 1000, []));
})
.then(updates =>
this.polling.started
? this.handleUpdates(updates).then(() => updates)
: [],
)
.catch(err => {
console.error('Failed to process updates.', err);
this.polling.started = false;
this.polling.offset = 0;
this.polling.stopCallback && this.polling.stopCallback();
return [];
})
.then(updates => {
if (updates.length > 0) {
this.polling.offset = updates[updates.length - 1].update_id + 1;
}
this.fetchUpdates();
});
}
}
module.exports = Object.assign(Telegraf, {
Context,
Composer,
default: Telegraf,
Extra,
Markup,
Router,
Telegraf,
Telegram,
Stage,
BaseScene,
session,
});