gramio
Version:
Powerful, extensible and really type-safe Telegram Bot API framework
1,490 lines (1,485 loc) • 65.9 kB
JavaScript
export * from '@gramio/callback-data';
import { contextsMappings } from '@gramio/contexts';
export * from '@gramio/contexts';
import { downloadFile, isMediaUpload, convertJsonToFormData, extractFilesToFormData } from '@gramio/files';
export * from '@gramio/files';
import { FormattableMap } from '@gramio/format';
export * from '@gramio/format';
export * from '@gramio/keyboards';
import debug from 'debug';
import { E as ErrorKind, w as withRetries, T as TelegramError, s as sleep, d as debug$updates, a as simpleHash, I as IS_BUN, b as simplifyObject, t as timeoutWebhook } from './utils-1ejkKLIB.js';
import { defineComposerMethods, buildFromOptions, noopNext, createComposer, eventTypes, EventQueue } from '@gramio/composer';
export { EventQueue, buildFromOptions, compose, noopNext, skip, stop } from '@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(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 = defineComposerMethods({
reaction(trigger, handler, macroOptions) {
const reactions = Array.isArray(trigger) ? trigger : [trigger];
const macroHandler = macroOptions ? 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, noopNext) : handler(context);
}
);
},
callbackQuery(trigger, handler, macroOptions) {
const macroHandler = macroOptions ? 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, 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, 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, noopNext) : handler(context);
});
},
chosenInlineResult(trigger, handler, macroOptions) {
const macroHandler = macroOptions ? 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, 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, 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, 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, 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 ? 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, 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, noopNext) : handler(context);
});
}
return this.on("inline_query", (context, next) => {
if (!trigger(context)) return next();
context.args = null;
return macroHandler ? macroHandler(context, noopNext) : handler(context);
});
},
guestQuery(triggerOrHandler, maybeHandler, macroOptions) {
const trigger = maybeHandler === void 0 ? (() => true) : triggerOrHandler;
const handler = maybeHandler ?? triggerOrHandler;
const macroHandler = macroOptions ? 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, 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, noopNext) : handler(context);
});
}
return this.on("guest_message", (context, next) => {
if (!trigger(context)) return next();
context.args = null;
return macroHandler ? macroHandler(context, noopNext) : handler(context);
});
},
hears(trigger, handler, macroOptions) {
const macroHandler = macroOptions ? 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, 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, 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, noopNext) : handler(context);
});
}
return this.on("message", (context, next) => {
if (typeof trigger !== "function" || !trigger(context)) return next();
context.args = null;
return macroHandler ? macroHandler(context, 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 ? 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, noopNext) : handler(context);
}
return next();
});
},
startParameter(parameter, handler, macroOptions) {
const macroHandler = macroOptions ? 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, noopNext) : handler(context, noopNext);
});
}
if (Array.isArray(parameter)) {
return this.on("message", (context, next) => {
if (!context.rawStartPayload || !parameter.includes(context.rawStartPayload))
return next();
return macroHandler ? macroHandler(context, noopNext) : handler(context, noopNext);
});
}
return this.on("message", (context, next) => {
if (!context.rawStartPayload || context.rawStartPayload !== parameter) return next();
return macroHandler ? macroHandler(context, noopNext) : handler(context, noopNext);
});
}
});
const { Composer } = createComposer({
discriminator: (ctx) => ctx.updateType,
types: 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[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 EventQueue(this.handleUpdate.bind(this));
}
async handleUpdate(data) {
const updateType = Object.keys(data).at(1);
const UpdateContext = 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 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 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 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 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 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;
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: 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[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 = 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 && isMediaUpload(method, params)) {
if (IS_BUN) {
const formData = await convertJsonToFormData(method, params);
reqOptions.body = formData;
} else {
const [formData, paramsWithoutFiles] = await extractFilesToFormData(
method,
params
);
reqOptions.body = formData;
const simplifiedParams = 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 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 ? downloadFile(this, input, path) : 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 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[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 = 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 manually only i