UNPKG

gramio

Version:

Powerful, extensible and really type-safe Telegram Bot API framework

1,488 lines (1,482 loc) 68.1 kB
'use strict'; var callbackData = require('@gramio/callback-data'); var contexts = require('@gramio/contexts'); var files = require('@gramio/files'); var format = require('@gramio/format'); var keyboards = require('@gramio/keyboards'); var debug = require('debug'); var utils = require('./utils-BKLnNOBm.cjs'); var composer = require('@gramio/composer'); const ALL_NAMES = [ "message", "edited_message", "channel_post", "edited_channel_post", "business_connection", "business_message", "edited_business_message", "deleted_business_messages", "message_reaction", "message_reaction_count", "inline_query", "chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "purchased_paid_media", "poll", "poll_answer", "my_chat_member", "chat_member", "chat_join_request", "chat_boost", "removed_chat_boost", "managed_bot", "guest_message" ]; const MESSAGE_PARENT_TYPES = [ "message", "edited_message", "channel_post", "edited_channel_post", "business_message" ]; const OPT_IN_TYPES = [ "chat_member", "message_reaction", "message_reaction_count" ]; const KNOWN_EVENTS = new Set(Object.keys(contexts.contextsMappings)); const ALL_NAMES_SET = new Set(ALL_NAMES); function mapEventToAllowedUpdates(event) { if (ALL_NAMES_SET.has(event)) return [event]; if (KNOWN_EVENTS.has(event)) return MESSAGE_PARENT_TYPES; return void 0; } function detectOptInUpdates(registeredEvents) { return OPT_IN_TYPES.filter((type) => registeredEvents.has(type)); } class AllowedUpdatesFilter extends Array { /** @internal use static factory methods instead */ constructor(updates) { super(...updates); } /** * All update types, including the opt-in ones: * `chat_member`, `message_reaction`, and `message_reaction_count`. */ static get all() { return new AllowedUpdatesFilter(ALL_NAMES); } /** * Telegram's **default** update set. * * Receive all updates _except_ `chat_member`, `message_reaction`, and * `message_reaction_count` — the three types that Telegram requires to be * explicitly listed in `allowed_updates`. * * This matches what Telegram does when `allowed_updates` is omitted or * passed as an empty array. */ static get default() { return AllowedUpdatesFilter.all.except( "chat_member", "message_reaction", "message_reaction_count" ); } /** * Create a filter with **exactly** the specified update types. * * @example * ```typescript * AllowedUpdatesFilter.only("message", "callback_query", "inline_query") * ``` */ static only(...types) { return new AllowedUpdatesFilter(types); } /** * Return a new filter with the given types **added**. * Already-present types are silently deduplicated. * * @example * ```typescript * AllowedUpdatesFilter.default.add("chat_member", "message_reaction") * ``` */ add(...types) { const set = new Set(this); for (const t of types) set.add(t); return new AllowedUpdatesFilter([...set]); } /** * Return a new filter with the given types **removed**. * * @example * ```typescript * AllowedUpdatesFilter.all.except("poll", "poll_answer", "chosen_inline_result") * ``` */ except(...types) { const excluded = new Set(types); return new AllowedUpdatesFilter( Array.from(this).filter((t) => !excluded.has(t)) ); } /** Convert to a plain `AllowedUpdateName[]` array. */ toArray() { return Array.from(this); } } function buildAllowedUpdates(bot) { const registeredEvents = bot.updates.composer.registeredEvents(); const allowedSet = /* @__PURE__ */ new Set(); for (const event of registeredEvents) { const mapped = mapEventToAllowedUpdates(event); if (mapped) for (const name of mapped) allowedSet.add(name); } return new AllowedUpdatesFilter([...allowedSet]); } const methods = composer.defineComposerMethods({ reaction(trigger, handler, macroOptions) { const reactions = Array.isArray(trigger) ? trigger : [trigger]; const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; return this.on( "message_reaction", (context, next) => { const oldEmojis = /* @__PURE__ */ new Set(); for (const r of context.oldReactions) { if (r.type === "emoji") oldEmojis.add(r.emoji); } const hasNewMatchingReaction = context.newReactions.some( (reaction) => reaction.type === "emoji" && !oldEmojis.has(reaction.emoji) && reactions.includes(reaction.emoji) ); if (!hasNewMatchingReaction) return next(); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); } ); }, callbackQuery(trigger, handler, macroOptions) { const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (typeof trigger === "string") { return this.on("callback_query", (context, next) => { if (context.data !== trigger) return next(); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (trigger instanceof RegExp) { return this.on("callback_query", (context, next) => { if (!context.data || !trigger.test(context.data)) return next(); context.queryData = context.data.match(trigger); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } return this.on("callback_query", (context, next) => { if (!context.data || !trigger.filter(context.data)) return next(); context.queryData = trigger.unpack(context.data); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); }, chosenInlineResult(trigger, handler, macroOptions) { const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (typeof trigger === "string") { return this.on("chosen_inline_result", (context, next) => { if (context.query !== trigger) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (trigger instanceof RegExp) { return this.on("chosen_inline_result", (context, next) => { if (!trigger.test(context.query)) return next(); context.args = context.query?.match(trigger); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (typeof trigger === "function") { return this.on("chosen_inline_result", (context, next) => { if (!trigger(context)) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } return this.on("chosen_inline_result", (context, next) => { if (!context.resultId || !trigger.filter(context.resultId)) return next(); context.args = null; context.queryData = trigger.unpack(context.resultId); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); }, inlineQuery(triggerOrHandler, maybeHandler, options = {}) { const trigger = maybeHandler === void 0 ? (() => true) : triggerOrHandler; const handler = maybeHandler ?? triggerOrHandler; if (options.onResult) this.chosenInlineResult(trigger, options.onResult); const { onResult: _, ...macroOptions } = options; const hasMacros = Object.keys(macroOptions).length > 0; const macroHandler = hasMacros ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (typeof trigger === "string") { return this.on("inline_query", (context, next) => { if (context.query !== trigger) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (trigger instanceof RegExp) { return this.on("inline_query", (context, next) => { if (!trigger.test(context.query)) return next(); context.args = context.query?.match(trigger); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } return this.on("inline_query", (context, next) => { if (!trigger(context)) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); }, guestQuery(triggerOrHandler, maybeHandler, macroOptions) { const trigger = maybeHandler === void 0 ? (() => true) : triggerOrHandler; const handler = maybeHandler ?? triggerOrHandler; const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (typeof trigger === "string") { return this.on("guest_message", (context, next) => { const text = context.text ?? context.caption; if (text !== trigger) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (trigger instanceof RegExp) { return this.on("guest_message", (context, next) => { const text = context.text ?? context.caption; if (!text || !trigger.test(text)) return next(); context.args = text.match(trigger); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } return this.on("guest_message", (context, next) => { if (!trigger(context)) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); }, hears(trigger, handler, macroOptions) { const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (typeof trigger === "string") { return this.on("message", (context, next) => { if ((context.text ?? context.caption) !== trigger) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (Array.isArray(trigger)) { return this.on("message", (context, next) => { const text = context.text ?? context.caption; if (!text || !trigger.includes(text)) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } if (trigger instanceof RegExp) { return this.on("message", (context, next) => { const text = context.text ?? context.caption; if (!text || !trigger.test(text)) return next(); context.args = text.match(trigger); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); } return this.on("message", (context, next) => { if (typeof trigger !== "function" || !trigger(context)) return next(); context.args = null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); }); }, command(command, handlerOrMeta, handlerOrOptions, macroOptions) { let handler; let meta; let resolvedMacroOptions; if (typeof handlerOrMeta === "function") { handler = handlerOrMeta; resolvedMacroOptions = handlerOrOptions; } else { meta = handlerOrMeta; handler = handlerOrOptions; resolvedMacroOptions = macroOptions; } const normalizedCommands = typeof command === "string" ? [command] : Array.from(command); for (const cmd of normalizedCommands) { if (cmd.startsWith("/")) throw new Error(`Do not use / in command name (${cmd})`); } if (meta) { if (!this["~"].commandsMeta) { this["~"].commandsMeta = /* @__PURE__ */ new Map(); } for (const cmd of normalizedCommands) { this["~"].commandsMeta.set(cmd, meta); } } const macroHandler = resolvedMacroOptions ? composer.buildFromOptions(this["~"].macros, resolvedMacroOptions, handler) : null; return this.on(["message", "business_message"], (context, next) => { const entity = context.entities?.find((entity2) => { if (entity2.type !== "bot_command" || entity2.offset > 0) return false; const botInfo = context.bot.info; const cmd = context.text?.slice(1, entity2.length)?.replace( botInfo?.username ? `@${botInfo.username}` : /@[a-zA-Z0-9_]+/, "" ); return normalizedCommands.some( (normalizedCommand) => cmd === normalizedCommand ); }); if (entity) { context.args = context.text?.slice(entity.length).trim() || null; return macroHandler ? macroHandler(context, composer.noopNext) : handler(context); } return next(); }); }, startParameter(parameter, handler, macroOptions) { const macroHandler = macroOptions ? composer.buildFromOptions(this["~"].macros, macroOptions, handler) : null; if (parameter instanceof RegExp) { return this.on("message", (context, next) => { if (!context.rawStartPayload || !parameter.test(context.rawStartPayload)) return next(); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context, composer.noopNext); }); } if (Array.isArray(parameter)) { return this.on("message", (context, next) => { if (!context.rawStartPayload || !parameter.includes(context.rawStartPayload)) return next(); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context, composer.noopNext); }); } return this.on("message", (context, next) => { if (!context.rawStartPayload || context.rawStartPayload !== parameter) return next(); return macroHandler ? macroHandler(context, composer.noopNext) : handler(context, composer.noopNext); }); } }); const { Composer } = composer.createComposer({ discriminator: (ctx) => ctx.updateType, types: composer.eventTypes(), methods }); if (typeof Composer.prototype.registeredEvents !== "function") { Composer.prototype.registeredEvents = function() { const events = /* @__PURE__ */ new Set(); for (const mw of this["~"].middlewares) { if ((mw.type === "on" || mw.type === "derive") && mw.name) { for (const part of mw.name.split("|")) { const eventPart = part.includes(":") ? part.split(":")[0] : part; if (eventPart) events.add(eventPart); } } } return events; }; } { const originalExtend = Composer.prototype.extend; Composer.prototype.extend = function(other) { if ("_" in other && !(other instanceof Promise)) { if (!this["~"].__plugins) this["~"].__plugins = []; this["~"].__plugins.push(other); } const result = originalExtend.call(this, other); if (other["~"]?.commandsMeta) { if (!this["~"].commandsMeta) { this["~"].commandsMeta = /* @__PURE__ */ new Map(); } for (const [cmd, meta] of other["~"].commandsMeta) { this["~"].commandsMeta.set(cmd, meta); } } if (other["~"]?.__plugins) { if (!this["~"].__plugins) this["~"].__plugins = []; this["~"].__plugins.push(...other["~"].__plugins); } return result; }; } class Plugin { /** * @internal * Set of Plugin data * * */ _ = { /** Name of plugin */ name: "", /** List of plugin dependencies. If user does't extend from listed there dependencies it throw a error */ dependencies: [], /** remap generic type. {} in runtime */ Errors: {}, /** remap generic type. {} in runtime */ Derives: {}, /** remap generic type. {} in runtime */ Macros: {}, /** Composer */ composer: new Composer(), /** Store plugin preRequests hooks */ preRequests: [], /** Store plugin onResponses hooks */ onResponses: [], /** Store plugin onResponseErrors hooks */ onResponseErrors: [], /** Store plugin onApiCalls hooks */ onApiCalls: [], /** * Store plugin groups * * If you use `on` or `use` in group and on plugin-level groups handlers are registered after plugin-level handlers * */ groups: [], /** Store plugin onStarts hooks */ onStarts: [], /** Store plugin onStops hooks */ onStops: [], /** Store plugin onErrors hooks */ onErrors: [], /** Map of plugin errors */ errorsDefinitions: {}, decorators: {} }; /** Expose composer internals so `composer.extend(plugin)` works via duck-typing */ get "~"() { this._.composer.as("scoped"); return this._.composer["~"]; } /** Create new Plugin. Please provide `name` */ constructor(name, { dependencies } = {}) { this._.name = name; this._.composer["~"].name = name; if (dependencies) this._.dependencies = dependencies; } /** Currently not isolated!!! * * > [!WARNING] * > If you use `on` or `use` in a `group` and at the plugin level, the group handlers are registered **after** the handlers at the plugin level */ group(grouped) { this._.groups.push(grouped); return this; } /** * Register custom class-error in plugin **/ error(kind, error) { error[utils.ErrorKind] = kind; this._.errorsDefinitions[kind] = error; this._.composer["~"].errorsDefinitions[kind] = error; return this; } derive(updateNameOrHandler, handler) { if (typeof updateNameOrHandler === "function") this._.composer.derive( updateNameOrHandler ); else if (handler) this._.composer.derive( updateNameOrHandler, handler ); return this; } decorate(nameOrValue, value) { if (typeof nameOrValue === "string") this._.decorators[nameOrValue] = value; else { for (const [name, value2] of Object.entries(nameOrValue)) { this._.decorators[name] = value2; } } return this; } macro(nameOrDefs, definition) { this._.composer.macro(nameOrDefs, definition); return this; } on(updateNameOrFilter, filterOrHandler, handler) { if (typeof updateNameOrFilter === "function") { this._.composer.on(updateNameOrFilter, filterOrHandler); return this; } if (handler) { this._.composer.on( updateNameOrFilter, filterOrHandler, handler ); } else { this._.composer.on( updateNameOrFilter, filterOrHandler ); } return this; } /** Register handler to any Updates */ use(handler) { this._.composer.use(handler); return this; } preRequest(methodsOrHandler, handler) { if ((typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) && handler) this._.preRequests.push([handler, methodsOrHandler]); else if (typeof methodsOrHandler === "function") this._.preRequests.push([methodsOrHandler, void 0]); return this; } onResponse(methodsOrHandler, handler) { if ((typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) && handler) this._.onResponses.push([handler, methodsOrHandler]); else if (typeof methodsOrHandler === "function") this._.onResponses.push([methodsOrHandler, void 0]); return this; } onResponseError(methodsOrHandler, handler) { if ((typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) && handler) this._.onResponseErrors.push([handler, methodsOrHandler]); else if (typeof methodsOrHandler === "function") this._.onResponseErrors.push([methodsOrHandler, void 0]); return this; } onApiCall(methodsOrHandler, handler) { if ((typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) && handler) this._.onApiCalls.push([handler, methodsOrHandler]); else if (typeof methodsOrHandler === "function") this._.onApiCalls.push([methodsOrHandler, void 0]); return this; } /** * This hook called when the bot is `started`. * * @example * ```typescript * import { Bot } from "gramio"; * * const bot = new Bot(process.env.TOKEN!).onStart( * ({ plugins, info, updatesFrom, bot }) => { * console.log(`plugin list - ${plugins.join(", ")}`); * console.log(`bot username is @${info.username}`); * console.log(`updates from ${updatesFrom}`); * } * ); * * bot.start(); * ``` * * [Documentation](https://gramio.dev/hooks/on-start.html) * */ onStart(handler) { this._.onStarts.push(handler); return this; } /** * This hook called when the bot stops. * * @example * ```typescript * import { Bot } from "gramio"; * * const bot = new Bot(process.env.TOKEN!).onStop( * ({ plugins, info, bot }) => { * console.log(`plugin list - ${plugins.join(", ")}`); * console.log(`bot username is @${info.username}`); * } * ); * * bot.start(); * bot.stop(); * ``` * * [Documentation](https://gramio.dev/hooks/on-stop.html) * */ onStop(handler) { this._.onStops.push(handler); return this; } onError(updateNameOrHandler, handler) { if (typeof updateNameOrHandler === "function") { this._.onErrors.push(updateNameOrHandler); return this; } if (handler) { this._.onErrors.push(async (errContext) => { if (errContext.context.is(updateNameOrHandler)) await handler(errContext); }); } return this; } extend(pluginOrComposer) { if ("compose" in pluginOrComposer && "run" in pluginOrComposer && !("_" in pluginOrComposer)) { this._.composer.extend(pluginOrComposer); return this; } const plugin = pluginOrComposer; if (plugin._.composer["~"].middlewares.length) { plugin._.composer.as("scoped"); this._.composer.extend(plugin._.composer); } else if (Object.keys(plugin._.composer["~"].macros).length) { Object.assign(this._.composer["~"].macros, plugin._.composer["~"].macros); } for (const [key, value] of Object.entries(plugin._.errorsDefinitions)) { this._.errorsDefinitions[key] = value; this._.composer["~"].errorsDefinitions[key] = value; } Object.assign(this._.decorators, plugin._.decorators); this._.preRequests.push(...plugin._.preRequests); this._.onResponses.push(...plugin._.onResponses); this._.onResponseErrors.push(...plugin._.onResponseErrors); this._.onApiCalls.push(...plugin._.onApiCalls); this._.onErrors.push(...plugin._.onErrors); this._.onStarts.push(...plugin._.onStarts); this._.onStops.push(...plugin._.onStops); this._.groups.push(...plugin._.groups); for (const dep of plugin._.dependencies) { if (!this._.dependencies.includes(dep)) { this._.dependencies.push(dep); } } return this; } } for (const [name, fn] of Object.entries(methods)) { Plugin.prototype[name] = fn; } class Updates { bot; isStarted = false; isRequestActive = false; offset = 0; composer; queue; stopPollingPromiseResolve; constructor(bot, onError) { this.bot = bot; this.composer = new Composer(); this.composer.onError(async ({ error, context }) => { await onError(context, error); return true; }); this.queue = new composer.EventQueue(this.handleUpdate.bind(this)); } async handleUpdate(data) { const updateType = Object.keys(data).at(1); const UpdateContext = contexts.contextsMappings[updateType]; if (!UpdateContext) throw new Error(updateType); const updatePayload = data[updateType]; if (!updatePayload) throw new Error("Unsupported event??"); try { let context = new UpdateContext({ bot: this.bot, update: data, // @ts-expect-error payload: updatePayload, type: updateType, updateId: data.update_id }); if ("isEvent" in context && context.isEvent() && context.eventType) { const payload = data.message ?? data.edited_message ?? data.channel_post ?? data.edited_channel_post ?? data.business_message; if (!payload) throw new Error("Unsupported event??"); context = new contexts.contextsMappings[context.eventType]({ bot: this.bot, update: data, payload, // @ts-expect-error type: context.eventType, updateId: data.update_id }); } return this.composer.run( context ); } catch (_error) { throw new Error(`[UPDATES] Update type ${updateType} not supported.`); } } /** @deprecated use bot.start instead @internal */ startPolling(params = {}, options = {}) { if (this.isStarted) throw new Error("[UPDATES] Polling already started!"); this.isStarted = true; this.startFetchLoop(params, options); return; } async startFetchLoop(params = {}, options = {}) { if (options.dropPendingUpdates) await this.dropPendingUpdates(options.deleteWebhookOnConflict); while (this.isStarted) { try { this.isRequestActive = true; const updates = await utils.withRetries( () => this.bot.api.getUpdates({ timeout: 30, ...params, offset: this.offset }) ); this.isRequestActive = false; if (!this.isStarted) break; const updateId = updates.at(-1)?.update_id; this.offset = updateId ? updateId + 1 : this.offset; this.queue.addBatch(updates); } catch (error) { if (error instanceof utils.TelegramError) { if (error.code === 409 && error.message.includes("deleteWebhook")) { if (options.deleteWebhookOnConflict) await this.bot.api.deleteWebhook(); continue; } } console.error("Error received when fetching updates", error); await utils.sleep(this.bot.options.api.retryGetUpdatesWait ?? 1e3); } } this.stopPollingPromiseResolve?.(); } async dropPendingUpdates(deleteWebhookOnConflict = false) { const result = await this.bot.api.getUpdates({ // The negative offset can be specified to retrieve updates starting from *-offset* update from the end of the updates queue. // All previous updates will be forgotten. offset: -1, timeout: 0, suppress: true }); if (result instanceof utils.TelegramError) { if (result.code === 409 && result.message.includes("deleteWebhook")) { if (deleteWebhookOnConflict) { await this.bot.api.deleteWebhook({ drop_pending_updates: true }); return; } } throw result; } const lastUpdateId = result.at(-1)?.update_id; utils.debug$updates( "Dropping pending updates... Set offset to last update id %s + 1", lastUpdateId ); this.offset = lastUpdateId ? lastUpdateId + 1 : this.offset; } /** * ! Soon waitPendingRequests param default will changed to true */ stopPolling(waitPendingRequests = false) { this.isStarted = false; if (!this.isRequestActive || !waitPendingRequests) return Promise.resolve(); return new Promise((resolve) => { this.stopPollingPromiseResolve = resolve; }); } } class Bot { /** @deprecated use `~` instead*/ _ = { /** @deprecated @internal. Remap generic */ derives: {} }; /** @deprecated use `~.derives` instead @internal. Remap generic */ __Derives; "~" = this._; /** Options provided to instance */ options; /** Bot data (filled in when calling bot.init/bot.start) */ info; /** * Send API Request to Telegram Bot API * * @example * ```ts * const response = await bot.api.sendMessage({ * chat_id: "@gramio_forum", * text: "some text", * }); * ``` * * [Documentation](https://gramio.dev/bot-api.html) */ api = new Proxy({}, { get: (_target, method) => ( // @ts-expect-error // biome-ignore lint/suspicious/noAssignInExpressions: <explanation> _target[method] ??= (args) => { const callSite = new Error(); if (Error.captureStackTrace) { Error.captureStackTrace(callSite, _target[method]); } return this._callApi(method, args, callSite); } ) }); lazyloadPlugins = []; dependencies = []; errorsDefinitions = { TELEGRAM: utils.TelegramError }; errorHandler(context, error) { if (!this.hooks.onError.length) return console.error("[Default Error Handler]", context, error); return this.runImmutableHooks("onError", { context, //@ts-expect-error ErrorKind exists if user register error-class with .error("kind", SomeError); kind: error.constructor[utils.ErrorKind] ?? "UNKNOWN", error }); } /** This instance handle updates */ updates = new Updates(this, this.errorHandler.bind(this)); hooks = { preRequest: [], onResponse: [], onResponseError: [], onError: [], onStart: [], onStop: [], onApiCall: [] }; constructor(tokenOrOptions, optionsRaw) { const token = typeof tokenOrOptions === "string" ? tokenOrOptions : tokenOrOptions?.token; const options = typeof tokenOrOptions === "object" ? tokenOrOptions : optionsRaw; if (!token) throw new Error("Token is required!"); if (typeof token !== "string") throw new Error(`Token is ${typeof token} but it should be a string!`); this.options = { ...options, token, api: { baseURL: "https://api.telegram.org/bot", retryGetUpdatesWait: 1e3, ...options?.api } }; if (options?.info) { this.info = options.info; } if (!(optionsRaw?.plugins && "format" in optionsRaw.plugins && !optionsRaw.plugins.format)) this.extend( new Plugin("@gramio/format").preRequest((context) => { if (!context.params) return context; const formattable = format.FormattableMap[context.method]; if (formattable) context.params = formattable( context.params ); return context; }) ); } async runHooks(type, context) { let data = context; for await (const hook of this.hooks[type]) { data = await hook(data); } return data; } async runImmutableHooks(type, ...context) { for await (const hook of this.hooks[type]) { await hook(...context); } } async _callApi(method, params = {}, callSite) { const executeCall = async () => { const debug$api$method = debug(`gramio:api:${method}`); let url = `${this.options.api.baseURL}${this.options.token}/${this.options.api.useTest ? "test/" : ""}${method}`; const reqOptions = { method: "POST", ...this.options.api.fetchOptions, headers: new Headers(this.options.api.fetchOptions?.headers) }; const context = await this.runHooks("preRequest", { method, params }); params = context.params; if (params && files.isMediaUpload(method, params)) { if (utils.IS_BUN) { const formData = await files.convertJsonToFormData(method, params); reqOptions.body = formData; } else { const [formData, paramsWithoutFiles] = await files.extractFilesToFormData( method, params ); reqOptions.body = formData; const simplifiedParams = utils.simplifyObject(paramsWithoutFiles); url += `?${new URLSearchParams(simplifiedParams).toString()}`; } } else { reqOptions.headers.set("Content-Type", "application/json"); reqOptions.body = JSON.stringify(params); } debug$api$method("options: %j", reqOptions); const response = await fetch(url, reqOptions); const data = await response.json(); debug$api$method("response: %j", data); if (!data.ok) { const err = new utils.TelegramError(data, method, params, callSite); this.runImmutableHooks("onResponseError", err, this.api); if (!params?.suppress) throw err; return err; } this.runImmutableHooks("onResponse", { method, params, response: data.result }); return data.result; }; if (!this.hooks.onApiCall.length) return executeCall(); let fn = executeCall; for (const hook of [...this.hooks.onApiCall].reverse()) { const prev = fn; fn = () => hook({ method, params }, prev); } return fn(); } downloadFile(attachment, path) { const input = attachment; return path ? files.downloadFile(this, input, path) : files.downloadFile(this, input); } /** * Get a shareable download link for a file. * * When {@link BotOptions.files | `files.baseURL`} is set (e.g. a local Bot API * server with the bundled file server), the link is **token-less and path-based** * — safe to hand to users. Otherwise it falls back to the classic * `…/file/bot<token>/<path>` URL (which contains the bot token). * * @example * ```ts * const link = await bot.getFileLink(ctx.document.fileId); * await ctx.reply(`Download: ${link}`); * ``` */ getFileLink(attachment) { return files.downloadFile(this, attachment).link(); } /** * Register custom class-error for type-safe catch in `onError` hook * * @example * ```ts * export class NoRights extends Error { * needRole: "admin" | "moderator"; * * constructor(role: "admin" | "moderator") { * super(); * this.needRole = role; * } * } * * const bot = new Bot(process.env.TOKEN!) * .error("NO_RIGHTS", NoRights) * .onError(({ context, kind, error }) => { * if (context.is("message") && kind === "NO_RIGHTS") * return context.send( * format`You don't have enough rights! You need to have an «${bold( * error.needRole * )}» role.` * ); * }); * * bot.updates.on("message", (context) => { * if (context.text === "bun") throw new NoRights("admin"); * }); * ``` */ error(kind, error) { error[utils.ErrorKind] = kind; this.errorsDefinitions[kind] = error; return this; } onError(updateNameOrHandler, handler) { if (typeof updateNameOrHandler === "function") { this.hooks.onError.push(updateNameOrHandler); return this; } if (handler) { this.hooks.onError.push(async (errContext) => { if (errContext.context.is(updateNameOrHandler)) await handler(errContext); }); } return this; } derive(updateNameOrHandler, handler) { if (typeof updateNameOrHandler === "function") this.updates.composer.derive( updateNameOrHandler ); else if (handler) this.updates.composer.derive( updateNameOrHandler, handler ); return this; } decorate(nameOrRecordValue, value) { this.updates.composer.decorate( typeof nameOrRecordValue === "string" ? { [nameOrRecordValue]: value } : nameOrRecordValue ); return this; } macro(nameOrDefs, definition) { this.updates.composer.macro(nameOrDefs, definition); return this; } /** * This hook called when the bot is `started`. * * @example * ```typescript * import { Bot } from "gramio"; * * const bot = new Bot(process.env.TOKEN!).onStart( * ({ plugins, info, updatesFrom, bot }) => { * console.log(`plugin list - ${plugins.join(", ")}`); * console.log(`bot username is @${info.username}`); * console.log(`updates from ${updatesFrom}`); * } * ); * * bot.start(); * ``` * * [Documentation](https://gramio.dev/hooks/on-start.html) * */ onStart(handler) { this.hooks.onStart.push(handler); return this; } /** * This hook called when the bot stops. * * @example * ```typescript * import { Bot } from "gramio"; * * const bot = new Bot(process.env.TOKEN!).onStop( * ({ plugins, info, bot }) => { * console.log(`plugin list - ${plugins.join(", ")}`); * console.log(`bot username is @${info.username}`); * } * ); * * bot.start(); * bot.stop(); * ``` * * [Documentation](https://gramio.dev/hooks/on-stop.html) * */ onStop(handler) { this.hooks.onStop.push(handler); return this; } preRequest(methodsOrHandler, handler) { if (typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) { if (!handler) throw new Error("TODO:"); const methods = typeof methodsOrHandler === "string" ? [methodsOrHandler] : methodsOrHandler; this.hooks.preRequest.push((async (context) => { if (methods.includes(context.method)) return handler(context); return context; })); } else this.hooks.preRequest.push(methodsOrHandler); return this; } onResponse(methodsOrHandler, handler) { if (typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) { if (!handler) throw new Error("TODO:"); const methods = typeof methodsOrHandler === "string" ? [methodsOrHandler] : methodsOrHandler; this.hooks.onResponse.push((async (context) => { if (methods.includes(context.method)) return handler(context); })); } else this.hooks.onResponse.push(methodsOrHandler); return this; } onResponseError(methodsOrHandler, handler) { if (typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) { if (!handler) throw new Error("TODO:"); const methods = typeof methodsOrHandler === "string" ? [methodsOrHandler] : methodsOrHandler; this.hooks.onResponseError.push((async (context, api) => { if (methods.includes(context.method)) return handler(context, api); })); } else this.hooks.onResponseError.push( methodsOrHandler ); return this; } onApiCall(methodsOrHandler, handler) { if (typeof methodsOrHandler === "string" || Array.isArray(methodsOrHandler)) { if (!handler) throw new Error("TODO:"); const methods = typeof methodsOrHandler === "string" ? [methodsOrHandler] : methodsOrHandler; this.hooks.onApiCall.push((async (context, next) => { if (methods.includes(context.method)) return handler(context, next); return next(); })); } else { this.hooks.onApiCall.push(methodsOrHandler); } return this; } on(updateNameOrFilter, filterOrHandler, handler) { if (typeof updateNameOrFilter === "function") { this.updates.composer.on( updateNameOrFilter, filterOrHandler ); return this; } if (handler) { this.updates.composer.on( updateNameOrFilter, filterOrHandler, handler ); } else { this.updates.composer.on( updateNameOrFilter, filterOrHandler ); } return this; } /** Register handler to any Updates */ use(handler) { this.updates.composer.use(handler); return this; } extend(pluginOrComposer) { if ("compose" in pluginOrComposer && "run" in pluginOrComposer && !("_" in pluginOrComposer)) { this.updates.composer.extend(pluginOrComposer); const trackedPlugins = pluginOrComposer["~"]?.__plugins; if (trackedPlugins) { for (const p of trackedPlugins) { this.decorate(p._.decorators); for (const value of p._.preRequests) { const [preRequest, updateName] = value; if (!updateName) this.preRequest(preRequest); else this.preRequest(updateName, preRequest); } for (const value of p._.onResponses) { const [onResponse, updateName] = value; if (!updateName) this.onResponse(onResponse); else this.onResponse(updateName, onResponse); } for (const value of p._.onResponseErrors) { const [onResponseError, updateName] = value; if (!updateName) this.onResponseError(onResponseError); else this.onResponseError(updateName, onResponseError); } for (const value of p._.onApiCalls) { const [onApiCall, methods] = value; if (!methods) this.onApiCall(onApiCall); else this.onApiCall(methods, onApiCall); } for (const handler of p._.groups) { this.group(handler); } for (const value of p._.onErrors) { this.onError(value); } for (const value of p._.onStarts) { this.onStart(value); } for (const value of p._.onStops) { this.onStop(value); } this.dependencies.push(p._.name); } } return this; } const plugin = pluginOrComposer; if (plugin instanceof Promise) { this.lazyloadPlugins.push(plugin); return this; } if (plugin._.dependencies.some((dep) => !this.dependencies.includes(dep))) throw new Error( `The \xAB${plugin._.name}\xBB plugin needs dependencies registered before: ${plugin._.dependencies.filter((dep) => !this.dependencies.includes(dep)).join(", ")}` ); if (plugin._.composer["~"].middlewares.length) { plugin._.composer.as("scoped"); this.updates.composer.extend(plugin._.composer); } else if (Object.keys(plugin._.composer["~"].macros).length) { Object.assign( this.updates.composer["~"].macros, plugin._.composer["~"].macros ); } this.decorate(plugin._.decorators); for (const [key, value] of Object.entries(plugin._.errorsDefinitions)) { if (this.errorsDefinitions[key]) this.errorsDefinitions[key] = value; } for (const value of plugin._.preRequests) { const [preRequest, updateName] = value; if (!updateName) this.preRequest(preRequest); else this.preRequest(updateName, preRequest); } for (const value of plugin._.onResponses) { const [onResponse, updateName] = value; if (!updateName) this.onResponse(onResponse); else this.onResponse(updateName, onResponse); } for (const value of plugin._.onResponseErrors) { const [onResponseError, updateName] = value; if (!updateName) this.onResponseError(onResponseError); else this.onResponseError(updateName, onResponseError); } for (const value of plugin._.onApiCalls) { const [onApiCall, methods] = value; if (!methods) this.onApiCall(onApiCall); else this.onApiCall(methods, onApiCall); } for (const handler of plugin._.groups) { this.group(handler); } for (const value of plugin._.onErrors) { this.onError(value); } for (const value of plugin._.onStarts) { this.onStart(value); } for (const value of plugin._.onStops) { this.onStop(value); } this.dependencies.push(plugin._.name); return this; } /** * Register handler to reaction (`message_reaction` update) * * @example * ```ts * new Bot().reaction("👍", async (context) => { * await context.reply(`Thank you!`); * }); * ``` * */ reaction(trigger, handler, options) { this.updates.composer.reaction(trigger, handler, options); return this; } /** * Register handler to `callback_query` event * * @example * ```ts * const someData = new CallbackData("example").number("id"); * * new Bot() * .command("start", (context) => * context.send("some", { * reply_markup: new InlineKeyboard().text( * "example", * someData.pack({ * id: 1, * }) * ), * }) * ) * .callbackQuery(someData, (context) => { * context.queryData; // is type-safe * }); * ``` */ callbackQuery(trigger, handler, options) { this.updates.composer.callbackQuery( trigger, handler, options ); return this; } /** * Register handler to `chosen_inline_result` update * * Accepts a `CallbackData` schema for type-safe filtering on `result_id`: * * @example * ```ts * const trackRef = new CallbackData("track").string("src").string("id"); * * new Bot() * .on("inline_query", async (ctx) => { * await ctx.answer(tracks.map((t) => ({ * type: "audio", * id: trackRef.pack({ src: t.source, id: t.id }), * audio_url: t.url, * title: t.title, * }))); * }) * .chosenInlineResult(trackRef, (ctx) => { * ctx.queryData; // { src: string; id: string } * }); * ``` * * String/RegExp/predicate triggers filter on `context.query` (the user's * typed text); the `CallbackData` schema filters on `context.resultId`. */ chosenInlineResult(trigger, handler, options) { this.updates.composer.chosenInlineResult( trigger, handler, options ); return this; } inlineQuery(triggerOrHandler, handler, options) { this.updates.composer.inlineQuery( triggerOrHandler, handler, options ); return this; } guestQuery(triggerOrHandler, handler, options) { this.updates.composer.guestQuery(triggerOrHandler, handler, options); return this; } /** * Register handler to `message` and `business_message` event * * @example * ```ts * new Bot().hears(/regular expression with (.*)/i, async (context) => { * if (context.args) await context.send(`Params ${context.args[1]}`); * }); * ``` */ hears(trigger, handler, options) { this.updates.composer.hears(trigger, handler, options); return this; } command(command, handlerOrMeta, handlerOrOptions, macroOptions) { this.updates.composer.command(command, handlerOrMeta, handlerOrOptions, macroOptions); return this; } /** * Register handler to `start` command when start parameter is matched * * @example * ```ts * new Bot().startParameter(/^ref_(.+)$/, (context) => { * return context.send(`Reference: ${context.rawStartPayload}`); * }); * ``` */ startParameter(parameter, handler, options) { this.updates.composer.startParameter( parameter, handler, options ); return this; } /** Currently not isolated!!! */ group(grouped) { return grouped(this); } /** * Sync registered command metadata with the Telegram API. * * Groups commands by `{scope, language_code}` and calls `setMyCommands` for each group. * When a `storage` is provided, hashes each payload and skips unchanged groups. * * @example * ```ts * bot.onStart(() => bot.syncCommands()); * ``` */ async syncCommands(options) { const commandsMeta = this.updates.composer["~"].commandsMeta ?? /* @__PURE__ */ new Map(); if (commandsMeta.size === 0) return; const storage = options?.storage; const botId = this.info?.id; const excludeSet = options?.exclude ? new Set(options.exclude) : void 0; const groups = /* @__PURE__ */ new Map(); const getOrCreateGroup = (scope, langCode) => { const key = `${JSON.stringify(scope ?? { type: "default" })}:${langCode}`; let group = groups.get(key); if (!group) { group = { scope, language_code: langCode || void 0, commands: [] }; groups.set(key, group); } return group; }; for (const [name, meta] of commandsMeta) { if (meta.hide) continue; if (excludeSet?.has(name)) continue; const scopes = meta.scopes ? meta.scopes.map( (s) => typeof s === "string" ? { type: s } : s ) : [void 0]; for (const scope of scopes) { getOrCreateGroup(scope, "").commands.push({ command: name, description: meta.description }); if (meta.locales) { for (const [lang, desc] of Object.entries(meta.locales)) { getOrCreateGroup(scope, lang).commands.push({ command: name, description: desc }); } } } } for (const [key, group] of groups) { if (group.commands.length > 100) { throw new Error( `Too many commands (${group.commands.length}) for scope ${key}. Telegram allows max 100 per scope+language.` ); } } for (const [, group] of groups) { const payload = { commands: group.commands, scope: group.scope, language_code: group.language_code }; if (storage) { const hash = utils.simpleHash(JSON.stringify(payload)); const storageKey = `gramio:commands:${botId ?? "unknown"}:${JSON.stringify(group.scope ?? { type: "default" })}:${group.language_code ?? ""}`; const stored = await storage.get(storageKey); if (stored === hash) continue; await this.api.setMyCommands(payload); await storage.set(storageKey, hash); } else { await this.api.setMyCommands(payload); } } if (options?.cleanUnusedScopes) { const declaredScopes = /* @__PURE__ */ new Set(); for (const [, group] of groups) { declaredScopes.add(JSON.stringify(group.scope ?? { type: "default" })); } const allScopes = [ { type: "default" }, { type: "all_private_chats" }, { type: "all_group_chats" }, { type: "all_chat_administrators" } ]; for (const scope of allScopes) { if (!declaredScopes.has(JSON.stringify(scope))) { await this.api.deleteMyCommands({ scope }); } } } } /** * Init bot. Call it manua