551 lines
22 KiB
JavaScript
551 lines
22 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Bot = exports.BotError = exports.DEFAULT_UPDATE_TYPES = void 0;
|
|
// deno-lint-ignore-file camelcase
|
|
const composer_js_1 = require("./composer.js");
|
|
Object.defineProperty(exports, "BotError", { enumerable: true, get: function () { return composer_js_1.BotError; } });
|
|
const context_js_1 = require("./context.js");
|
|
const api_js_1 = require("./core/api.js");
|
|
const error_js_1 = require("./core/error.js");
|
|
const filter_js_1 = require("./filter.js");
|
|
const platform_node_js_1 = require("./platform.node.js");
|
|
const debug = (0, platform_node_js_1.debug)("grammy:bot");
|
|
const debugWarn = (0, platform_node_js_1.debug)("grammy:warn");
|
|
const debugErr = (0, platform_node_js_1.debug)("grammy:error");
|
|
exports.DEFAULT_UPDATE_TYPES = [
|
|
"message",
|
|
"edited_message",
|
|
"channel_post",
|
|
"edited_channel_post",
|
|
"inline_query",
|
|
"chosen_inline_result",
|
|
"callback_query",
|
|
"shipping_query",
|
|
"pre_checkout_query",
|
|
"poll",
|
|
"poll_answer",
|
|
"my_chat_member",
|
|
"chat_join_request",
|
|
];
|
|
/**
|
|
* This is the single most important class of grammY. It represents your bot.
|
|
*
|
|
* First, you must create a bot by talking to @BotFather, check out
|
|
* https://t.me/BotFather. Once it is ready, you obtain a secret token for your
|
|
* bot. grammY will use that token to identify as your bot when talking to the
|
|
* Telegram servers. Got the token? You are now ready to write some code and run
|
|
* your bot!
|
|
*
|
|
* You should do three things to run your bot:
|
|
* ```ts
|
|
* // 1. Create a bot instance
|
|
* const bot = new Bot('<secret-token>')
|
|
* // 2. Listen for updates
|
|
* bot.on('message:text', ctx => ctx.reply('You wrote: ' + ctx.message.text))
|
|
* // 3. Launch it!
|
|
* bot.start()
|
|
* ```
|
|
*/
|
|
class Bot extends composer_js_1.Composer {
|
|
/**
|
|
* Creates a new Bot with the given token.
|
|
*
|
|
* Remember that you can listen for messages by calling
|
|
* ```ts
|
|
* bot.on('message', ctx => { ... })
|
|
* ```
|
|
* or similar methods.
|
|
*
|
|
* The simplest way to start your bot is via simple long polling:
|
|
* ```ts
|
|
* bot.start()
|
|
* ```
|
|
*
|
|
* @param token The bot's token as acquired from https://t.me/BotFather
|
|
* @param config Optional configuration properties for the bot
|
|
*/
|
|
constructor(token, config) {
|
|
var _a;
|
|
super();
|
|
this.token = token;
|
|
this.pollingRunning = false;
|
|
this.lastTriedUpdateId = 0;
|
|
/** Used to log a warning if some update types are not in allowed_updates */
|
|
this.observedUpdateTypes = new Set();
|
|
/**
|
|
* Holds the bot's error handler that is invoked whenever middleware throws
|
|
* (rejects). If you set your own error handler via `bot.catch`, all that
|
|
* happens is that this variable is assigned.
|
|
*/
|
|
this.errorHandler = async (err) => {
|
|
var _a, _b;
|
|
console.error("Error in middleware while handling update", (_b = (_a = err.ctx) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b.update_id, err.error);
|
|
console.error("No error handler was set!");
|
|
console.error("Set your own error handler with `bot.catch = ...`");
|
|
if (this.pollingRunning) {
|
|
console.error("Stopping bot");
|
|
await this.stop();
|
|
}
|
|
throw err;
|
|
};
|
|
if (!token)
|
|
throw new Error("Empty token!");
|
|
this.me = config === null || config === void 0 ? void 0 : config.botInfo;
|
|
this.clientConfig = config === null || config === void 0 ? void 0 : config.client;
|
|
this.ContextConstructor = (_a = config === null || config === void 0 ? void 0 : config.ContextConstructor) !== null && _a !== void 0 ? _a : context_js_1.Context;
|
|
this.api = new api_js_1.Api(token, this.clientConfig);
|
|
}
|
|
/**
|
|
* Information about the bot itself as retrieved from `api.getMe()`. Only
|
|
* available after the bot has been initialized via `await bot.init()`, or
|
|
* after the value has been set manually.
|
|
*
|
|
* Starting the bot will always perform the initialization automatically,
|
|
* unless a manual value is already set.
|
|
*
|
|
* Note that the recommended way to set a custom bot information object is
|
|
* to pass it to the configuration object of the `new Bot()` instantiation,
|
|
* rather than assigning this property.
|
|
*/
|
|
set botInfo(botInfo) {
|
|
this.me = botInfo;
|
|
}
|
|
get botInfo() {
|
|
if (this.me === undefined) {
|
|
throw new Error("Bot information unavailable! Make sure to call `await bot.init()` before accessing `bot.botInfo`!");
|
|
}
|
|
return this.me;
|
|
}
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
on(filter, ...middleware) {
|
|
for (const [u] of (0, filter_js_1.parse)(filter).flatMap(filter_js_1.preprocess)) {
|
|
this.observedUpdateTypes.add(u);
|
|
}
|
|
return super.on(filter, ...middleware);
|
|
}
|
|
/**
|
|
* Checks if the bot has been initialized. A bot is initialized if the bot
|
|
* information is set. The bot information can either be set automatically
|
|
* by calling `bot.init`, or manually through the bot constructor. Note that
|
|
* usually, initialization is done automatically and you do not have to care
|
|
* about this method.
|
|
*
|
|
* @returns true if the bot is initialized, and false otherwise
|
|
*/
|
|
isInited() {
|
|
return this.me !== undefined;
|
|
}
|
|
/**
|
|
* Initializes the bot, i.e. fetches information about the bot itself. This
|
|
* method is called automatically, you usually don't have to call it
|
|
* manually.
|
|
*
|
|
* @param signal Optional `AbortSignal` to cancel the initialization
|
|
*/
|
|
async init(signal) {
|
|
var _a;
|
|
if (!this.isInited()) {
|
|
debug("Initializing bot");
|
|
(_a = this.mePromise) !== null && _a !== void 0 ? _a : (this.mePromise = withRetries(() => this.api.getMe(signal), signal));
|
|
let me;
|
|
try {
|
|
me = await this.mePromise;
|
|
}
|
|
finally {
|
|
this.mePromise = undefined;
|
|
}
|
|
if (this.me === undefined)
|
|
this.me = me;
|
|
else
|
|
debug("Bot info was set by now, will not overwrite");
|
|
}
|
|
debug(`I am ${this.me.username}!`);
|
|
}
|
|
/**
|
|
* Internal. Do not call. Handles an update batch sequentially by supplying
|
|
* it one-by-one to the middleware. Handles middleware errors and stores the
|
|
* last update identifier that was being tried to handle.
|
|
*
|
|
* @param updates An array of updates to handle
|
|
*/
|
|
async handleUpdates(updates) {
|
|
// handle updates sequentially (!)
|
|
for (const update of updates) {
|
|
this.lastTriedUpdateId = update.update_id;
|
|
try {
|
|
await this.handleUpdate(update);
|
|
}
|
|
catch (err) {
|
|
// should always be true
|
|
if (err instanceof composer_js_1.BotError) {
|
|
await this.errorHandler(err);
|
|
}
|
|
else {
|
|
console.error("FATAL: grammY unable to handle:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* This is an internal method that you probably will not ever need to call.
|
|
* It is used whenever a new update arrives from the Telegram servers that
|
|
* your bot will handle.
|
|
*
|
|
* If you're writing a library on top of grammY, check out the
|
|
* [documentation](https://grammy.dev/plugins/runner.html) of the runner
|
|
* plugin for an example that uses this method.
|
|
*
|
|
* @param update An update from the Telegram Bot API
|
|
* @param webhookReplyEnvelope An optional webhook reply envelope
|
|
*/
|
|
async handleUpdate(update, webhookReplyEnvelope) {
|
|
if (this.me === undefined) {
|
|
throw new Error("Bot not initialized! Either call `await bot.init()`, \
|
|
or directly set the `botInfo` option in the `Bot` constructor to specify \
|
|
a known bot info object.");
|
|
}
|
|
debug(`Processing update ${update.update_id}`);
|
|
// create API object
|
|
const api = new api_js_1.Api(this.token, this.clientConfig, webhookReplyEnvelope);
|
|
// configure it with the same transformers as bot.api
|
|
const t = this.api.config.installedTransformers();
|
|
if (t.length > 0)
|
|
api.config.use(...t);
|
|
// create context object
|
|
const ctx = new this.ContextConstructor(update, api, this.me);
|
|
try {
|
|
// run middleware stack
|
|
await (0, composer_js_1.run)(this.middleware(), ctx);
|
|
}
|
|
catch (err) {
|
|
debugErr(`Error in middleware for update ${update.update_id}`);
|
|
throw new composer_js_1.BotError(err, ctx);
|
|
}
|
|
}
|
|
/**
|
|
* Starts your bot using long polling.
|
|
*
|
|
* > This method returns a `Promise` that will never resolve except if your
|
|
* > bot is stopped. **You don't need to `await` the call to `bot.start`**,
|
|
* > but remember to catch potential errors by calling `bot.catch`.
|
|
* > Otherwise your bot will crash (and stop) if something goes wrong in
|
|
* > your code.
|
|
*
|
|
* This method effectively enters a loop that will repeatedly call
|
|
* `getUpdates` and run your middleware for every received update, allowing
|
|
* your bot to respond to messages.
|
|
*
|
|
* If your bot is already running, this method does nothing.
|
|
*
|
|
* **Note that this starts your bot using a very simple long polling
|
|
* implementation.** `bot.start` should only be used for small bots. While
|
|
* the rest of grammY was built to perform well even under extreme loads,
|
|
* simple long polling is not capable of scaling up in a similar fashion.
|
|
* You should switch over to using `@grammyjs/runner` if you are running a
|
|
* bot with high load.
|
|
*
|
|
* What exactly _high load_ means differs from bot to bot, but as a rule of
|
|
* thumb, simple long polling should not be processing more than ~5K
|
|
* messages every hour. Also, if your bot has long-running operations such
|
|
* as large file transfers that block the middleware from completing, this
|
|
* will impact the responsiveness negatively, so it makes sense to use the
|
|
* `@grammyjs/runner` package even if you receive much fewer messages. If
|
|
* you worry about how much load your bot can handle, check out the grammY
|
|
* [documentation](https://grammy.dev/advanced/scaling.html) about scaling
|
|
* up.
|
|
*
|
|
* @param options Options to use for simple long polling
|
|
*/
|
|
async start(options) {
|
|
var _a, _b, _c;
|
|
// Perform setup
|
|
if (!this.isInited()) {
|
|
await this.init((_a = this.pollingAbortController) === null || _a === void 0 ? void 0 : _a.signal);
|
|
}
|
|
if (this.pollingRunning) {
|
|
debug("Simple long polling already running!");
|
|
return;
|
|
}
|
|
else {
|
|
this.pollingRunning = true;
|
|
this.pollingAbortController = new shim_node_js_1.AbortController();
|
|
}
|
|
await withRetries(() => {
|
|
var _a;
|
|
return this.api.deleteWebhook({
|
|
drop_pending_updates: options === null || options === void 0 ? void 0 : options.drop_pending_updates,
|
|
}, (_a = this.pollingAbortController) === null || _a === void 0 ? void 0 : _a.signal);
|
|
}, (_b = this.pollingAbortController) === null || _b === void 0 ? void 0 : _b.signal);
|
|
// All async ops of setup complete, run callback
|
|
await ((_c = options === null || options === void 0 ? void 0 : options.onStart) === null || _c === void 0 ? void 0 : _c.call(options, this.botInfo));
|
|
// Bot was stopped during `onStart`
|
|
if (!this.pollingRunning)
|
|
return;
|
|
// Prevent common misuse that leads to missing updates
|
|
validateAllowedUpdates(this.observedUpdateTypes, options === null || options === void 0 ? void 0 : options.allowed_updates);
|
|
// Prevent common misuse that causes memory leak
|
|
this.use = noUseFunction;
|
|
// Start polling
|
|
debug("Starting simple long polling");
|
|
await this.loop(options);
|
|
debug("Middleware is done running");
|
|
}
|
|
/**
|
|
* Stops the bot from long polling.
|
|
*
|
|
* All middleware that is currently being executed may complete, but no
|
|
* further `getUpdates` calls will be performed. The current `getUpdates`
|
|
* request will be cancelled.
|
|
*
|
|
* In addition, this method will _confirm_ the last received update to the
|
|
* Telegram servers by calling `getUpdates` one last time with the latest
|
|
* offset value. If any updates are received in this call, they are
|
|
* discarded and will be fetched again when the bot starts up the next time.
|
|
* Confer the official documentation on confirming updates if you want to
|
|
* know more: https://core.telegram.org/bots/api#getupdates
|
|
*
|
|
* > Note that this method will not wait for the middleware stack to finish.
|
|
* > If you need to run code after all middleware is done, consider waiting
|
|
* > for the promise returned by `bot.start()` to resolve.
|
|
*/
|
|
async stop() {
|
|
var _a;
|
|
if (this.pollingRunning) {
|
|
debug("Stopping bot, saving update offset");
|
|
this.pollingRunning = false;
|
|
(_a = this.pollingAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
const offset = this.lastTriedUpdateId + 1;
|
|
await this.api.getUpdates({ offset, limit: 1 })
|
|
.finally(() => this.pollingAbortController = undefined);
|
|
}
|
|
else {
|
|
debug("Bot is not running!");
|
|
}
|
|
}
|
|
/**
|
|
* Sets the bots error handler that is used during long polling.
|
|
*
|
|
* You should call this method to set an error handler if you are using long
|
|
* polling, no matter whether you use `bot.start` or the `@grammyjs/runner`
|
|
* package to run your bot.
|
|
*
|
|
* Calling `bot.catch` when using other means of running your bot (or
|
|
* webhooks) has no effect.
|
|
*
|
|
* @param errorHandler A function that handles potential middleware errors
|
|
*/
|
|
catch(errorHandler) {
|
|
this.errorHandler = errorHandler;
|
|
}
|
|
/**
|
|
* Internal. Do not call. Enters a loop that will perform long polling until
|
|
* the bot is stopped.
|
|
*/
|
|
async loop(options) {
|
|
var _a, _b;
|
|
const limit = options === null || options === void 0 ? void 0 : options.limit;
|
|
const timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : 30; // seconds
|
|
let allowed_updates = (_b = options === null || options === void 0 ? void 0 : options.allowed_updates) !== null && _b !== void 0 ? _b : []; // reset to default if unspecified
|
|
while (this.pollingRunning) {
|
|
// fetch updates
|
|
const updates = await this.fetchUpdates({ limit, timeout, allowed_updates });
|
|
// check if polling stopped
|
|
if (updates === undefined)
|
|
break;
|
|
// handle updates
|
|
await this.handleUpdates(updates);
|
|
// Telegram uses the last setting if `allowed_updates` is omitted so
|
|
// we can save some traffic by only sending it in the first request
|
|
allowed_updates = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Internal. Do not call. Reliably fetches an update batch via `getUpdates`.
|
|
* Handles all known errors. Returns `undefined` if the bot is stopped and
|
|
* the call gets cancelled.
|
|
*
|
|
* @param options Polling options
|
|
* @returns An array of updates, or `undefined` if the bot is stopped.
|
|
*/
|
|
async fetchUpdates({ limit, timeout, allowed_updates }) {
|
|
var _a;
|
|
const offset = this.lastTriedUpdateId + 1;
|
|
let updates = undefined;
|
|
do {
|
|
try {
|
|
updates = await this.api.getUpdates({ offset, limit, timeout, allowed_updates }, (_a = this.pollingAbortController) === null || _a === void 0 ? void 0 : _a.signal);
|
|
}
|
|
catch (error) {
|
|
await this.handlePollingError(error);
|
|
}
|
|
} while (updates === undefined && this.pollingRunning);
|
|
return updates;
|
|
}
|
|
/**
|
|
* Internal. Do not call. Handles an error that occurred during long
|
|
* polling.
|
|
*/
|
|
async handlePollingError(error) {
|
|
var _a;
|
|
if (!this.pollingRunning) {
|
|
debug("Pending getUpdates request cancelled");
|
|
return;
|
|
}
|
|
let sleepSeconds = 3;
|
|
if (error instanceof error_js_1.GrammyError) {
|
|
debugErr(error.message);
|
|
if (error.error_code === 401) {
|
|
debugErr("Make sure you are using the bot token you obtained from @BotFather (https://t.me/BotFather).");
|
|
throw error;
|
|
}
|
|
else if (error.error_code === 409) {
|
|
debugErr("Consider revoking the bot token if you believe that no other instance is running.");
|
|
throw error;
|
|
}
|
|
else if (error.error_code === 429) {
|
|
debugErr("Bot API server is closing.");
|
|
sleepSeconds = (_a = error.parameters.retry_after) !== null && _a !== void 0 ? _a : sleepSeconds;
|
|
}
|
|
}
|
|
else
|
|
debugErr(error);
|
|
debugErr(`Call to getUpdates failed, retrying in ${sleepSeconds} seconds ...`);
|
|
await sleep(sleepSeconds);
|
|
}
|
|
}
|
|
exports.Bot = Bot;
|
|
/**
|
|
* Performs a network call task, retrying upon known errors until success.
|
|
*
|
|
* If the task errors and a retry_after value can be used, a subsequent retry
|
|
* will be delayed by the specified period of time.
|
|
*
|
|
* Otherwise, if the first attempt at running the task fails, the task is
|
|
* retried immediately. If second attempt fails, too, waits for 100 ms, and then
|
|
* doubles this delay for every subsequent attemt. Never waits longer than 1
|
|
* hour before retrying.
|
|
*
|
|
* @param task Async task to perform
|
|
* @param signal Optional `AbortSignal` to prevent further retries
|
|
*/
|
|
async function withRetries(task, signal) {
|
|
// Set up delays between retries
|
|
const INITIAL_DELAY = 50; // ms
|
|
let lastDelay = INITIAL_DELAY;
|
|
// Define error handler
|
|
/**
|
|
* Determines the error handling strategy based on various error types.
|
|
* Sleeps if necessary, and returns whether to retry or rethrow an error.
|
|
*/
|
|
async function handleError(error) {
|
|
let delay = false;
|
|
let strategy = "rethrow";
|
|
if (error instanceof error_js_1.HttpError) {
|
|
delay = true;
|
|
strategy = "retry";
|
|
}
|
|
else if (error instanceof error_js_1.GrammyError) {
|
|
if (error.error_code >= 500) {
|
|
delay = true;
|
|
strategy = "retry";
|
|
}
|
|
else if (error.error_code === 429) {
|
|
const retryAfter = error.parameters.retry_after;
|
|
if (typeof retryAfter === "number") {
|
|
// ignore the backoff for sleep, then reset it
|
|
await sleep(retryAfter, signal);
|
|
lastDelay = INITIAL_DELAY;
|
|
}
|
|
else {
|
|
delay = true;
|
|
}
|
|
strategy = "retry";
|
|
}
|
|
}
|
|
if (delay) {
|
|
// Do not sleep for the first retry
|
|
if (lastDelay !== INITIAL_DELAY) {
|
|
await sleep(lastDelay, signal);
|
|
}
|
|
const TWENTY_MINUTES = 20 * 60 * 1000; // ms
|
|
lastDelay = Math.min(TWENTY_MINUTES, 2 * lastDelay);
|
|
}
|
|
return strategy;
|
|
}
|
|
// Perform the actual task with retries
|
|
let result = { ok: false };
|
|
while (!result.ok) {
|
|
try {
|
|
result = { ok: true, value: await task() };
|
|
}
|
|
catch (error) {
|
|
debugErr(error);
|
|
const strategy = await handleError(error);
|
|
switch (strategy) {
|
|
case "retry":
|
|
continue;
|
|
case "rethrow":
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
return result.value;
|
|
}
|
|
/**
|
|
* Returns a new promise that resolves after the specified number of seconds, or
|
|
* rejects as soon as the given signal is aborted.
|
|
*/
|
|
async function sleep(seconds, signal) {
|
|
let handle;
|
|
let reject;
|
|
function abort() {
|
|
reject === null || reject === void 0 ? void 0 : reject(new Error("Aborted delay"));
|
|
if (handle !== undefined)
|
|
clearTimeout(handle);
|
|
}
|
|
try {
|
|
await new Promise((res, rej) => {
|
|
reject = rej;
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
abort();
|
|
return;
|
|
}
|
|
signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", abort);
|
|
handle = setTimeout(res, 1000 * seconds);
|
|
});
|
|
}
|
|
finally {
|
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", abort);
|
|
}
|
|
}
|
|
/**
|
|
* Takes a set of observed update types and a list of allowed updates and logs a
|
|
* warning in debug mode if some update types were observed that have not been
|
|
* allowed.
|
|
*/
|
|
function validateAllowedUpdates(updates, allowed = exports.DEFAULT_UPDATE_TYPES) {
|
|
const impossible = Array.from(updates).filter((u) => !allowed.includes(u));
|
|
if (impossible.length > 0) {
|
|
debugWarn(`You registered listeners for the following update types, \
|
|
but you did not specify them in \`allowed_updates\` \
|
|
so they may not be received: ${impossible.map((u) => `'${u}'`).join(", ")}`);
|
|
}
|
|
}
|
|
function noUseFunction() {
|
|
throw new Error(`It looks like you are registering more listeners \
|
|
on your bot from within other listeners! This means that every time your bot \
|
|
handles a message like this one, new listeners will be added. This list grows until \
|
|
your machine crashes, so grammY throws this error to tell you that you should \
|
|
probably do things a bit differently. If you're unsure how to resolve this problem, \
|
|
you can ask in the group chat: https://telegram.me/grammyjs
|
|
|
|
On the other hand, if you actually know what you're doing and you do need to install \
|
|
further middleware while your bot is running, consider installing a composer \
|
|
instance on your bot, and in turn augment the composer after the fact. This way, \
|
|
you can circumvent this protection against memory leaks.`);
|
|
}
|
|
const shim_node_js_1 = require("./shim.node.js");
|