From 5168b0a13d6cdad72c773429e6559a8887dc7733 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sun, 20 Sep 2026 00:47:55 +0800 Subject: [PATCH 1/2] Create bot.ts --- src/bot.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/bot.ts diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1 @@ + From 3d17459b9d0f71197e7b79ecf25c33e21fe1a2be Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sun, 20 Sep 2026 00:51:46 +0800 Subject: [PATCH 2/2] Add files via upload --- src/README.md | 41 + src/api.ts | 4219 ++++++++++++++++++++++++++++++++++++ src/bot.ts | 759 +++++++ src/client.ts | 525 +++++ src/composer.ts | 1038 +++++++++ src/constants.ts | 115 + src/context.ts | 4825 ++++++++++++++++++++++++++++++++++++++++++ src/error.ts | 106 + src/filter.ts | 797 +++++++ src/frameworks.ts | 660 ++++++ src/inline_query.ts | 733 +++++++ src/input_media.ts | 105 + src/keyboard.ts | 1328 ++++++++++++ src/mod.ts | 60 + src/payload.ts | 227 ++ src/platform.deno.ts | 29 + src/platform.node.ts | 49 + src/platform.web.ts | 23 + src/session.ts | 732 +++++++ src/shim.node.ts | 2 + src/types.deno.ts | 456 ++++ src/types.node.ts | 440 ++++ src/types.ts | 1 + src/types.web.ts | 409 ++++ src/webhook.ts | 216 ++ 25 files changed, 17895 insertions(+) create mode 100644 src/README.md create mode 100644 src/api.ts create mode 100644 src/client.ts create mode 100644 src/composer.ts create mode 100644 src/constants.ts create mode 100644 src/context.ts create mode 100644 src/error.ts create mode 100644 src/filter.ts create mode 100644 src/frameworks.ts create mode 100644 src/inline_query.ts create mode 100644 src/input_media.ts create mode 100644 src/keyboard.ts create mode 100644 src/mod.ts create mode 100644 src/payload.ts create mode 100644 src/platform.deno.ts create mode 100644 src/platform.node.ts create mode 100644 src/platform.web.ts create mode 100644 src/session.ts create mode 100644 src/shim.node.ts create mode 100644 src/types.deno.ts create mode 100644 src/types.node.ts create mode 100644 src/types.ts create mode 100644 src/types.web.ts create mode 100644 src/webhook.ts diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..e2d19f5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,41 @@ +# grammY + +The grammY module lets you easily write Telegram bots. Here is a quickstart for you to get started, but note that a better explanation is [in our repo on GitHub](https://github.com/grammyjs/grammY). + +You may also want to check out the [docs](https://grammy.dev). + +## Quickstart + +Talk to [@BotFather](https://t.me/BotFather) to create a new Telegram bot and obtain a _bot token_. + +Paste the following code into a new file `bot.ts`. + +```ts +import { Bot } from "https://deno.land/x/grammy/mod.ts"; + +// Create bot object +const bot = new Bot(""); // <-- place your bot token inside this string + +// Listen for messages +bot.command("start", (ctx) => ctx.reply("Welcome! Send me a photo!")); +bot.on("message:text", (ctx) => ctx.reply("That is text and not a photo!")); +bot.on("message:photo", (ctx) => ctx.reply("Nice photo! Is that you?")); +bot.on( + "edited_message", + (ctx) => + ctx.reply("Ha! Gotcha! You just edited this!", { + reply_parameters: { message_id: ctx.editedMessage.message_id }, + }), +); + +// Launch! +bot.start(); +``` + +**Congratulations!** You have successfully created your first Telegram bot. + +You can run it like so: + +```bash +deno run --allow-net bot.ts +``` diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..1c58515 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,4219 @@ +// deno-lint-ignore-file camelcase +import { + type AcceptedGiftTypes, + type BotCommand, + type ChatPermissions, + type InlineQueryResult, + type InputChecklist, + type InputFile, + type InputMedia, + type InputMediaAudio, + type InputMediaDocument, + type InputMediaLivePhoto, + type InputMediaPhoto, + type InputMediaVideo, + type InputMediaWithoutUpload, + type InputPaidMedia, + type InputPollOption, + type InputProfilePhoto, + type InputRichMessage, + type InputRichMessageWithoutUpload, + type InputSticker, + type InputStoryContent, + type KeyboardButton, + type LabeledPrice, + type MaskPosition, + type PassportElementError, + type ReactionType, +} from "../types.ts"; +import { + type ApiClientOptions, + createRawApi, + type Methods, + type Payload, + type RawApi, + type Transformer, + type TransformerConsumer, + type WebhookReplyEnvelope, +} from "./client.ts"; + +/** + * Helper type to derive remaining properties of a given API method call M, + * given that some properties X have already been specified. + */ +export type Other< + R extends RawApi, + M extends Methods, + X extends string = never, +> = Omit, X>; +/** + * This class provides access to the full Telegram Bot API. All methods of the + * API have an equivalent on this class, with the most important parameters + * pulled up into the function signature, and the other parameters captured by + * an object. + * + * In addition, this class has a property `raw` that provides raw access to the + * complete Telegram API, with the method signatures 1:1 represented as + * documented on the website (https://core.telegram.org/bots/api). + * + * Every method takes an optional `AbortSignal` object that allows you to cancel + * the request if desired. + * + * In advanced use cases, this class allows to install transformers that can + * modify the method and payload on the fly before sending it to the Telegram + * servers. Confer the `config` property for this. + */ +export class Api { + /** + * Provides access to all methods of the Telegram Bot API exactly as + * documented on the website (https://core.telegram.org/bots/api). No + * arguments are pulled up in the function signature for convenience. + * + * If you suppress compiler warnings, this also allows for raw api calls to + * undocumented methods with arbitrary parametersβ€”use only if you know what + * you are doing. + */ + public readonly raw: R; + + /** + * Configuration object for the API instance, used as a namespace to + * separate those API operations that are related to grammY from methods of + * the Telegram Bot API. Contains advanced options! + */ + public readonly config: { + /** + * Allows to install an API request transformer function. A transformer + * function has access to every API call before it is being performed. + * This includes the method as string, the payload as object and the + * upstream transformer function. + * + * _Note that using transformer functions is an advanced feature of + * grammY that most bots will not need to make use of._ + */ + readonly use: TransformerConsumer; + /** + * Provides read access to all currently installed transformers (those + * that have previously been passed to `config.use`). + * + * _Note that using transformer functions is an advanced feature of + * grammY that most bots will not need to make use of._ + */ + readonly installedTransformers: () => Transformer[]; + }; + + /** + * Constructs a new instance of `Api`. It is independent from all other + * instances of this class. For example, this lets you install a custom set + * of transformers. + * + * @param token Bot API token obtained from [@BotFather](https://t.me/BotFather) + * @param options Optional API client options for the underlying client instance + * @param webhookReplyEnvelope Optional envelope to handle webhook replies + */ + constructor( + public readonly token: string, + public readonly options?: ApiClientOptions, + webhookReplyEnvelope?: WebhookReplyEnvelope, + ) { + const { raw, use, installedTransformers } = createRawApi( + token, + options, + webhookReplyEnvelope, + ); + this.raw = raw; + this.config = { + use, + installedTransformers: () => installedTransformers.slice(), + }; + } + + /** + * Use this method to receive incoming updates using long polling (wiki). Returns an Array of Update objects. + * + * Notes + * 1. This method will not work if an outgoing webhook is set up. + * 2. In order to avoid getting duplicate updates, recalculate offset after each server response. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getupdates + */ + getUpdates(other?: Other, signal?: AbortSignal) { + return this.raw.getUpdates({ ...other }, signal); + } + + /** + * Use this method to specify a URL and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified URL, containing a JSON-serialized Update. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns True on success. + * + * If you'd like to make sure that the webhook was set by you, you can specify secret data in the parameter secret_token. If specified, the request will contain a header β€œX-Telegram-Bot-Api-Secret-Token” with the secret token as content. + * + * Notes + * 1. You will not be able to receive updates using getUpdates for as long as an outgoing webhook is set up. + * 2. To use a self-signed certificate, you need to upload your public key certificate using certificate parameter. Please upload as InputFile, sending a String will not work. + * 3. Ports currently supported for Webhooks: 443, 80, 88, 8443. + * + * If you're having any trouble setting up webhooks, please check out this amazing guide to webhooks. + * + * @param url HTTPS url to send updates to. Use an empty string to remove webhook integration. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setwebhook + */ + setWebhook( + url: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setWebhook({ url, ...other }, signal); + } + + /** + * Use this method to remove webhook integration if you decide to switch back to getUpdates. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletewebhook + */ + deleteWebhook(other?: Other, signal?: AbortSignal) { + return this.raw.deleteWebhook({ ...other }, signal); + } + + /** + * Use this method to get current webhook status. Requires no parameters. On success, returns a WebhookInfo object. If the bot is using getUpdates, will return an object with the url field empty. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getwebhookinfo + */ + getWebhookInfo(signal?: AbortSignal) { + return this.raw.getWebhookInfo(signal); + } + + /** + * A simple method for testing your bot's authentication token. Requires no parameters. Returns basic information about the bot in form of a User object. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getme + */ + getMe(signal?: AbortSignal) { + return this.raw.getMe(signal); + } + + /** + * Use this method to log out from the cloud Bot API server before launching the bot locally. You must log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates. After a successful call, you can immediately log in on a local server, but will not be able to log in back to the cloud Bot API server for 10 minutes. Returns True on success. Requires no parameters. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#logout + */ + logOut(signal?: AbortSignal) { + return this.raw.logOut(signal); + } + + /** + * Use this method to close the bot instance before moving it from one local server to another. You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart. The method will return error 429 in the first 10 minutes after the bot is launched. Returns True on success. Requires no parameters. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#close + */ + close(signal?: AbortSignal) { + return this.raw.close(signal); + } + + /** + * Use this method to send text messages. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param text Text of the message to be sent, 1-4096 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmessage + */ + sendMessage( + chat_id: number | string, + text: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendMessage({ chat_id, text, ...other }, signal); + } + + /** + * Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format \@username + * @param rich_message The message to be sent + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendrichmessage + */ + sendRichMessage( + chat_id: number | string, + rich_message: InputRichMessage, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendRichMessage( + { chat_id, rich_message, ...other }, + signal, + ); + } + + /** + * Use this method to forward messages of any kind. Service messages and messages with protected content can't be forwarded. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param from_chat_id Unique identifier for the chat where the original message was sent (or username of the target bot, supergroup or channel in the format `@username`) + * @param message_id Message identifier in the chat specified in from_chat_id + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#forwardmessage + */ + forwardMessage( + chat_id: number | string, + from_chat_id: number | string, + message_id: number, + other?: Other< + R, + "forwardMessage", + "chat_id" | "from_chat_id" | "message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.forwardMessage( + { chat_id, from_chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an Array of MessageId of the sent messages is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param from_chat_id Unique identifier for the chat where the original messages were sent (or username of the target bot, supergroup or channel in the format `@username`) + * @param message_ids A list of 1-100 identifiers of messages in the chat from_chat_id to forward. The identifiers must be specified in a strictly increasing order. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#forwardmessages + */ + forwardMessages( + chat_id: number | string, + from_chat_id: number | string, + message_ids: number[], + other?: Other< + R, + "forwardMessages", + "chat_id" | "from_chat_id" | "message_ids" + >, + signal?: AbortSignal, + ) { + return this.raw.forwardMessages({ + chat_id, + from_chat_id, + message_ids, + ...other, + }, signal); + } + + /** + * Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param from_chat_id Unique identifier for the chat where the original message was sent (or username of the target bot, supergroup or channel in the format `@username`) + * @param message_id Message identifier in the chat specified in from_chat_id + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#copymessage + */ + copyMessage( + chat_id: number | string, + from_chat_id: number | string, + message_id: number, + other?: Other< + R, + "copyMessage", + "chat_id" | "from_chat_id" | "message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.copyMessage( + { chat_id, from_chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an Array of MessageId of the sent messages is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param from_chat_id Unique identifier for the chat where the original messages were sent (or username of the target bot, supergroup or channel in the format `@username`) + * @param message_ids A list of 1-100 identifiers of messages in the chat from_chat_id to copy. The identifiers must be specified in a strictly increasing order. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#copymessages + */ + copyMessages( + chat_id: number | string, + from_chat_id: number | string, + message_ids: number[], + other?: Other< + R, + "copyMessages", + "chat_id" | "from_chat_id" | "message_ids" + >, + signal?: AbortSignal, + ) { + return this.raw.copyMessages({ + chat_id, + from_chat_id, + message_ids, + ...other, + }, signal); + } + + /** + * Use this method to send photos. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param photo Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. The photo must be at most 10 MB in size. The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendphoto + */ + sendPhoto( + chat_id: number | string, + photo: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendPhoto({ chat_id, photo, ...other }, signal); + } + + /** + * Use this method to send live photos. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param live_photo Live photo video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. Sending live photos by a URL is currently unsupported. + * @param photo The static photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. Sending live photos by a URL is currently unsupported. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendlivephoto + */ + sendLivePhoto( + chat_id: number | string, + live_photo: InputFile | string, + photo: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendLivePhoto( + { chat_id, live_photo, photo, ...other }, + signal, + ); + } + + /** + * Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent Message is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. + * + * For sending voice messages, use the sendVoice method instead. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param audio Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendaudio + */ + sendAudio( + chat_id: number | string, + audio: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendAudio({ chat_id, audio, ...other }, signal); + } + + /** + * Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param document File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#senddocument + */ + sendDocument( + chat_id: number | string, + document: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendDocument({ chat_id, document, ...other }, signal); + } + + /** + * Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). On success, the sent Message is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param video Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvideo + */ + sendVideo( + chat_id: number | string, + video: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendVideo({ chat_id, video, ...other }, signal); + } + + /** + * Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent Message is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param animation Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendanimation + */ + sendAnimation( + chat_id: number | string, + animation: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendAnimation({ chat_id, animation, ...other }, signal); + } + + /** + * Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param voice Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvoice + */ + sendVoice( + chat_id: number | string, + voice: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendVoice({ chat_id, voice, ...other }, signal); + } + + /** + * Use this method to send video messages. On success, the sent Message is returned. + * As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param video_note Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data.. Sending video notes by a URL is currently unsupported + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvideonote + */ + sendVideoNote( + chat_id: number | string, + video_note: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendVideoNote( + { chat_id, video_note, ...other }, + signal, + ); + } + + /** + * Use this method to send paid media. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param star_count The number of Telegram Stars that must be paid to buy access to the media + * @param media An Array describing the media to be sent; up to 10 items + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendpaidmedia + */ + sendPaidMedia( + chat_id: number | string, + star_count: number, + media: InputPaidMedia[], + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendPaidMedia( + { chat_id, star_count, media, ...other }, + signal, + ); + } + + /** + * Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an Array of Message objects that were sent is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param media An Array describing messages to be sent, must include 2-10 items + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmediagroup + */ + sendMediaGroup( + chat_id: number | string, + media: + | ReadonlyArray + | ReadonlyArray + | ReadonlyArray< + | InputMediaLivePhoto + | InputMediaPhoto + | InputMediaVideo + >, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendMediaGroup({ chat_id, media, ...other }, signal); + } + + /** + * Use this method to send point on the map. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param latitude Latitude of the location + * @param longitude Longitude of the location + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendlocation + */ + sendLocation( + chat_id: number | string, + latitude: number, + longitude: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendLocation( + { chat_id, latitude, longitude, ...other }, + signal, + ); + } + + /** + * Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to stopMessageLiveLocation. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to edit + * @param latitude Latitude of new location + * @param longitude Longitude of new location + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagelivelocation + */ + editMessageLiveLocation( + chat_id: number | string, + message_id: number, + latitude: number, + longitude: number, + other?: Other< + R, + "editMessageLiveLocation", + | "chat_id" + | "message_id" + | "inline_message_id" + | "latitude" + | "longitude" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageLiveLocation( + { chat_id, message_id, latitude, longitude, ...other }, + signal, + ); + } + + /** + * Use this method to edit live location inline messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to stopMessageLiveLocation. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param inline_message_id Identifier of the inline message + * @param latitude Latitude of new location + * @param longitude Longitude of new location + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagelivelocation + */ + editMessageLiveLocationInline( + inline_message_id: string, + latitude: number, + longitude: number, + other?: Other< + R, + "editMessageLiveLocation", + | "chat_id" + | "message_id" + | "inline_message_id" + | "latitude" + | "longitude" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageLiveLocation( + { inline_message_id, latitude, longitude, ...other }, + signal, + ); + } + + /** + * Use this method to stop updating a live location message before live_period expires. On success, if the message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message with live location to stop + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#stopmessagelivelocation + */ + stopMessageLiveLocation( + chat_id: number | string, + message_id: number, + other?: Other< + R, + "stopMessageLiveLocation", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.stopMessageLiveLocation( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to stop updating a live location message before live_period expires. On success, if the message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param inline_message_id Identifier of the inline message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#stopmessagelivelocation + */ + stopMessageLiveLocationInline( + inline_message_id: string, + other?: Other< + R, + "stopMessageLiveLocation", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.stopMessageLiveLocation( + { inline_message_id, ...other }, + signal, + ); + } + + /** + * Use this method to send information about a venue. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param latitude Latitude of the venue + * @param longitude Longitude of the venue + * @param title Name of the venue + * @param address Address of the venue + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvenue + */ + sendVenue( + chat_id: number | string, + latitude: number, + longitude: number, + title: string, + address: string, + other?: Other< + R, + "sendVenue", + "chat_id" | "latitude" | "longitude" | "title" | "address" + >, + signal?: AbortSignal, + ) { + return this.raw.sendVenue( + { chat_id, latitude, longitude, title, address, ...other }, + signal, + ); + } + + /** + * Use this method to send phone contacts. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param phone_number Contact's phone number + * @param first_name Contact's first name + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendcontact + */ + sendContact( + chat_id: number | string, + phone_number: string, + first_name: string, + other?: Other< + R, + "sendContact", + "chat_id" | "phone_number" | "first_name" + >, + signal?: AbortSignal, + ) { + return this.raw.sendContact( + { chat_id, phone_number, first_name, ...other }, + signal, + ); + } + + /** + * Use this method to send a native poll. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param question Poll question, 1-300 characters + * @param options A list of answer options, 1-12 strings 1-100 characters each + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendpoll + */ + sendPoll( + chat_id: number | string, + question: string, + options: (string | InputPollOption)[], + other?: Other, + signal?: AbortSignal, + ) { + const opts = options.map((o) => + typeof o === "string" ? { text: o } : o + ); + return this.raw.sendPoll( + { chat_id, question, options: opts, ...other }, + signal, + ); + } + + /** + * Use this method to send a checklist on behalf of a connected business account. On success, the sent Message is returned. + * + * @param business_connection_id Unique identifier of the business connection on behalf of which the message will be sent + * @param chat_id Unique identifier for the target chat or username of the target bot in the format `@username` + * @param checklist An object for the checklist to send + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchecklist + */ + sendChecklist( + business_connection_id: string, + chat_id: number | string, + checklist: InputChecklist, + other?: Other< + R, + "sendChecklist", + "business_connection_id" | "chat_id" | "checklist" + >, + signal?: AbortSignal, + ) { + return this.raw.sendChecklist({ + business_connection_id, + chat_id, + checklist, + ...other, + }, signal); + } + + /** + * Use this method to edit a checklist on behalf of a connected business account. On success, the edited Message is returned. + * + * @param business_connection_id Unique identifier of the business connection on behalf of which the message will be sent + * @param chat_id Unique identifier for the target chat or username of the target bot in the format `@username` + * @param message_id Unique identifier for the target message + * @param checklist An object for the new checklist + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagechecklist + */ + editMessageChecklist( + business_connection_id: string, + chat_id: number | string, + message_id: number, + checklist: InputChecklist, + other?: Other< + R, + "editMessageChecklist", + "business_connection_id" | "chat_id" | "messaage_id" | "checklist" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageChecklist({ + business_connection_id, + chat_id, + message_id, + checklist, + ...other, + }, signal); + } + + /** + * Use this method to send an animated emoji that will display a random value. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param emoji Emoji on which the dice throw animation is based. Currently, must be one of β€œπŸŽ²β€, β€œπŸŽ―β€, β€œπŸ€β€, β€œβš½β€, β€œπŸŽ³β€, or β€œπŸŽ°β€. Dice can have values 1-6 for β€œπŸŽ²β€, β€œπŸŽ―β€ and β€œπŸŽ³β€, values 1-5 for β€œπŸ€β€ and β€œβš½β€, and values 1-64 for β€œπŸŽ°β€. Defaults to β€œπŸŽ²β€. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#senddice + */ + sendDice( + chat_id: number | string, + emoji: + | (string & Record) + | "🎲" + | "🎯" + | "πŸ€" + | "⚽" + | "🎳" + | "🎰", + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendDice({ chat_id, emoji, ...other }, signal); + } + + /** + * Use this method to change the chosen reactions on a message. Service messages of some types can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. Bots can't use paid reactions. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) + * @param message_id Identifier of the target message + * @param reaction A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either already present on the message or explicitly allowed by chat administrators. Paid reactions can't be used by bots. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmessagereaction + */ + setMessageReaction( + chat_id: number | string, + message_id: number, + reaction: ReactionType[], + other?: Other< + R, + "setMessageReaction", + "chat_id" | "message_id" | "reaction" + >, + signal?: AbortSignal, + ) { + return this.raw.setMessageReaction({ + chat_id, + message_id, + reaction, + ...other, + }, signal); + } + + /** + * Use this method to stream a partial message to a user while the message is being generated. Returns True on success. + * + * @param chat_id Unique identifier for the target private chat + * @param draft_id Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated. + * @param text Text of the message to be sent, 1-4096 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmessagedraft + */ + sendMessageDraft( + chat_id: number, + draft_id: number, + text: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendMessageDraft( + { chat_id, draft_id, text, ...other }, + signal, + ); + } + + /** + * Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success. + * + * @param chat_id Unique identifier for the target private chat + * @param draft_id Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated. + * @param rich_message The partial message to be streamed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendrichmessagedraft + */ + sendRichMessageDraft( + chat_id: number, + draft_id: number, + rich_message: InputRichMessageWithoutUpload, + other?: Other< + R, + "sendRichMessageDraft", + "chat_id" | "draft_id" | "rich_message" + >, + signal?: AbortSignal, + ) { + return this.raw.sendRichMessageDraft( + { chat_id, draft_id, rich_message, ...other }, + signal, + ); + } + + /** + * Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success. + * + * Example: The ImageBot needs some time to process a request and upload the image. Instead of sending a text message along the lines of β€œRetrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a β€œsending photo” status for the bot. + * + * We only recommend using this method when a response from the bot will take a noticeable amount of time to arrive. + * + * @param chat_id Unique identifier for the target chat or username of the target bot or supergroup in the format `@username` + * @param action Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for text messages, upload_photo for photos, record_video or upload_video for videos, record_voice or upload_voice for voice notes, upload_document for general files, choose_sticker for stickers, find_location for location data, record_video_note or upload_video_note for video notes. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchataction + */ + sendChatAction( + chat_id: number | string, + action: + | "typing" + | "upload_photo" + | "record_video" + | "upload_video" + | "record_voice" + | "upload_voice" + | "upload_document" + | "choose_sticker" + | "find_location" + | "record_video_note" + | "upload_video_note", + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendChatAction({ chat_id, action, ...other }, signal); + } + + /** + * Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos object. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserprofilephotos + */ + getUserProfilePhotos( + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getUserProfilePhotos({ user_id, ...other }, signal); + } + + /** + * Use this method to get a list of profile audios for a user. Returns a UserProfileAudios object. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserprofileaudios + */ + getUserProfileAudios( + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getUserProfileAudios({ user_id, ...other }, signal); + } + + /** + * Changes the emoji status for a given user that previously allowed the bot to manage their emoji status via the Mini App method requestEmojiStatusAccess. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setuseremojistatus + */ + setUserEmojiStatus( + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setUserEmojiStatus({ user_id, ...other }, signal); + } + + /** + * Use this method to get the list of boosts added to a chat by a user. Requires administrator rights in the chat. Returns a UserChatBoosts object. + * + * @param chat_id Unique identifier for the chat or username of the channel (in the format @channelusername) + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserchatboosts + */ + getUserChatBoosts( + chat_id: number | string, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.getUserChatBoosts({ chat_id, user_id }, signal); + } + + /** + * Returns the gifts owned and hosted by a user. Returns OwnedGifts on success. + * + * @param user_id Unique identifier of the user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getusergifts + */ + getUserGifts( + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getUserGifts({ user_id, ...other }, signal); + } + + /** + * Returns the gifts owned by a chat. Returns OwnedGifts on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatgifts + */ + getChatGifts( + chat_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getChatGifts({ chat_id, ...other }, signal); + } + + /** + * Use this method to get information about the connection of the bot with a business account. Returns a BusinessConnection object on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessconnection + */ + getBusinessConnection( + business_connection_id: string, + signal?: AbortSignal, + ) { + return this.raw.getBusinessConnection( + { business_connection_id }, + signal, + ); + } + + /** + * Use this method to get the token of a managed bot. Returns the token as String on success. + * + * @param user_id User identifier of the managed bot whose token will be returned + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmanagedbottoken + */ + getManagedBotToken(user_id: number, signal?: AbortSignal) { + return this.raw.getManagedBotToken({ user_id }, signal); + } + + /** + * Use this method to revoke the current token of a managed bot and generate a new one. Returns the new token as String on success. + * + * @param user_id User identifier of the managed bot whose token will be replaced + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#replacemanagedbottoken + */ + replaceManagedBotToken(user_id: number, signal?: AbortSignal) { + return this.raw.replaceManagedBotToken({ user_id }, signal); + } + + /** + * Use this method to get the access settings of a managed bot. Returns a BotAccessSettings object on success. + * + * @param user_id User identifier of the managed bot whose access settings will be returned + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmanagedbotaccesssettings + */ + getManagedBotAccessSettings(user_id: number, signal?: AbortSignal) { + return this.raw.getManagedBotAccessSettings({ user_id }, signal); + } + + /** + * Use this method to change the access settings of a managed bot. Returns True on success. + * + * @param user_id User identifier of the managed bot whose access settings will be changed + * @param is_access_restricted Pass True, if only selected users can access the bot + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmanagedbotaccesssettingsrestricted + */ + setManagedBotAccessSettings( + user_id: number, + is_access_restricted: boolean, + other?: Other< + R, + "setManagedBotAccessSettings", + "user_id" | "is_access_restricted" + >, + signal?: AbortSignal, + ) { + return this.raw.setManagedBotAccessSettings({ + user_id, + is_access_restricted, + ...other, + }, signal); + } + + /** + * Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a File object is returned. The file can then be downloaded via the link `https://api.telegram.org/file/bot/`, where `` is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling getFile again. + * + * Note: This function may not preserve the original file name and MIME type. You should save the file's MIME type and name (if available) when the File object is received. + * + * @param file_id File identifier to get info about + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getfile + */ + getFile(file_id: string, signal?: AbortSignal) { + return this.raw.getFile({ file_id }, signal); + } + + /** @deprecated Use `banChatMember` instead. */ + kickChatMember(...args: Parameters) { + return this.banChatMember(...args); + } + + /** + * Use this method to ban a user in a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the chat on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target group or username of the target supergroup or channel in the format `@username` + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#banchatmember + */ + banChatMember( + chat_id: number | string, + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.banChatMember({ chat_id, user_id, ...other }, signal); + } + + /** + * Use this method to unban a previously banned user in a supergroup or channel. The user will not return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. By default, this method guarantees that after the call the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat they will also be removed from the chat. If you don't want this, use the parameter only_if_banned. Returns True on success. + * + * @param chat_id Unique identifier for the target group or username of the target supergroup or channel in the format `@username` + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unbanchatmember + */ + unbanChatMember( + chat_id: number | string, + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.unbanChatMember({ chat_id, user_id, ...other }, signal); + } + + /** + * Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate administrator rights. Pass True for all permissions to lift restrictions from a user. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param user_id Unique identifier of the target user + * @param permissions An object for new user permissions + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#restrictchatmember + */ + restrictChatMember( + chat_id: number | string, + user_id: number, + permissions: ChatPermissions, + other?: Other< + R, + "restrictChatMember", + "chat_id" | "user_id" | "permissions" + >, + signal?: AbortSignal, + ) { + return this.raw.restrictChatMember( + { chat_id, user_id, permissions, ...other }, + signal, + ); + } + + /** + * Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Pass False for all boolean parameters to demote a user. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#promotechatmember + */ + promoteChatMember( + chat_id: number | string, + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.promoteChatMember( + { chat_id, user_id, ...other }, + signal, + ); + } + + /** + * Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param user_id Unique identifier of the target user + * @param custom_title New custom title for the administrator; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatadministratorcustomtitle + */ + setChatAdministratorCustomTitle( + chat_id: number | string, + user_id: number, + custom_title: string, + signal?: AbortSignal, + ) { + return this.raw.setChatAdministratorCustomTitle( + { chat_id, user_id, custom_title }, + signal, + ); + } + + /** + * Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the β€œcan_manage_tags” administrator right. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param user_id Unique identifier of the target user + * @param tag New tag for the member; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setChatMemberTag + */ + setChatMemberTag( + chat_id: number | string, + user_id: number, + tag: string, + signal?: AbortSignal, + ) { + return this.raw.setChatMemberTag({ chat_id, user_id, tag }, signal); + } + + /** + * Use this method to ban a channel chat in a supergroup or a channel. Until the chat is unbanned, the owner of the banned chat won't be able to send messages on behalf of any of their channels. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param sender_chat_id Unique identifier of the target sender chat + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#banchatsenderchat + */ + banChatSenderChat( + chat_id: number | string, + sender_chat_id: number, + signal?: AbortSignal, + ) { + return this.raw.banChatSenderChat({ chat_id, sender_chat_id }, signal); + } + + /** + * Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param sender_chat_id Unique identifier of the target sender chat + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unbanchatsenderchat + */ + unbanChatSenderChat( + chat_id: number | string, + sender_chat_id: number, + signal?: AbortSignal, + ) { + return this.raw.unbanChatSenderChat( + { chat_id, sender_chat_id }, + signal, + ); + } + + /** + * Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the can_restrict_members administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param permissions New default chat permissions + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatpermissions + */ + setChatPermissions( + chat_id: number | string, + permissions: ChatPermissions, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setChatPermissions( + { chat_id, permissions, ...other }, + signal, + ); + } + + /** + * Use this method to generate a new primary invite link for a chat; any previously generated primary link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the new invite link as String on success. + * + * Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using exportChatInviteLink or by calling the getChat method. If your bot needs to generate a new primary invite link replacing its previous one, use exportChatInviteLink again. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#exportchatinvitelink + */ + exportChatInviteLink(chat_id: number | string, signal?: AbortSignal) { + return this.raw.exportChatInviteLink({ chat_id }, signal); + } + + /** + * Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. The link can be revoked using the method revokeChatInviteLink. Returns the new invite link as ChatInviteLink object. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createchatinvitelink + */ + createChatInviteLink( + chat_id: number | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.createChatInviteLink({ chat_id, ...other }, signal); + } + + /** + * Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the edited invite link as a ChatInviteLink object. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param invite_link The invite link to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editchatinvitelink + */ + editChatInviteLink( + chat_id: number | string, + invite_link: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.editChatInviteLink( + { chat_id, invite_link, ...other }, + signal, + ); + } + + /** + * Use this method to create a subscription invite link for a channel chat. The bot must have the can_invite_users administrator rights. The link can be edited using the method editChatSubscriptionInviteLink or revoked using the method revokeChatInviteLink. Returns the new invite link as a ChatInviteLink object. + * + * @param chat_id Unique identifier for the target channel chat or username of the target channel in the format `@username` + * @param subscription_period The number of seconds the subscription will be active for before the next payment. Currently, it must always be 2592000 (30 days). + * @param subscription_price The amount of Telegram Stars a user must pay initially and after each subsequent subscription period to be a member of the chat; 1-2500 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createchatsubscriptioninvitelink + */ + createChatSubscriptionInviteLink( + chat_id: number | string, + subscription_period: number, + subscription_price: number, + other?: Other< + R, + "createChatSubscriptionInviteLink", + "chat_id" | "subscription_period" | "subscription_price" + >, + signal?: AbortSignal, + ) { + return this.raw.createChatSubscriptionInviteLink( + { chat_id, subscription_period, subscription_price, ...other }, + signal, + ); + } + + /** + * Use this method to edit a subscription invite link created by the bot. The bot must have the can_invite_users administrator rights. Returns the edited invite link as a ChatInviteLink object. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param invite_link The invite link to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editchatsubscriptioninvitelink + */ + editChatSubscriptionInviteLink( + chat_id: number | string, + invite_link: string, + other?: Other< + R, + "editChatSubscriptionInviteLink", + "chat_id" | "invite_link" + >, + signal?: AbortSignal, + ) { + return this.raw.editChatSubscriptionInviteLink( + { chat_id, invite_link, ...other }, + signal, + ); + } + + /** + * Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the revoked invite link as ChatInviteLink object. + * + * @param chat_id Unique identifier of the target chat or username of the target channel in the format `@username` + * @param invite_link The invite link to revoke + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#revokechatinvitelink + */ + revokeChatInviteLink( + chat_id: number | string, + invite_link: string, + signal?: AbortSignal, + ) { + return this.raw.revokeChatInviteLink({ chat_id, invite_link }, signal); + } + + /** + * Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#approvechatjoinrequest + */ + approveChatJoinRequest( + chat_id: number | string, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.approveChatJoinRequest({ chat_id, user_id }, signal); + } + + /** + * Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#declinechatjoinrequest + */ + declineChatJoinRequest( + chat_id: number | string, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.declineChatJoinRequest({ chat_id, user_id }, signal); + } + + /** + * Use this method to process a received chat join request query. Returns True on success. + * + * @param chat_join_request_query_id Unique identifier of the join request query + * @param result Result of the query. Must be either β€œapprove” to allow the user to join the chat, β€œdecline” to disallow the user to join the chat, or β€œqueue” to leave the decision to other administrators. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerchatjoinrequestquery + */ + answerChatJoinRequestQuery( + chat_join_request_query_id: string, + result: "approve" | "decline" | "queue", + signal?: AbortSignal, + ) { + return this.raw.answerChatJoinRequestQuery( + { chat_join_request_query_id, result }, + signal, + ); + } + + /** + * Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success. + * + * @param chat_join_request_query_id Unique identifier of the join request query + * @param web_app_url The URL of the Mini App to be opened + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchatjoinrequestwebapp + */ + sendChatJoinRequestWebApp( + chat_join_request_query_id: string, + web_app_url: string, + signal?: AbortSignal, + ) { + return this.raw.sendChatJoinRequestWebApp( + { chat_join_request_query_id, web_app_url }, + signal, + ); + } + + /** + * Use this method to approve a suggested post in a direct messages chat. The bot must have the 'can_post_messages' administrator right in the corresponding channel chat. Returns True on success. + * + * @param chat_id Unique identifier for the target direct messages chat + * @param message_id Identifier of a suggested post message to approve + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#approvesuggestedpost + */ + approveSuggestedPost( + chat_id: number, + message_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.approveSuggestedPost( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to decline a suggested post in a direct messages chat. The bot must have the 'can_manage_direct_messages' administrator right in the corresponding channel chat. Returns True on success. + * + * @param chat_id Unique identifier for the target direct messages chat + * @param message_id Identifier of a suggested post message to decline + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#declinesuggestedpost + */ + declineSuggestedPost( + chat_id: number, + message_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.declineSuggestedPost( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param photo New chat photo, uploaded using multipart/form-data + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatphoto + */ + setChatPhoto( + chat_id: number | string, + photo: InputFile, + signal?: AbortSignal, + ) { + return this.raw.setChatPhoto({ chat_id, photo }, signal); + } + + /** + * Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletechatphoto + */ + deleteChatPhoto(chat_id: number | string, signal?: AbortSignal) { + return this.raw.deleteChatPhoto({ chat_id }, signal); + } + + /** + * Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param title New chat title, 1-255 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchattitle + */ + setChatTitle( + chat_id: number | string, + title: string, + signal?: AbortSignal, + ) { + return this.raw.setChatTitle({ chat_id, title }, signal); + } + + /** + * Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param description New chat description, 0-255 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatdescription + */ + setChatDescription( + chat_id: number | string, + description?: string, + signal?: AbortSignal, + ) { + return this.raw.setChatDescription({ chat_id, description }, signal); + } + + /** + * Use this method to add a message to the list of pinned messages in a chat. In private chats and channel direct messages chats, all non-service messages can be pinned. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to pin messages in groups and channels respectively. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param message_id Identifier of a message to pin + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#pinchatmessage + */ + pinChatMessage( + chat_id: number | string, + message_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.pinChatMessage( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to remove a message from the list of pinned messages in a chat. In private chats and channel direct messages chats, all messages can be unpinned. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to unpin messages in groups and channels respectively. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param message_id Identifier of a message to unpin. If not specified, the most recent pinned message (by sending date) will be unpinned. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinchatmessage + */ + unpinChatMessage( + chat_id: number | string, + message_id?: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.unpinChatMessage( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to clear the list of pinned messages in a chat. In private chats and channel direct messages chats, no additional rights are required to unpin all pinned messages. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to unpin all pinned messages in groups and channels respectively. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallchatmessages + */ + unpinAllChatMessages(chat_id: number | string, signal?: AbortSignal) { + return this.raw.unpinAllChatMessages({ chat_id }, signal); + } + + /** + * Use this method for your bot to leave a group, supergroup or channel. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup or channel in the format `@username`. Channel direct messages chats aren't supported; leave the corresponding channel instead. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#leavechat + */ + leaveChat(chat_id: number | string, signal?: AbortSignal) { + return this.raw.leaveChat({ chat_id }, signal); + } + + /** + * Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup or channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchat + */ + getChat(chat_id: number | string, signal?: AbortSignal) { + return this.raw.getChat({ chat_id }, signal); + } + + /** + * Use this method to get a list of administrators in a chat. Returns an Array of ChatMember objects. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup or channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatadministrators + */ + getChatAdministrators( + chat_id: number | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getChatAdministrators({ chat_id, ...other }, signal); + } + + /** @deprecated Use `getChatMemberCount` instead. */ + getChatMembersCount(...args: Parameters) { + return this.getChatMemberCount(...args); + } + + /** + * Use this method to get the number of members in a chat. Returns Integer on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup or channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmembercount + */ + getChatMemberCount(chat_id: number | string, signal?: AbortSignal) { + return this.raw.getChatMemberCount({ chat_id }, signal); + } + + /** + * Use this method to get information about a member of a chat. The method is guaranteed to work only if the bot is an administrator in the chat. Returns a ChatMember object on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup or channel (in the format @channelusername) + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmember + */ + getChatMember( + chat_id: number | string, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.getChatMember({ chat_id, user_id }, signal); + } + + /** + * Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an Array of Message objects is returned. + * + * @param user_id Unique identifier for the target user + * @param limit The maximum number of messages to return; 1-20 + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserpersonalchatmessages + */ + getUserPersonalChatMessages( + user_id: number, + limit: number, + signal?: AbortSignal, + ) { + return this.raw.getUserPersonalChatMessages({ user_id, limit }, signal); + } + + /** + * Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Use the field can_set_sticker_set ly returned in getChat requests to check if the bot can use this method. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param sticker_set_name Name of the sticker set to be set as the group sticker set + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatstickerset + */ + setChatStickerSet( + chat_id: number | string, + sticker_set_name: string, + signal?: AbortSignal, + ) { + return this.raw.setChatStickerSet( + { chat_id, sticker_set_name }, + signal, + ); + } + + /** + * Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Use the field can_set_sticker_set ly returned in getChat requests to check if the bot can use this method. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletechatstickerset + */ + deleteChatStickerSet(chat_id: number | string, signal?: AbortSignal) { + return this.raw.deleteChatStickerSet({ chat_id }, signal); + } + + /** + * Use this method to get custom emoji stickers, which can be used as a forum topic icon by any user. Requires no parameters. Returns an Array of Sticker objects. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getforumtopiciconstickers + */ + getForumTopicIconStickers(signal?: AbortSignal) { + return this.raw.getForumTopicIconStickers(signal); + } + + /** + * Use this method to create a topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator right. Returns information about the created topic as a ForumTopic object. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param name Topic name, 1-128 characters + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createforumtopic + */ + createForumTopic( + chat_id: number | string, + name: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.createForumTopic({ chat_id, name, ...other }, signal); + } + + /** + * Use this method to edit name and icon of a topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_thread_id Unique identifier for the target message thread of the forum topic + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editforumtopic + */ + editForumTopic( + chat_id: number | string, + message_thread_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.editForumTopic( + { chat_id, message_thread_id, ...other }, + signal, + ); + } + + /** + * Use this method to close an open topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_thread_id Unique identifier for the target message thread of the forum topic + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#closeforumtopic + */ + closeForumTopic( + chat_id: number | string, + message_thread_id: number, + signal?: AbortSignal, + ) { + return this.raw.closeForumTopic({ chat_id, message_thread_id }, signal); + } + + /** + * Use this method to reopen a closed topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_thread_id Unique identifier for the target message thread of the forum topic + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#reopenforumtopic + */ + reopenForumTopic( + chat_id: number | string, + message_thread_id: number, + signal?: AbortSignal, + ) { + return this.raw.reopenForumTopic( + { chat_id, message_thread_id }, + signal, + ); + } + + /** + * Use this method to delete a forum topic along with all its messages in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_delete_messages administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_thread_id Unique identifier for the target message thread of the forum topic + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteforumtopic + */ + deleteForumTopic( + chat_id: number | string, + message_thread_id: number, + signal?: AbortSignal, + ) { + return this.raw.deleteForumTopic( + { chat_id, message_thread_id }, + signal, + ); + } + + /** + * Use this method to clear the list of pinned messages in a forum topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_thread_id Unique identifier for the target message thread of the forum topic + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallforumtopicmessages + */ + unpinAllForumTopicMessages( + chat_id: number | string, + message_thread_id: number, + signal?: AbortSignal, + ) { + return this.raw.unpinAllForumTopicMessages( + { chat_id, message_thread_id }, + signal, + ); + } + + /** + * Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param name New topic name, 1-128 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editgeneralforumtopic + */ + editGeneralForumTopic( + chat_id: number | string, + name: string, + signal?: AbortSignal, + ) { + return this.raw.editGeneralForumTopic({ chat_id, name }, signal); + } + + /** + * Use this method to close an open 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#closegeneralforumtopic + */ + closeGeneralForumTopic(chat_id: number | string, signal?: AbortSignal) { + return this.raw.closeGeneralForumTopic({ chat_id }, signal); + } + + /** + * Use this method to reopen a closed 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically unhidden if it was hidden. Returns True on success. * + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#reopengeneralforumtopic + */ + reopenGeneralForumTopic(chat_id: number | string, signal?: AbortSignal) { + return this.raw.reopenGeneralForumTopic({ chat_id }, signal); + } + + /** + * Use this method to hide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically closed if it was open. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#hidegeneralforumtopic + */ + hideGeneralForumTopic(chat_id: number | string, signal?: AbortSignal) { + return this.raw.hideGeneralForumTopic({ chat_id }, signal); + } + + /** + * Use this method to unhide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unhidegeneralforumtopic + */ + unhideGeneralForumTopic(chat_id: number | string, signal?: AbortSignal) { + return this.raw.unhideGeneralForumTopic({ chat_id }, signal); + } + + /** + * Use this method to clear the list of pinned messages in a General forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages + */ + unpinAllGeneralForumTopicMessages( + chat_id: number | string, + signal?: AbortSignal, + ) { + return this.raw.unpinAllGeneralForumTopicMessages({ chat_id }, signal); + } + + /** + * Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. + * + * Alternatively, the user can be redirected to the specified Game URL. For this option to work, you must first create a game for your bot via @BotFather and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter. + * + * @param callback_query_id Unique identifier for the query to be answered + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answercallbackquery + */ + answerCallbackQuery( + callback_query_id: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.answerCallbackQuery( + { callback_query_id, ...other }, + signal, + ); + } + + /** + * Use this method to reply to a received guest message. On success, a SentGuestMessage object is returned. + * + * @param guest_query_id Unique identifier for the query to be answered + * @param result An object describing the message to be sent + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerguestquery + */ + answerGuestQuery( + guest_query_id: string, + result: InlineQueryResult, + signal?: AbortSignal, + ) { + return this.raw.answerGuestQuery({ guest_query_id, result }, signal); + } + + /** + * Use this method to change the bot's name. Returns True on success. + * + * @param name New bot name; 0-64 characters. Pass an empty string to remove the dedicated name for the given language. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmyname + */ + setMyName( + name: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setMyName({ name, ...other }, signal); + } + + /** + * Use this method to get the current bot name for the given user language. Returns BotName on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmyname + */ + getMyName(other?: Other, signal?: AbortSignal) { + return this.raw.getMyName(other ?? {}, signal); + } + + /** + * Use this method to change the list of the bot's commands. See https://core.telegram.org/bots/features#commands for more details about bot commands. Returns True on success. + * + * @param commands A list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmycommands + */ + setMyCommands( + commands: readonly BotCommand[], + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setMyCommands({ commands, ...other }, signal); + } + + /** + * Use this method to delete the list of the bot's commands for the given scope and user language. After deletion, higher level commands will be shown to affected users. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemycommands + */ + deleteMyCommands( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.deleteMyCommands({ ...other }, signal); + } + + /** + * Use this method to get the current list of the bot's commands for the given scope and user language. Returns an Array of BotCommand objects. If commands aren't set, an empty list is returned. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmycommands + */ + getMyCommands(other?: Other, signal?: AbortSignal) { + return this.raw.getMyCommands({ ...other }, signal); + } + + /** + * Use this method to change the bot's description, which is shown in the chat with the bot if the chat is empty. Returns True on success. + * + * @param description New bot description; 0-512 characters. Pass an empty string to remove the dedicated description for the given language. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmydescription + */ + setMyDescription( + description: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setMyDescription({ description, ...other }, signal); + } + + /** + * Use this method to get the current bot description for the given user language. Returns BotDescription on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmydescription + */ + getMyDescription( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getMyDescription({ ...other }, signal); + } + + /** + * Use this method to change the bot's short description, which is shown on the bot's profile page and is sent together with the link when users share the bot. Returns True on success. + * + * @param short_description New short description for the bot; 0-120 characters. Pass an empty string to remove the dedicated short description for the given language. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmyshortdescription + */ + setMyShortDescription( + short_description: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setMyShortDescription( + { short_description, ...other }, + signal, + ); + } + + /** + * Use this method to get the current bot short description for the given user language. Returns BotShortDescription on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmyshortdescription + */ + getMyShortDescription( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getMyShortDescription({ ...other }, signal); + } + + /** + * Changes the profile photo of the bot. Returns True on success. + * + * @param photo The new profile photo to set + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmyprofilephoto + */ + setMyProfilePhoto( + photo: InputProfilePhoto, + signal?: AbortSignal, + ) { + return this.raw.setMyProfilePhoto({ photo }, signal); + } + + /** + * Removes the profile photo of the bot. Requires no parameters. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removemyprofilephoto + */ + removeMyProfilePhoto(signal?: AbortSignal) { + return this.raw.removeMyProfilePhoto(signal); + } + + /** + * Use this method to change the bot's menu button in a private chat, or the default menu button. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatmenubutton + */ + setChatMenuButton( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setChatMenuButton({ ...other }, signal); + } + + /** + * Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns MenuButton on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmenubutton + */ + getChatMenuButton( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getChatMenuButton({ ...other }, signal); + } + + /** + * Use this method to the change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmydefaultadministratorrights + */ + setMyDefaultAdministratorRights( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.setMyDefaultAdministratorRights({ ...other }, signal); + } + + /** + * Use this method to get the current default administrator rights of the bot. Returns ChatAdministratorRights on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmydefaultadministratorrights + */ + getMyDefaultAdministratorRights( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getMyDefaultAdministratorRights({ ...other }, signal); + } + + /** + * A method to get the current Telegram Stars balance of the bot. Requires no parameters. On success, returns a StarAmount object. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmystarbalance + */ + getMyStarBalance(signal?: AbortSignal) { + return this.raw.getMyStarBalance(signal); + } + + /** + * Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to edit + * @param text_or_rich_message New text or rich content of the message, a string maps to the `text` parameter and an object maps to the `rich_message` parameter + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagetext + */ + editMessageText( + chat_id: number | string, + message_id: number, + text_or_rich_message: string | InputRichMessage, + other?: Other< + R, + "editMessageText", + | "chat_id" + | "message_id" + | "inline_message_id" + | "text" + | "rich_message" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageText( + typeof text_or_rich_message === "string" + ? { chat_id, message_id, text: text_or_rich_message, ...other } + : { + chat_id, + message_id, + rich_message: text_or_rich_message, + ...other, + }, + signal, + ); + } + + /** + * Use this method to edit text, rich and game inline messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param inline_message_id Identifier of the inline message + * @param text_or_rich_message New text or rich content of the message, a string maps to the `text` parameter and an object maps to the `rich_message` parameter + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagetext + */ + editMessageTextInline( + inline_message_id: string, + text_or_rich_message: string | InputRichMessage, + other?: Other< + R, + "editMessageText", + | "chat_id" + | "message_id" + | "inline_message_id" + | "text" + | "rich_message" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageText( + typeof text_or_rich_message === "string" + ? { inline_message_id, text: text_or_rich_message, ...other } + : { + inline_message_id, + rich_message: text_or_rich_message, + ...other, + }, + signal, + ); + } + + /** + * Use this method to edit captions of messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagecaption + */ + editMessageCaption( + chat_id: number | string, + message_id: number, + other?: Other< + R, + "editMessageCaption", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageCaption( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to edit captions of inline messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param inline_message_id Identifier of the inline message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagecaption + */ + editMessageCaptionInline( + inline_message_id: string, + other?: Other< + R, + "editMessageCaption", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageCaption( + { inline_message_id, ...other }, + signal, + ); + } + + /** + * Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to edit + * @param media An object for a new media content of the message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagemedia + */ + editMessageMedia( + chat_id: number | string, + message_id: number, + media: InputMedia, + other?: Other< + R, + "editMessageMedia", + "chat_id" | "message_id" | "inline_message_id" | "media" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageMedia( + { chat_id, message_id, media, ...other }, + signal, + ); + } + + /** + * Use this method to edit animation, audio, document, live photo, photo, or video inline messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param inline_message_id Identifier of the inline message + * @param media An object for a new media content of the message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagemedia + */ + editMessageMediaInline( + inline_message_id: string, + media: InputMedia, + other?: Other< + R, + "editMessageMedia", + "chat_id" | "message_id" | "inline_message_id" | "media" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageMedia( + { inline_message_id, media, ...other }, + signal, + ); + } + + /** + * Use this method to edit only the reply markup of messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagereplymarkup + */ + editMessageReplyMarkup( + chat_id: number | string, + message_id: number, + other?: Other< + R, + "editMessageReplyMarkup", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageReplyMarkup( + { chat_id, message_id, ...other }, + signal, + ); + } + + /** + * Use this method to edit only the reply markup of inline messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param inline_message_id Identifier of the inline message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagereplymarkup + */ + editMessageReplyMarkupInline( + inline_message_id: string, + other?: Other< + R, + "editMessageReplyMarkup", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.editMessageReplyMarkup( + { inline_message_id, ...other }, + signal, + ); + } + + /** + * Use this method to stop a poll which was sent by the bot. On success, the stopped Poll is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the original message with the poll + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#stoppoll + */ + stopPoll( + chat_id: number | string, + message_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.stopPoll({ chat_id, message_id, ...other }, signal); + } + + /** + * Use this method to edit an ephemeral text message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to edit + * @param text New text of the message, 1-4096 characters after entity parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagetext + */ + editEphemeralMessageText( + chat_id: number | string, + receiver_user_id: number, + ephemeral_message_id: number, + text: string, + other?: Other< + R, + "editEphemeralMessageText", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "text" + >, + signal?: AbortSignal, + ) { + return this.raw.editEphemeralMessageText( + { chat_id, receiver_user_id, ephemeral_message_id, text, ...other }, + signal, + ); + } + + /** + * Use this method to edit the media of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to edit + * @param media An object for the new media content of the message. A new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagemedia + */ + editEphemeralMessageMedia( + chat_id: number | string, + receiver_user_id: number, + ephemeral_message_id: number, + media: InputMediaWithoutUpload, + other?: Other< + R, + "editEphemeralMessageMedia", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "media" + >, + signal?: AbortSignal, + ) { + return this.raw.editEphemeralMessageMedia({ + chat_id, + receiver_user_id, + ephemeral_message_id, + media, + ...other, + }, signal); + } + + /** + * Use this method to edit the caption of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to edit + * @param caption New caption of the message, 0-1024 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagecaption + */ + editEphemeralMessageCaption( + chat_id: number | string, + receiver_user_id: number, + ephemeral_message_id: number, + caption: string, + other?: Other< + R, + "editEphemeralMessageCaption", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "caption" + >, + signal?: AbortSignal, + ) { + return this.raw.editEphemeralMessageCaption({ + chat_id, + receiver_user_id, + ephemeral_message_id, + caption, + ...other, + }, signal); + } + + /** + * Use this method to edit only the reply markup of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagecaption + */ + editEphemeralMessageReplyMarkup( + chat_id: number | string, + receiver_user_id: number, + ephemeral_message_id: number, + other?: Other< + R, + "editEphemeralMessageReplyMarkup", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" + >, + signal?: AbortSignal, + ) { + return this.raw.editEphemeralMessageReplyMarkup( + { chat_id, receiver_user_id, ephemeral_message_id, ...other }, + signal, + ); + } + + /** + * Use this method to delete a message, including service messages, with the following limitations: + * - A message can only be deleted if it was sent less than 48 hours ago. + * - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. + * - Bots can delete outgoing messages in private chats, groups, and supergroups. + * - Bots can delete incoming messages in private chats. + * - Bots granted can_post_messages permissions can delete outgoing messages in channels. + * - If the bot is an administrator of a group, it can delete any message there. + * - If the bot has can_delete_messages administrator right in a supergroup or a channel, it can delete any message there. + * - If the bot has can_manage_direct_messages administrator right in a channel, it can delete any message in the corresponding direct messages chat. + * Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_id Identifier of the message to delete + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessage + */ + deleteMessage( + chat_id: number | string, + message_id: number, + signal?: AbortSignal, + ) { + return this.raw.deleteMessage({ chat_id, message_id }, signal); + } + + /** + * Use this method to delete multiple messages simultaneously. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_ids A list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessages + */ + deleteMessages( + chat_id: number | string, + message_ids: number[], + signal?: AbortSignal, + ) { + return this.raw.deleteMessages({ chat_id, message_ids }, signal); + } + + /** + * Use this method to delete an ephemeral message. Note that it is not guaranteed that the user will receive the message deletion event, especially if they are offline. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to delete + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteephemeralmessage + */ + deleteEphemeralMessage( + chat_id: number | string, + receiver_user_id: number, + ephemeral_message_id: number, + signal?: AbortSignal, + ) { + return this.raw.deleteEphemeralMessage( + { chat_id, receiver_user_id, ephemeral_message_id }, + signal, + ); + } + + /** + * Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_id Identifier of the target message + * @param user_id Identifier of the user whose reaction will be removed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessagereaction + */ + deleteMessageReactionUser( + chat_id: number | string, + message_id: number, + user_id: number, + other?: Other< + R, + "deleteMessageReaction", + "chat_id" | "message_id" | "user_id" + >, + signal?: AbortSignal, + ) { + return this.raw.deleteMessageReaction({ + chat_id, + message_id, + user_id, + ...other, + }, signal); + } + + /** + * Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param message_id Identifier of the target message + * @param actor_chat_id Identifier of the chat whose reaction will be removed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessagereaction + */ + deleteMessageReactionChat( + chat_id: number | string, + message_id: number, + actor_chat_id: number, + other?: Other< + R, + "deleteMessageReaction", + "chat_id" | "message_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + return this.raw.deleteMessageReaction({ + chat_id, + message_id, + actor_chat_id, + ...other, + }, signal); + } + + /** + * Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param user_id Identifier of the user whose reactions will be removed, if the reactions were added by a user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteallmessagereactions + */ + deleteAllMessageReactionsUser( + chat_id: number | string, + user_id: number, + other?: Other< + R, + "deleteAllMessageReactions", + "chat_id" | "message_id" | "user_id" + >, + signal?: AbortSignal, + ) { + return this.raw.deleteAllMessageReactions({ + chat_id, + user_id, + ...other, + }, signal); + } + + /** + * Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param actor_chat_id Identifier of the chat whose reactions will be removed, if the reactions were added by a chat + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteallmessagereactions + */ + deleteAllMessageReactionsChat( + chat_id: number | string, + actor_chat_id: number, + other?: Other< + R, + "deleteAllMessageReactions", + "chat_id" | "message_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + return this.raw.deleteAllMessageReactions({ + chat_id, + actor_chat_id, + ...other, + }, signal); + } + + /** + * Delete messages on behalf of a business account. Requires the can_delete_outgoing_messages business bot right to delete messages sent by the bot itself, or the can_delete_all_messages business bot right to delete any message. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection on behalf of which to delete the messages + * @param message_ids A list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletebusinessmessages + */ + deleteBusinessMessages( + business_connection_id: string, + message_ids: number[], + signal?: AbortSignal, + ) { + return this.raw.deleteBusinessMessages( + { business_connection_id, message_ids }, + signal, + ); + } + + /** + * Changes the first and last name of a managed business account. Requires the can_change_name business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param first_name The new value of the first name for the business account; 1-64 characters + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountname + */ + setBusinessAccountName( + business_connection_id: string, + first_name: string, + other: Other< + R, + "setBusinessAccountName", + "business_connection_id" | "first_name" + >, + signal?: AbortSignal, + ) { + return this.raw.setBusinessAccountName( + { business_connection_id, first_name, ...other }, + signal, + ); + } + + /** + * Changes the username of a managed business account. Requires the can_change_username business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection * + * @param username The new value of the username for the business account; 0-32 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountusername + */ + setBusinessAccountUsername( + business_connection_id: string, + username: string, + signal?: AbortSignal, + ) { + return this.raw.setBusinessAccountUsername( + { business_connection_id, username }, + signal, + ); + } + + /** + * Changes the bio of a managed business account. Requires the can_change_bio business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param bio The new value of the bio for the business account; 0-140 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountbio + */ + setBusinessAccountBio( + business_connection_id: string, + bio: string, + signal?: AbortSignal, + ) { + return this.raw.setBusinessAccountBio( + { business_connection_id, bio }, + signal, + ); + } + + /** + * Changes the profile photo of a managed business account. Requires the can_edit_profile_photo business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param photo The new profile photo to set + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountprofilephoto + */ + setBusinessAccountProfilePhoto( + business_connection_id: string, + photo: InputProfilePhoto, + other: Other< + R, + "setBusinessAccountProfilePhoto", + "business_connection_id" | "photo" + >, + signal?: AbortSignal, + ) { + return this.raw.setBusinessAccountProfilePhoto( + { business_connection_id, photo, ...other }, + signal, + ); + } + + /** + * Removes the current profile photo of a managed business account. Requires the can_edit_profile_photo business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removebusinessaccountprofilephoto + */ + removeBusinessAccountProfilePhoto( + business_connection_id: string, + other: Other< + R, + "removeBusinessAccountProfilePhoto", + "business_connection_id" + >, + signal?: AbortSignal, + ) { + return this.raw.removeBusinessAccountProfilePhoto( + { business_connection_id, ...other }, + signal, + ); + } + + /** + * Changes the privacy settings pertaining to incoming gifts in a managed business account. Requires the can_change_gift_settings business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param show_gift_button Pass True, if a button for sending a gift to the user or by the business account must always be shown in the input field + * @param accepted_gift_types Types of gifts accepted by the business account + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountgiftsettings + */ + setBusinessAccountGiftSettings( + business_connection_id: string, + show_gift_button: boolean, + accepted_gift_types: AcceptedGiftTypes, + signal?: AbortSignal, + ) { + return this.raw.setBusinessAccountGiftSettings( + { business_connection_id, show_gift_button, accepted_gift_types }, + signal, + ); + } + + /** + * Returns the amount of Telegram Stars owned by a managed business account. Requires the can_view_gifts_and_stars business bot right. Returns StarAmount on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessaccountstarbalance + */ + getBusinessAccountStarBalance( + business_connection_id: string, + signal?: AbortSignal, + ) { + return this.raw.getBusinessAccountStarBalance( + { business_connection_id }, + signal, + ); + } + + /** + * Transfers Telegram Stars from the business account balance to the bot's balance. Requires the can_transfer_stars business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param star_count Number of Telegram Stars to transfer; 1-10000 + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#transferbusinessaccountstars + */ + transferBusinessAccountStars( + business_connection_id: string, + star_count: number, + signal?: AbortSignal, + ) { + return this.raw.transferBusinessAccountStars( + { business_connection_id, star_count }, + signal, + ); + } + + /** + * Returns the gifts received and owned by a managed business account. Requires the can_view_gifts_and_stars business bot right. Returns OwnedGifts on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessaccountgifts + */ + getBusinessAccountGifts( + business_connection_id: string, + other: Other, + signal?: AbortSignal, + ) { + return this.raw.getBusinessAccountGifts( + { business_connection_id, ...other }, + signal, + ); + } + + /** + * Converts a given regular gift to Telegram Stars. Requires the can_convert_gifts_to_stars business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param owned_gift_id Unique identifier of the regular gift that should be converted to Telegram Stars + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#convertgifttostars + */ + convertGiftToStars( + business_connection_id: string, + owned_gift_id: string, + signal?: AbortSignal, + ) { + return this.raw.convertGiftToStars( + { business_connection_id, owned_gift_id }, + signal, + ); + } + + /** + * Upgrades a given regular gift to a unique gift. Requires the can_transfer_and_upgrade_gifts business bot right. Additionally requires the can_transfer_stars business bot right if the upgrade is paid. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param owned_gift_id Unique identifier of the regular gift that should be upgraded to a unique one + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#upgradegift + */ + upgradeGift( + business_connection_id: string, + owned_gift_id: string, + other: Other< + R, + "getBusinessAccountGifts", + "business_connection_id" | "owned_gift_id" + >, + signal?: AbortSignal, + ) { + return this.raw.upgradeGift( + { business_connection_id, owned_gift_id, ...other }, + signal, + ); + } + + /** + * Transfers an owned unique gift to another user. Requires the can_transfer_and_upgrade_gifts business bot right. Requires can_transfer_stars business bot right if the transfer is paid. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param owned_gift_id Unique identifier of the regular gift that should be transferred + * @param new_owner_chat_id Unique identifier of the chat which will own the gift. The chat must be active in the last 24 hours. + * @param star_count The amount of Telegram Stars that will be paid for the transfer from the business account balance. If positive, then the can_transfer_stars business bot right is required. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#transfergift + */ + transferGift( + business_connection_id: string, + owned_gift_id: string, + new_owner_chat_id: number, + star_count: number, + signal?: AbortSignal, + ) { + return this.raw.transferGift({ + business_connection_id, + owned_gift_id, + new_owner_chat_id, + star_count, + }, signal); + } + + /** + * Posts a story on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns Story on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param content Content of the story + * @param active_period Period after which the story is moved to the archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or 2 * 86400 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#poststory + */ + postStory( + business_connection_id: string, + content: InputStoryContent, + active_period: number, + other: Other< + R, + "postStory", + "business_connection_id" | "content" | "active_period" + >, + signal?: AbortSignal, + ) { + return this.raw.postStory( + { business_connection_id, content, active_period, ...other }, + signal, + ); + } + + /** + * Reposts a story on behalf of a business account from another business account. Both business accounts must be managed by the same bot, and the story on the source account must have been posted (or reposted) by the bot. Requires the can_manage_stories business bot right for both business accounts. Returns Story on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param from_chat_id Unique identifier of the chat which posted the story that should be reposted + * @param from_story_id Unique identifier of the story that should be reposted + * @param active_period Period after which the story is moved to the archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or 2 * 86400 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#repoststory + */ + repostStory( + business_connection_id: string, + from_chat_id: number, + from_story_id: number, + active_period: number, + other: Other< + R, + "repostStory", + | "business_connection_id" + | "from_chat_id" + | "from_story_id" + | "active_period" + >, + signal?: AbortSignal, + ) { + return this.raw.repostStory({ + business_connection_id, + from_chat_id, + from_story_id, + active_period, + ...other, + }, signal); + } + + /** + * Edits a story previously posted by the bot on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns Story on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param story_id Unique identifier of the story to edit + * @param content Content of the story + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editstory + */ + editStory( + business_connection_id: string, + story_id: number, + content: InputStoryContent, + other: Other< + R, + "editStory", + "business_connection_id" | "story_id" | "content" + >, + signal?: AbortSignal, + ) { + return this.raw.editStory( + { business_connection_id, story_id, content, ...other }, + signal, + ); + } + + /** + * Deletes a story previously posted by the bot on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection + * @param story_id Unique identifier of the story to delete + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletestory + */ + deleteStory( + business_connection_id: string, + story_id: number, + signal?: AbortSignal, + ) { + return this.raw.deleteStory( + { business_connection_id, story_id }, + signal, + ); + } + + /** + * Use this method to send static .WEBP, animated .TGS, or video .WEBM stickers. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param sticker Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP sticker from the Internet, or upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data. Video and animated stickers can't be sent via an HTTP URL. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendsticker + */ + sendSticker( + chat_id: number | string, + sticker: InputFile | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendSticker({ chat_id, sticker, ...other }, signal); + } + + /** + * Use this method to get a sticker set. On success, a StickerSet object is returned. + * + * @param name Name of the sticker set + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getstickerset + */ + getStickerSet(name: string, signal?: AbortSignal) { + return this.raw.getStickerSet({ name }, signal); + } + + /** + * Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects. + * + * @param custom_emoji_ids A list of custom emoji identifiers + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getcustomemojistickers + */ + getCustomEmojiStickers(custom_emoji_ids: string[], signal?: AbortSignal) { + return this.raw.getCustomEmojiStickers({ custom_emoji_ids }, signal); + } + + /** + * Use this method to upload a file with a sticker for later use in the createNewStickerSet, addStickerToSet, or replaceStickerInSet methods (the file can be used multiple times). Returns the uploaded File on success. + * + * @param user_id User identifier of sticker file owner + * @param sticker_format Format of the sticker, must be one of β€œstatic”, β€œanimated”, β€œvideo” + * @param sticker A file with the sticker in .WEBP, .PNG, .TGS, or .WEBM format. See https://core.telegram.org/stickers for technical requirements. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#uploadstickerfile + */ + uploadStickerFile( + user_id: number, + sticker_format: "static" | "animated" | "video", + sticker: InputFile, + signal?: AbortSignal, + ) { + return this.raw.uploadStickerFile( + { user_id, sticker_format, sticker }, + signal, + ); + } + + /** + * Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. Returns True on success. + * + * @param user_id User identifier of created sticker set owner + * @param name Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals). Can contain only English letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in `_by_`. `` is case insensitive. 1-64 characters. + * @param title Sticker set title, 1-64 characters + * @param stickers A list of 1-50 initial stickers to be added to the sticker set + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createnewstickerset + */ + createNewStickerSet( + user_id: number, + name: string, + title: string, + stickers: InputSticker[], + other?: Other< + R, + "createNewStickerSet", + | "user_id" + | "name" + | "title" + | "sticker_format" + | "stickers" + >, + signal?: AbortSignal, + ) { + return this.raw.createNewStickerSet( + { user_id, name, title, stickers, ...other }, + signal, + ); + } + + /** + * Use this method to add a new sticker to a set created by the bot. The format of the added sticker must match the format of the other stickers in the set. Emoji sticker sets can have up to 200 stickers. Animated and video sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns True on success. + * + * @param user_id User identifier of sticker set owner + * @param name Sticker set name + * @param sticker An object with information about the added sticker. If exactly the same sticker had already been added to the set, then the set isn't changed. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#addstickertoset + */ + addStickerToSet( + user_id: number, + name: string, + sticker: InputSticker, + signal?: AbortSignal, + ) { + return this.raw.addStickerToSet( + { user_id, name, sticker }, + signal, + ); + } + + /** + * Use this method to move a sticker in a set created by the bot to a specific position. Returns True on success. + * + * @param sticker File identifier of the sticker + * @param position New sticker position in the set, zero-based + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickerpositioninset + */ + setStickerPositionInSet( + sticker: string, + position: number, + signal?: AbortSignal, + ) { + return this.raw.setStickerPositionInSet({ sticker, position }, signal); + } + + /** + * Use this method to delete a sticker from a set created by the bot. Returns True on success. + * + * @param sticker File identifier of the sticker + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletestickerfromset + */ + deleteStickerFromSet(sticker: string, signal?: AbortSignal) { + return this.raw.deleteStickerFromSet({ sticker }, signal); + } + + /** + * Use this method to replace an existing sticker in a sticker set with a new one. The method is equivalent to calling deleteStickerFromSet, then addStickerToSet, then setStickerPositionInSet. Returns True on success. + * + * @param user_id User identifier of the sticker set owner + * @param name Sticker set name + * @param old_sticker File identifier of the replaced sticker + * @param sticker An object with information about the added sticker. If exactly the same sticker had already been added to the set, then the set remains unchanged.:x + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#replacestickerinset + */ + replaceStickerInSet( + user_id: number, + name: string, + old_sticker: string, + sticker: InputSticker, + signal?: AbortSignal, + ) { + return this.raw.replaceStickerInSet( + { user_id, name, old_sticker, sticker }, + signal, + ); + } + + /** + * Use this method to change the list of emoji assigned to a regular or custom emoji sticker. The sticker must belong to a sticker set created by the bot. Returns True on success. + * + * @param sticker File identifier of the sticker + * @param emoji_list A list of 1-20 emoji associated with the sticker + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickeremojilist + */ + setStickerEmojiList( + sticker: string, + emoji_list: string[], + signal?: AbortSignal, + ) { + return this.raw.setStickerEmojiList({ sticker, emoji_list }, signal); + } + + /** + * Use this method to change search keywords assigned to a regular or custom emoji sticker. The sticker must belong to a sticker set created by the bot. Returns True on success. + * + * @param sticker File identifier of the sticker + * @param keywords A list of 0-20 search keywords for the sticker with total length of up to 64 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickerkeywords + */ + setStickerKeywords( + sticker: string, + keywords: string[], + signal?: AbortSignal, + ) { + return this.raw.setStickerKeywords({ sticker, keywords }, signal); + } + + /** + * Use this method to change the mask position of a mask sticker. The sticker must belong to a sticker set that was created by the bot. Returns True on success. + * + * @param sticker File identifier of the sticker + * @param mask_position An object with the position where the mask should be placed on faces. Omit the parameter to remove the mask position. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickermaskposition + */ + setStickerMaskPosition( + sticker: string, + mask_position?: MaskPosition, + signal?: AbortSignal, + ) { + return this.raw.setStickerMaskPosition( + { sticker, mask_position }, + signal, + ); + } + + /** + * Use this method to set the title of a created sticker set. Returns True on success. + * + * @param name Sticker set name + * @param title Sticker set title, 1-64 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickersettitle + */ + setStickerSetTitle(name: string, title: string, signal?: AbortSignal) { + return this.raw.setStickerSetTitle({ name, title }, signal); + } + + /** + * Use this method to delete a sticker set that was created by the bot. Returns True on success. + * + * @param name Sticker set name + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletestickerset + */ + deleteStickerSet(name: string, signal?: AbortSignal) { + return this.raw.deleteStickerSet({ name }, signal); + } + + /** + * Use this method to set the thumbnail of a regular or mask sticker set. The format of the thumbnail file must match the format of the stickers in the set. Returns True on success. + * + * @param name Sticker set name + * @param user_id User identifier of the sticker set owner + * @param thumbnail A .WEBP or .PNG image with the thumbnail, must be up to 128 kilobytes in size and have a width and height of exactly 100px, or a .TGS animation with a thumbnail up to 32 kilobytes in size (see https://core.telegram.org/stickers#animated-sticker-requirements for animated sticker technical requirements), or a WEBM video with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#video-sticker-requirements for video sticker technical requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files Β». Animated and video sticker set thumbnails can't be uploaded via HTTP URL. If omitted, then the thumbnail is dropped and the first sticker is used as the thumbnail. + * @param format Format of the thumbnail, must be one of β€œstatic” for a .WEBP or .PNG image, β€œanimated” for a .TGS animation, or β€œvideo” for a WEBM video + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setstickersetthumbnail + */ + setStickerSetThumbnail( + name: string, + user_id: number, + thumbnail: InputFile | string | undefined, + format: "static" | "animated" | "video", + signal?: AbortSignal, + ) { + return this.raw.setStickerSetThumbnail( + { name, user_id, thumbnail, format }, + signal, + ); + } + + /** + * Use this method to set the thumbnail of a custom emoji sticker set. Returns True on success. + * + * @param name Sticker set name + * @param custom_emoji_id Custom emoji identifier of a sticker from the sticker set; pass an empty string to drop the thumbnail and use the first sticker as the thumbnail. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail + */ + setCustomEmojiStickerSetThumbnail( + name: string, + custom_emoji_id: string, + signal?: AbortSignal, + ) { + return this.raw.setCustomEmojiStickerSetThumbnail({ + name, + custom_emoji_id, + }, signal); + } + + /** + * Returns the list of gifts that can be sent by the bot to users and channel chats. Requires no parameters. Returns a Gifts object. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getavailablegifts + */ + getAvailableGifts(signal?: AbortSignal) { + return this.raw.getAvailableGifts(signal); + } + + /** + * Sends a gift to the given user. The gift can't be converted to Telegram Stars by the receiver. Returns True on success. + * + * @param user_id Unique identifier for the chat or username of the channel (in the format `@username`) that will receive the gift. + * @param gift_id Identifier of the gift + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgift + */ + sendGift( + user_id: number, + gift_id: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendGift({ user_id, gift_id, ...other }, signal); + } + + /** + * Gifts a Telegram Premium subscription to the given user. Returns True on success. + * + * @param user_id Unique identifier of the target user who will receive a Telegram Premium subscription + * @param month_count Number of months the Telegram Premium subscription will be active for the user; must be one of 3, 6, or 12 + * @param star_count Number of Telegram Stars to pay for the Telegram Premium subscription; must be 1000 for 3 months, 1500 for 6 months, and 2500 for 12 months + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#giftpremiumsubscription + */ + giftPremiumSubscription( + user_id: number, + month_count: 3 | 6 | 12, + star_count: 1000 | 1500 | 2500, + other?: Other< + R, + "giftPremiumSubscription", + "user_id" | "month_count" | "star_count" + >, + signal?: AbortSignal, + ) { + return this.raw.giftPremiumSubscription( + { user_id, month_count, star_count, ...other }, + signal, + ); + } + + /** + * Sends a gift to the given channel chat. The gift can't be converted to Telegram Stars by the receiver. Returns True on success. + * + * @param chat_id Unique identifier for the chat or username of the channel (in the format @channelusername) that will receive the gift + * @param gift_id Identifier of the gift + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgift + */ + sendGiftToChannel( + chat_id: number | string, + gift_id: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendGift({ chat_id, gift_id, ...other }, signal); + } + + /** + * Use this method to send answers to an inline query. On success, True is returned. + * No more than 50 results per query are allowed. + * + * Example: An inline bot that sends YouTube videos can ask the user to connect the bot to their YouTube account to adapt search results accordingly. To do this, it displays a 'Connect your YouTube account' button above the results, or even before showing any. The user presses the button, switches to a private chat with the bot and, in doing so, passes a start parameter that instructs the bot to return an OAuth link. Once done, the bot can offer a switch_inline button so that the user can easily return to the chat where they wanted to use the bot's inline capabilities. + * + * @param inline_query_id Unique identifier for the answered query + * @param results An Array of results for the inline query + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerinlinequery + */ + answerInlineQuery( + inline_query_id: string, + results: readonly InlineQueryResult[], + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.answerInlineQuery( + { inline_query_id, results, ...other }, + signal, + ); + } + + /** + * Use this method to set the result of an interaction with a Web App and send a corresponding message on behalf of the user to the chat from which the query originated. On success, a SentWebAppMessage object is returned. + * + * @param web_app_query_id Unique identifier for the query to be answered + * @param result An object describing the message to be sent + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerwebappquery + */ + answerWebAppQuery( + web_app_query_id: string, + result: InlineQueryResult, + signal?: AbortSignal, + ) { + return this.raw.answerWebAppQuery({ web_app_query_id, result }, signal); + } + + /** + * Stores a message that can be sent by a user of a Mini App. Returns a PreparedInlineMessage object. + * + * @param user_id Unique identifier of the target user that can use the prepared message + * @param result An object describing the message to be sent + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#savepreparedinlinemessage + */ + savePreparedInlineMessage( + user_id: number, + result: InlineQueryResult, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.savePreparedInlineMessage( + { user_id, result, ...other }, + signal, + ); + } + + /** + * Stores a keyboard button that can be used by a user within a Mini App. Returns a PreparedKeyboardButton object. + * + * @param user_id Unique identifier of the target user that can use the button + * @param button An object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#savepreparedkeyboardbutton + */ + savePreparedKeyboardButton( + user_id: number, + button: + | KeyboardButton.RequestUsersButton + | KeyboardButton.RequestChatButton + | KeyboardButton.RequestManagedBotButton, + signal?: AbortSignal, + ) { + return this.raw.savePreparedKeyboardButton({ user_id, button }, signal); + } + + /** + * Use this method to send invoices. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param title Product name, 1-32 characters + * @param description Product description, 1-255 characters + * @param payload Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + * @param currency Three-letter ISO 4217 currency code, see more on currencies + * @param prices Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendinvoice + */ + sendInvoice( + chat_id: number | string, + title: string, + description: string, + payload: string, + currency: string, + prices: readonly LabeledPrice[], + other?: Other< + R, + "sendInvoice", + | "chat_id" + | "title" + | "description" + | "payload" + | "currency" + | "prices" + >, + signal?: AbortSignal, + ) { + return this.raw.sendInvoice({ + chat_id, + title, + description, + payload, + currency, + prices, + ...other, + }, signal); + } + + /** + * Use this method to create a link for an invoice. Returns the created invoice link as String on success. + * + * @param title Product name, 1-32 characters + * @param description Product description, 1-255 characters + * @param payload Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + * @param provider_token Payment provider token, obtained via BotFather + * @param currency Three-letter ISO 4217 currency code, see more on currencies + * @param prices Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createinvoicelink + */ + createInvoiceLink( + title: string, + description: string, + payload: string, + provider_token: string, + currency: string, + prices: LabeledPrice[], + other?: Other< + R, + "createInvoiceLink", + | "title" + | "description" + | "payload" + | "provider_token" + | "currency" + | "prices" + >, + signal?: AbortSignal, + ) { + return this.raw.createInvoiceLink({ + title, + description, + payload, + provider_token, + currency, + prices, + ...other, + }, signal); + } + + /** + * If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. Use this method to reply to shipping queries. On success, True is returned. + * + * @param shipping_query_id Unique identifier for the query to be answered + * @param ok Pass True if delivery to the specified address is possible and False if there are any problems (for example, if delivery to the specified address is not possible) + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answershippingquery + */ + answerShippingQuery( + shipping_query_id: string, + ok: boolean, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.answerShippingQuery( + { shipping_query_id, ok, ...other }, + signal, + ); + } + + /** + * Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to respond to such pre-checkout queries. On success, True is returned. Note: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent. + * + * @param pre_checkout_query_id Unique identifier for the query to be answered + * @param ok Specify True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerprecheckoutquery + */ + answerPreCheckoutQuery( + pre_checkout_query_id: string, + ok: boolean, + other?: Other< + R, + "answerPreCheckoutQuery", + "pre_checkout_query_id" | "ok" + >, + signal?: AbortSignal, + ) { + return this.raw.answerPreCheckoutQuery( + { pre_checkout_query_id, ok, ...other }, + signal, + ); + } + + /** + * Returns the bot's Telegram Star transactions in chronological order. On success, returns a StarTransactions object. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getstartransactions + */ + getStarTransactions( + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.getStarTransactions({ ...other }, signal); + } + + /** + * Refunds a successful payment in Telegram Stars. + * + * @param user_id Identifier of the user whose payment will be refunded + * @param telegram_payment_charge_id Telegram payment identifier + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#refundstarpayment + */ + refundStarPayment( + user_id: number, + telegram_payment_charge_id: string, + signal?: AbortSignal, + ) { + return this.raw.refundStarPayment( + { user_id, telegram_payment_charge_id }, + signal, + ); + } + + /** + * Allows the bot to cancel or re-enable extension of a subscription paid in Telegram Stars. Returns True on success. + * + * @param user_id Identifier of the user whose subscription will be edited + * @param telegram_payment_charge_id Telegram payment identifier for the subscription + * @param is_canceled Pass True to cancel extension of the user subscription; the subscription must be active up to the end of the current subscription period. Pass False to allow the user to re-enable a subscription that was previously canceled by the bot. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#edituserstarsubscription + */ + editUserStarSubscription( + user_id: number, + telegram_payment_charge_id: string, + is_canceled: boolean, + signal?: AbortSignal, + ) { + return this.raw.editUserStarSubscription( + { user_id, telegram_payment_charge_id, is_canceled }, + signal, + ); + } + + /** + * Verifies a user on behalf of the organization which is represented by the bot. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#verifyuser + */ + verifyUser( + user_id: number, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.verifyUser({ user_id, ...other }, signal); + } + + /** + * Verifies a chat on behalf of the organization which is represented by the bot. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#verifychat + */ + verifyChat( + chat_id: number | string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.verifyChat({ chat_id, ...other }, signal); + } + + /** + * Removes verification from a user who is currently verified on behalf of the organization represented by the bot. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removeuserverification + */ + removeUserVerification(user_id: number, signal?: AbortSignal) { + return this.raw.removeUserVerification({ user_id }, signal); + } + + /** + * Removes verification from a chat that is currently verified on behalf of the organization represented by the bot. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot or channel in the format `@username` + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removechatverification + */ + removeChatVerification( + chat_id: number | string, + signal?: AbortSignal, + ) { + return this.raw.removeChatVerification({ chat_id }, signal); + } + + /** + * Marks incoming message as read on behalf of a business account. Requires the can_read_messages business bot right. Returns True on success. + * + * @param business_connection_id Unique identifier of the business connection on behalf of which to read the message + * @param chat_id Unique identifier of the chat in which the message was received. The chat must have been active in the last 24 hours. + * @param message_id Unique identifier of the message to mark as read + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#readbusinessmessage + */ + readBusinessMessage( + business_connection_id: string, + chat_id: number, + message_id: number, + signal?: AbortSignal, + ) { + return this.raw.readBusinessMessage( + { business_connection_id, chat_id, message_id }, + signal, + ); + } + + /** + * Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed (the contents of the field for which you returned the error must change). Returns True on success. + * + * Use this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted document is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message to make sure the user knows how to correct the issues. + * + * @param user_id User identifier + * @param errors An Array describing the errors + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setpassportdataerrors + */ + setPassportDataErrors( + user_id: number, + errors: readonly PassportElementError[], + signal?: AbortSignal, + ) { + return this.raw.setPassportDataErrors({ user_id, errors }, signal); + } + + /** + * Use this method to send a game. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot in the format `@username`. Games can't be sent to channel direct messages chats and channel chats. + * @param game_short_name Short name of the game, serves as the unique identifier for the game. Set up your games via BotFather. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgame + */ + sendGame( + chat_id: number | string, + game_short_name: string, + other?: Other, + signal?: AbortSignal, + ) { + return this.raw.sendGame( + { chat_id, game_short_name, ...other }, + signal, + ); + } + + /** + * Use this method to set the score of the specified user in a game message. On success, if the message is not an inline message, the Message is returned, otherwise True is returned. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. + * + * @param chat_id Unique identifier for the target chat + * @param message_id Identifier of the sent message + * @param user_id User identifier + * @param score New score, must be non-negative + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setgamescore + */ + setGameScore( + chat_id: number, + message_id: number, + user_id: number, + score: number, + other?: Other< + R, + "setGameScore", + "chat_id" | "message_id" | "inline_message_id" | "user_id" | "score" + >, + signal?: AbortSignal, + ) { + return this.raw.setGameScore( + { chat_id, message_id, user_id, score, ...other }, + signal, + ); + } + + /** + * Use this method to set the score of the specified user in a game message. On success, if the message is not an inline message, the Message is returned, otherwise True is returned. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. + * + * @param inline_message_id Identifier of the inline message + * @param user_id User identifier + * @param score New score, must be non-negative + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setgamescore + */ + setGameScoreInline( + inline_message_id: string, + user_id: number, + score: number, + other?: Other< + R, + "setGameScore", + "chat_id" | "message_id" | "inline_message_id" | "user_id" | "score" + >, + signal?: AbortSignal, + ) { + return this.raw.setGameScore( + { inline_message_id, user_id, score, ...other }, + signal, + ); + } + + /** + * Use this method to get data for high score tables. Will return the score of the specified user and several of their neighbors in a game. Returns an Array of GameHighScore objects. + * + * This method will currently return scores for the target user, plus two of their closest neighbors on each side. Will also return the top three users if the user and his neighbors are not among them. Please note that this behavior is subject to change. + * + * @param chat_id Unique identifier for the target chat + * @param message_id Identifier of the sent message + * @param user_id Target user id + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getgamehighscores + */ + getGameHighScores( + chat_id: number, + message_id: number, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.getGameHighScores( + { chat_id, message_id, user_id }, + signal, + ); + } + + /** + * Use this method to get data for high score tables. Will return the score of the specified user and several of their neighbors in an inline game. On success, returns an Array of GameHighScore objects. + * + * This method will currently return scores for the target user, plus two of their closest neighbors on each side. Will also return the top three users if the user and his neighbors are not among them. Please note that this behavior is subject to change. + * + * @param inline_message_id Identifier of the inline message + * @param user_id Target user id + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getgamehighscores + */ + getGameHighScoresInline( + inline_message_id: string, + user_id: number, + signal?: AbortSignal, + ) { + return this.raw.getGameHighScores( + { inline_message_id, user_id }, + signal, + ); + } +} diff --git a/src/bot.ts b/src/bot.ts index 8b13789..1a648a8 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1 +1,760 @@ +// deno-lint-ignore-file camelcase +import { + BotError, + Composer, + type Middleware, + type ReactionMiddleware, + run, +} from "./composer.ts"; +import { Context, type MaybeArray, type ReactionContext } from "./context.ts"; +import { Api } from "./core/api.ts"; +import { + type ApiClientOptions, + type WebhookReplyEnvelope, +} from "./core/client.ts"; +import { GrammyError, HttpError } from "./core/error.ts"; +import { type Filter, type FilterQuery, parse, preprocess } from "./filter.ts"; +import { debug as d } from "./platform.deno.ts"; +import { + type ReactionType, + type ReactionTypeEmoji, + type Update, + type UserFromGetMe, +} from "./types.ts"; +const debug = d("grammy:bot"); +const debugWarn = d("grammy:warn"); +const debugErr = d("grammy:error"); +export const DEFAULT_UPDATE_TYPES = [ + "message", + "edited_message", + "channel_post", + "edited_channel_post", + "business_connection", + "business_message", + "edited_business_message", + "deleted_business_messages", + "guest_message", + "inline_query", + "chosen_inline_result", + "callback_query", + "shipping_query", + "pre_checkout_query", + "purchased_paid_media", + "poll", + "poll_answer", + "my_chat_member", + "managed_bot", + "chat_join_request", + "chat_boost", + "removed_chat_boost", + "subscription", +] as const satisfies ReadonlyArray>; + +/** + * Options that can be specified when running the bot via simple long polling. + */ +export interface PollingOptions { + /** + * Limits the number of updates to be retrieved per `getUpdates` call. + * Values between 1-100 are accepted. Defaults to 100. + */ + limit?: number; + /** + * Timeout in seconds for long polling. grammY uses 30 seconds as a default + * value. + */ + timeout?: number; + /** + * A list of the update types you want your bot to receive. For example, + * specify ["message", "edited_channel_post", "callback_query"] to only + * receive updates of these types. See Update for a complete list of + * available update types. Specify an empty list to receive all update types + * except chat_member, message_reaction, and message_reaction_count + * (default). If not specified, the previous setting will be used. + * + * Please note that this parameter doesn't affect updates created before the + * call to the getUpdates, so unwanted updates may be received for a short + * period of time. + */ + allowed_updates?: ReadonlyArray>; + /** + * Pass True to drop all pending updates before starting the long polling. + */ + drop_pending_updates?: boolean; + /** + * A callback function that is useful for logging (or setting up middleware + * if you did not do this before). It will be executed after the setup of + * the bot has completed, and immediately before the first updates are being + * fetched. The bot information `bot.botInfo` will be available when the + * function is run. For convenience, the callback function receives the + * value of `bot.botInfo` as an argument. + * + * When this function is invoked, the bot already signals that it is + * running. In other words, `bot.isRunning()` already returns true. + */ + onStart?: (botInfo: UserFromGetMe) => void | Promise; +} + +export { BotError }; +/** + * Error handler that can be installed on a bot to catch error thrown by + * middleware. + */ +export type ErrorHandler = ( + error: BotError, +) => unknown; + +/** + * Options to pass to the bot when creating it. + */ +export interface BotConfig { + /** + * You can specify a number of advanced options under the `client` property. + * The options will be passed to the grammY clientβ€”this is the part of + * grammY that actually connects to the Telegram Bot API server in the end + * when making HTTP requests. + */ + client?: ApiClientOptions; + /** + * grammY automatically calls `getMe` when starting up to make sure that + * your bot has access to the bot's own information. If you restart your bot + * often, for example because it is running in a serverless environment, + * then you may want to skip this initial API call. + * + * Set this property of the options to pre-initialize the bot with cached + * values. If you use this option, grammY will not attempt to make a `getMe` + * call but use the provided data instead. + */ + botInfo?: UserFromGetMe; + /** + * Pass the constructor of a custom context object that will be used when + * creating the context for each incoming update. + */ + ContextConstructor?: new ( + ...args: ConstructorParameters + ) => C; +} + +/** + * 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('') + * // 2. Listen for updates + * bot.on('message:text', ctx => ctx.reply('You wrote: ' + ctx.message.text)) + * // 3. Launch it! + * bot.start() + * ``` + */ +export class Bot< + C extends Context = Context, + A extends Api = Api, +> extends Composer { + private pollingRunning = false; + private pollingAbortController: AbortController | undefined; + private lastTriedUpdateId = 0; + + /** + * Gives you full access to the Telegram Bot API. + * ```ts + * // This is how to call the Bot API methods: + * bot.api.sendMessage(chat_id, 'Hello, grammY!') + * ``` + * + * Use this only outside of your middleware. If you have access to `ctx`, + * then using `ctx.api` instead of `bot.api` is preferred. + */ + public readonly api: A; + + private me: UserFromGetMe | undefined; + private mePromise: Promise | undefined; + private readonly clientConfig: ApiClientOptions | undefined; + + private readonly ContextConstructor: new ( + ...args: ConstructorParameters + ) => C; + + /** Used to log a warning if some update types are not in allowed_updates */ + private 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. + */ + public errorHandler: ErrorHandler = async (err) => { + console.error( + "Error in middleware while handling update", + err.ctx?.update?.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; + }; + + /** + * 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(public readonly token: string, config?: BotConfig) { + super(); + if (!token) throw new Error("Empty token!"); + this.me = config?.botInfo; + this.clientConfig = config?.client; + this.ContextConstructor = config?.ContextConstructor ?? + (Context as unknown as new ( + ...args: ConstructorParameters + ) => C); + this.api = new Api(token, this.clientConfig) as A; + } + + /** + * 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. + */ + public set botInfo(botInfo: UserFromGetMe) { + this.me = botInfo; + } + public get botInfo(): UserFromGetMe { + 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 + */ + override on( + filter: Q | Q[], + ...middleware: Array>> + ): Composer> { + for (const [u] of parse(filter).flatMap(preprocess)) { + this.observedUpdateTypes.add(u); + } + return super.on(filter, ...middleware); + } + /** + * @inheritdoc + */ + override reaction( + reaction: MaybeArray, + ...middleware: Array> + ): Composer> { + this.observedUpdateTypes.add("message_reaction"); + return super.reaction(reaction, ...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?: AbortSignal) { + if (!this.isInited()) { + debug("Initializing bot"); + this.mePromise ??= withRetries( + () => this.api.getMe(signal), + signal, + ); + let me: UserFromGetMe; + 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 + */ + private async handleUpdates(updates: Update[]) { + // 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 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) 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: Update, + webhookReplyEnvelope?: 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( + 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 run(this.middleware(), ctx); + } catch (err) { + debugErr(`Error in middleware for update ${update.update_id}`); + throw new 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) about scaling + * up. + * + * @param options Options to use for simple long polling + */ + async start(options?: PollingOptions) { + // Perform setup + const setup: Promise[] = []; + if (!this.isInited()) { + setup.push(this.init(this.pollingAbortController?.signal)); + } + if (this.pollingRunning) { + await Promise.all(setup); + debug("Simple long polling already running!"); + return; + } + + this.pollingRunning = true; + this.pollingAbortController = new AbortController(); + + setup.push(withRetries(async () => { + await this.api.deleteWebhook({ + drop_pending_updates: options?.drop_pending_updates, + }, this.pollingAbortController?.signal); + }, this.pollingAbortController?.signal)); + + try { + await Promise.all(setup); + // All async ops of setup complete, run callback + await options?.onStart?.(this.botInfo); + } catch (err) { + this.pollingRunning = false; + this.pollingAbortController = undefined; + throw err; + } + + // Bot was stopped during `onStart` + if (!this.pollingRunning) return; + + // Prevent common misuse that leads to missing updates + validateAllowedUpdates( + this.observedUpdateTypes, + 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() { + if (this.pollingRunning) { + debug("Stopping bot, saving update offset"); + this.pollingRunning = false; + this.pollingAbortController?.abort(); + const offset = this.lastTriedUpdateId + 1; + await this.api.getUpdates({ offset, limit: 1 }) + .finally(() => this.pollingAbortController = undefined); + } else { + debug("Bot is not running!"); + } + } + + /** + * Returns true if the bot is currently running via built-in long polling, + * and false otherwise. + * + * If this method returns true, it means that `bot.start()` has been called, + * and that the bot has neither crashed nor was it stopped via a call to + * `bot.stop()`. This also means that you cannot use this method to check if + * a webhook server is running, or if grammY runner was started. + * + * Note that this method will already begin to return true even before the + * call to `bot.start()` has completed its initialization phase (and hence + * before `bot.isInited()` returns true). By extension, this method + * returns true before `onStart` callback of `bot.start()` is invoked. + */ + isRunning() { + return this.pollingRunning; + } + + /** + * 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: ErrorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Internal. Do not call. Enters a loop that will perform long polling until + * the bot is stopped. + */ + private async loop(options?: PollingOptions) { + const limit = options?.limit; + const timeout = options?.timeout ?? 30; // seconds + let allowed_updates: PollingOptions["allowed_updates"] = + options?.allowed_updates ?? []; // reset to default if unspecified + + try { + 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; + } + } finally { + this.pollingRunning = false; + } + } + + /** + * 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. + */ + private async fetchUpdates( + { limit, timeout, allowed_updates }: PollingOptions, + ) { + const offset = this.lastTriedUpdateId + 1; + let updates: Update[] | undefined = undefined; + do { + try { + updates = await this.api.getUpdates( + { offset, limit, timeout, allowed_updates }, + this.pollingAbortController?.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. + */ + private async handlePollingError(error: unknown) { + if (!this.pollingRunning) { + debug("Pending getUpdates request cancelled"); + return; + } + let sleepSeconds = 3; + if (error instanceof GrammyError) { + debugErr(error.message); + // rethrow upon unauthorized or conflict + if (error.error_code === 401 || error.error_code === 409) { + throw error; + } else if (error.error_code === 429) { + debugErr("Bot API server is closing."); + sleepSeconds = error.parameters.retry_after ?? sleepSeconds; + } + } else debugErr(error); + debugErr( + `Call to getUpdates failed, retrying in ${sleepSeconds} seconds ...`, + ); + await sleep(sleepSeconds); + } +} + +/** + * 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 attempt. 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: () => Promise, + signal?: AbortSignal, +): Promise { + // 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: unknown) { + let delay = false; + let strategy: "retry" | "rethrow" = "rethrow"; + + if (error instanceof HttpError) { + delay = true; + strategy = "retry"; + } else if (error instanceof 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 } | { ok: true; value: T } = { 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: number, signal?: AbortSignal) { + let handle: Parameters[0]; + let reject: ((err: Error) => void) | undefined; + function abort() { + reject?.(new Error("Aborted delay")); + if (handle !== undefined) clearTimeout(handle); + } + try { + await new Promise((res, rej) => { + reject = rej; + if (signal?.aborted) { + abort(); + return; + } + signal?.addEventListener("abort", abort); + handle = setTimeout(res, 1000 * seconds); + }); + } finally { + 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: Set, + allowed: readonly string[] = 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(): never { + 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.`); +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..d2fb4ef --- /dev/null +++ b/src/client.ts @@ -0,0 +1,525 @@ +import { baseFetchConfig, debug as d } from "../platform.deno.ts"; +import { + type ApiMethods as Telegram, + type ApiResponse, + type Opts, +} from "../types.ts"; +import { toGrammyError, toHttpError } from "./error.ts"; +import { + createFormDataPayload, + createJsonPayload, + requiresFormDataUpload, +} from "./payload.ts"; +const debug = d("grammy:core"); + +export type Methods = string & keyof R; + +// Available under `bot.api.raw` +/** + * Represents the raw Telegram Bot API with all methods specified 1:1 as + * documented on the website (https://core.telegram.org/bots/api). + * + * Every method takes an optional `AbortSignal` object that allows to cancel the + * API call if desired. + */ +export type RawApi = { + [M in keyof Telegram]: Parameters[0] extends undefined + ? (signal?: AbortSignal) => Promise> + : ( + args: Opts, + signal?: AbortSignal, + ) => Promise>; +}; + +export type Payload, R extends RawApi> = M extends unknown + ? R[M] extends (signal?: AbortSignal) => unknown // deno-lint-ignore ban-types + ? {} // deno-lint-ignore no-explicit-any + : R[M] extends (args: any, signal?: AbortSignal) => unknown + ? Parameters[0] + : never + : never; + +/** + * Small utility interface that abstracts from webhook reply calls of different + * web frameworks. + */ +export interface WebhookReplyEnvelope { + send?: (payload: string) => void | Promise; +} + +/** + * Type of a function that can perform an API call. Used for Transformers. + */ +export type ApiCallFn = >( + method: M, + payload: Payload, + signal?: AbortSignal, +) => Promise>>; + +type ApiCallResult, R extends RawApi> = R[M] extends + (...args: unknown[]) => unknown ? Awaited> : never; + +/** + * API call transformers are functions that can access and modify the method and + * payload of an API call on the fly. This can be useful if you want to + * implement rate limiting or other things against the Telegram Bot API. + * + * Confer the grammY + * [documentation](https://grammy.dev/advanced/transformers) to read more + * about how to use transformers. + */ +export type Transformer = >( + prev: ApiCallFn, + method: M, + payload: Payload, + signal?: AbortSignal, +) => Promise>>; +export type TransformerConsumer = TransformableApi< + R +>["use"]; +/** + * A transformable API enhances the `RawApi` type by transformers. + */ +export interface TransformableApi { + /** + * Access to the raw API that the transformers will be installed on. + */ + raw: R; + /** + * Can be used to register any number of transformers on the API. + */ + use: (...transformers: Transformer[]) => this; + /** + * Returns a readonly list or the currently installed transformers. The list + * is sorted by time of installation where index 0 represents the + * transformer that was installed first. + */ + installedTransformers: Transformer[]; +} + +// Transformer base functions +function concatTransformer( + prev: ApiCallFn, + trans: Transformer, +): ApiCallFn { + return (method, payload, signal) => trans(prev, method, payload, signal); +} + +/** + * Options to pass to the API client that eventually connects to the Telegram + * Bot API server and makes the HTTP requests. + */ +export interface ApiClientOptions { + /** + * Root URL of the Telegram Bot API server. Default: + * https://api.telegram.org + */ + apiRoot?: string; + /** + * Specifies whether to use the [test + * environment](https://core.telegram.org/bots/webapps#using-bots-in-the-test-environment). + * Can be either `"prod"` (default) or `"test"`. + * + * The testing infrastructure is separate from the regular production + * infrastructure. No chats, accounts, or other data is shared between them. + * If you set this option to `"test"`, you will need to make your Telegram + * client connect to the testing data centers of Telegram, register your + * phone number again, open a new chat with @BotFather, and create a + * separate bot. + */ + environment?: "prod" | "test"; + /** + * URL builder function for API calls. Can be used to modify which API + * server should be called. + * + * @param root The URL that was passed in `apiRoot`, or its default value + * @param token The bot's token that was passed when creating the bot + * @param method The API method to be called, e.g. `getMe` + * @param env The value that was passed in `environment`, or its default value + * @returns The URL that will be fetched during the API call + */ + buildUrl?: ( + root: string, + token: string, + method: string, + env: "prod" | "test", + ) => string | URL; + /** + * Maximum number of seconds that a request to the Bot API server may take. + * If a request has not completed before this time has elapsed, grammY + * aborts the request and errors. Without such a timeout, networking issues + * may cause your bot to leave open a connection indefinitely, which may + * effectively make your bot freeze. + * + * You probably do not have to care about this option. In rare cases, you + * may want to adjust it if you are transferring large files via slow + * connections to your own Bot API server. + * + * The default number of seconds is `500`, which corresponds to 8 minutes + * and 20 seconds. Note that this is also the value that is hard-coded in + * the official Bot API server, so you cannot perform any successful + * requests that exceed this time frame (even if you would allow it in + * grammY). Setting this option to higher than the default only makes sense + * with a custom Bot API server. + */ + timeoutSeconds?: number; + /** + * If the bot is running on webhooks, as soon as the bot receives an update + * from Telegram, it is possible to make up to one API call in the response + * to the webhook request. As a benefit, this saves your bot from making up + * to one HTTP request per update. However, there are a number of drawbacks + * to using this: + * 1) You will not be able to handle potential errors of the respective API + * call. This includes rate limiting errors, so sent messages can be + * swallowed by the Bot API server and there is no way to detect if a + * message was actually sent or not. + * 2) More importantly, you also won't have access to the response object, + * so e.g. calling `sendMessage` will not give you access to the message + * you sent. + * 3) Furthermore, it is not possible to cancel the request. The + * `AbortSignal` will be disregarded. + * 4) Note also that the types in grammY do not reflect the consequences of + * a performed webhook callback! For instance, they indicate that you + * always receive a response object, so it is your own responsibility to + * make sure you're not screwing up while using this minor performance + * optimization. + * + * With this warning out of the way, here is what you can do with the + * `canUseWebhookReply` option: it can be used to pass a function that + * determines whether to use webhook reply for the given method. It will + * only be invoked if the payload can be sent as JSON. It will not be + * invoked again for a given update after it returned `true`, indicating + * that the API call should be performed as a webhook send. In other words, + * subsequent API calls (during the same update) will always perform their + * own HTTP requests. + * + * @param method The method to call + */ + canUseWebhookReply?: (method: string) => boolean; + /** + * Base configuration for `fetch` calls. Specify any additional parameters + * to use when fetching a method of the Telegram Bot API. Default: `{ + * compress: true, duplex: "half" }` (Node), `{ duplex: "half" }` (Deno, Web) + */ + baseFetchConfig?: Omit< + NonNullable[1]>, + "method" | "headers" | "body" + >; + + /** + * `fetch` function to use for making HTTP requests. Default: `node-fetch` in Node.js, `fetch` in Deno. + */ + fetch?: typeof fetch; + + /** + * When the network connection is unreliable and some API requests fail + * because of that, grammY will throw errors that tell you exactly which + * requests failed. However, the error messages do not disclose the fetched + * URL as it contains your bot's token. Logging it may lead to token leaks. + * + * If you are sure that no logs are ever posted in Telegram chats, GitHub + * issues, or otherwise shared, you can set this option to `true` in order + * to obtain more detailed logs that may help you debug your bot. The + * default value is `false`, meaning that the bot token is not logged. + */ + sensitiveLogs?: boolean; +} + +class ApiClient { + private readonly options: Required; + + private readonly fetch: typeof fetch; + + private hasUsedWebhookReply = false; + + readonly installedTransformers: Transformer[] = []; + + constructor( + private readonly token: string, + options: ApiClientOptions = {}, + private readonly webhookReplyEnvelope: WebhookReplyEnvelope = {}, + ) { + const apiRoot = options.apiRoot ?? "https://api.telegram.org"; + const environment = options.environment ?? "prod"; + + // In an ideal world, `fetch` is independent of the context being called, + // but in a Cloudflare worker, any context other than global throws an error. + // That is why we need to call custom fetch or fetch without context. + const { fetch: customFetch } = options; + const fetchFn = customFetch ?? fetch; + + this.options = { + apiRoot, + environment, + buildUrl: options.buildUrl ?? defaultBuildUrl, + timeoutSeconds: options.timeoutSeconds ?? 500, + baseFetchConfig: { + ...baseFetchConfig(apiRoot), + ...options.baseFetchConfig, + }, + canUseWebhookReply: options.canUseWebhookReply ?? (() => false), + sensitiveLogs: options.sensitiveLogs ?? false, + fetch: + ((...args: Parameters) => + fetchFn(...args)) as typeof fetch, + }; + this.fetch = this.options.fetch; + if (this.options.apiRoot.endsWith("/")) { + throw new Error( + `Remove the trailing '/' from the 'apiRoot' option (use '${ + this.options.apiRoot.substring( + 0, + this.options.apiRoot.length - 1, + ) + }' instead of '${this.options.apiRoot}')`, + ); + } + } + + private call: ApiCallFn = async >( + method: M, + p: Payload, + signal?: AbortSignal, + ) => { + const payload = p ?? {}; + debug(`Calling ${method}`); + if (signal !== undefined) validateSignal(method, payload, signal); + // General config + const opts = this.options; + const formDataRequired = requiresFormDataUpload(payload); + // Short-circuit on webhook reply + if ( + this.webhookReplyEnvelope.send !== undefined && + !this.hasUsedWebhookReply && + !formDataRequired && + opts.canUseWebhookReply(method) + ) { + this.hasUsedWebhookReply = true; + const config = createJsonPayload({ ...payload, method }); + await this.webhookReplyEnvelope.send(config.body); + return { ok: true, result: true as ApiCallResult }; + } + // Handle timeouts and errors in the underlying form-data stream + const { controller, unregisterSignal } = + createAbortControllerFromSignal(signal); + const timeout = createTimeout(controller, opts.timeoutSeconds, method); + const streamErr = createStreamError(controller); + // Build request URL and config + const url = opts.buildUrl( + opts.apiRoot, + this.token, + method, + opts.environment, + ); + const config = formDataRequired + ? createFormDataPayload(payload, (err) => streamErr.catch(err)) + : createJsonPayload(payload); + const sig = controller.signal; + const options = { ...opts.baseFetchConfig, signal: sig, ...config }; + // Perform fetch call + const successPromise = this.fetch(url, options) + .then((res) => res.json()); + // Those are the three possible outcomes of the fetch call: + const operations = [successPromise, streamErr.promise, timeout.promise]; + // Wait for result + try { + return await Promise.race(operations); + } catch (error) { + throw toHttpError(method, opts.sensitiveLogs, error); + } finally { + if (timeout.handle !== undefined) clearTimeout(timeout.handle); + unregisterSignal?.(); + } + }; + + use(...transformers: Transformer[]) { + this.call = transformers.reduce(concatTransformer, this.call); + this.installedTransformers.push(...transformers); + return this; + } + + async callApi>( + method: M, + payload: Payload, + signal?: AbortSignal, + ) { + const data = await this.call(method, payload, signal); + if (data.ok) return data.result; + else throw toGrammyError(data, method, payload); + } +} + +/** + * Creates a new transformable API, i.e. an object that lets you perform raw API + * calls to the Telegram Bot API server but pass the calls through a stack of + * transformers before. This will create a new API client instance under the + * hood that will be used to connect to the Telegram servers. You therefore need + * to pass the bot token. In addition, you may pass API client options as well + * as a webhook reply envelope that allows the client to perform up to one HTTP + * request in response to a webhook call if this is desired. + * + * @param token The bot's token + * @param options A number of options to pass to the created API client + * @param webhookReplyEnvelope The webhook reply envelope that will be used + */ +export function createRawApi( + token: string, + options?: ApiClientOptions, + webhookReplyEnvelope?: WebhookReplyEnvelope, +): TransformableApi { + const client = new ApiClient(token, options, webhookReplyEnvelope); + + const proxyHandler: ProxyHandler = { + get(_, m: Methods | "toJSON") { + return m === "toJSON" + ? "__internal" + // Methods with zero parameters are called without any payload, + // so we have to manually inject an empty payload. + : m === "getMe" || + m === "getWebhookInfo" || + m === "getForumTopicIconStickers" || + m === "getAvailableGifts" || + m === "logOut" || + m === "close" || + m === "getMyStarBalance" || + m === "removeMyProfilePhoto" + ? client.callApi.bind(client, m, {} as Payload) + : client.callApi.bind(client, m); + }, + ...proxyMethods, + }; + const raw = new Proxy({} as R, proxyHandler); + const installedTransformers = client.installedTransformers; + const api: TransformableApi = { + raw, + installedTransformers, + use: (...t) => { + client.use(...t); + return api; + }, + }; + + return api; +} + +const defaultBuildUrl: NonNullable = ( + root, + token, + method, + env, +) => { + const prefix = env === "test" ? "test/" : ""; + return `${root}/bot${token}/${prefix}${method}`; +}; + +const proxyMethods = { + set() { + return false; + }, + defineProperty() { + return false; + }, + deleteProperty() { + return false; + }, + ownKeys() { + return []; + }, +}; + +/** A container for a rejecting promise */ +interface AsyncError { + promise: Promise; +} +/** An async error caused by a timeout */ +interface Timeout extends AsyncError { + handle: ReturnType | undefined; +} +/** An async error caused by an error in an underlying resource stream */ +interface StreamError extends AsyncError { + catch: (err: unknown) => void; +} + +/** Creates a timeout error which aborts a given controller */ +function createTimeout( + controller: AbortController, + seconds: number, + method: string, +): Timeout { + let handle: Timeout["handle"] = undefined; + const promise = new Promise((_, reject) => { + handle = setTimeout(() => { + const msg = + `Request to '${method}' timed out after ${seconds} seconds`; + reject(new Error(msg)); + controller.abort(); + }, 1000 * seconds); + }); + return { promise, handle }; +} +/** Creates a stream error which abort a given controller */ +function createStreamError(abortController: AbortController): StreamError { + let onError: StreamError["catch"] = (err) => { + // Re-throw by default, but will be overwritten immediately + throw err; + }; + const promise = new Promise((_, reject) => { + onError = (err: unknown) => { + reject(err); + abortController.abort(); + }; + }); + return { promise, catch: onError }; +} + +function createAbortControllerFromSignal(signal?: AbortSignal) { + const controller = new AbortController(); + if (signal === undefined) { + return { controller, unregisterSignal: undefined }; + } + const sig = signal; + function abort() { + controller.abort(); + unregisterSignal(); + } + function unregisterSignal() { + sig.removeEventListener("abort", abort); + } + if (sig.aborted) abort(); + else sig.addEventListener("abort", abort); + return { + controller: { abort, signal: controller.signal }, + unregisterSignal, + }; +} + +function validateSignal( + method: string, + payload: Record, + signal: AbortSignal, +) { + // We use a very simple heuristic to check for AbortSignal instances + // in order to avoid doing a runtime-specific version of `instanceof`. + if (typeof signal?.addEventListener === "function") { + return; + } + + let payload0 = JSON.stringify(payload); + if (payload0.length > 20) { + payload0 = payload0.substring(0, 16) + " ..."; + } + let payload1 = JSON.stringify(signal); + if (payload1.length > 20) { + payload1 = payload1.substring(0, 16) + " ..."; + } + throw new Error( + `Incorrect abort signal instance found! \ +You passed two payloads to '${method}' but you should merge \ +the second one containing '${payload1}' into the first one \ +containing '${payload0}'! If you are using context shortcuts, \ +you may want to use a method on 'ctx.api' instead. + +If you want to prevent such mistakes in the future, \ +consider using TypeScript. https://www.typescriptlang.org/`, + ); +} diff --git a/src/composer.ts b/src/composer.ts new file mode 100644 index 0000000..2096b4f --- /dev/null +++ b/src/composer.ts @@ -0,0 +1,1038 @@ +import { + type CallbackQueryContext, + type ChatTypeContext, + type ChosenInlineResultContext, + type CommandContext, + Context, + type GameQueryContext, + type HearsContext, + type InlineQueryContext, + type MaybeArray, + type PreCheckoutQueryContext, + type ReactionContext, + type ShippingQueryContext, + type StringWithCommandSuggestions, +} from "./context.ts"; +import { type Filter, type FilterQuery } from "./filter.ts"; +import { + type Chat, + type ReactionType, + type ReactionTypeEmoji, +} from "./types.ts"; + +type MaybePromise = T | Promise; + +// === Middleware types +/** + * A function of this type is passed as the second parameter to all middleware. + * Invoke it to call the downstream middleware and pass on the control flow. + * + * In other words, if your middleware is done handling the context object, and + * other middleware should take over, this function should be called and + * `await`ed. + * + * Once the `Promise` returned by this function resolves, the downstream + * middleware is done executing, hence returning the control. + */ +export type NextFunction = () => Promise; + +/** + * Middleware in the form of a function. + */ +export type MiddlewareFn = ( + ctx: C, + next: NextFunction, +) => MaybePromise; +/** + * Middleware in the form of a container for a function. + */ +export interface MiddlewareObj { + /** + * Returns the contained middleware. + */ + middleware: () => MiddlewareFn; +} +/** + * Middleware for grammY, either as a function or as a container for a function. + * + * Simply put, middleware is just a fancy term for a _listener_. You can + * register middleware on a bot to listen for updates. Example: + * + * ```ts + * bot.on('message', ctx => ctx.reply('I got your message!')) + * // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * // ^ + * // | + * // This is middleware! + * ``` + * + * Middleware receives one object that we call the _context object_. This is + * another fancy term for a simple object that holds information about the + * update you're processing. For instance, the context object gives you access + * to the message that was sent to your bot (`ctx.message`), including the text + * (or photo or whatever message the user has sent). The context object is + * commonly named `ctx`. + * + * It also provides you with the `ctx.api` object that you also find on + * `bot.api`. As a result, you can call `ctx.api.sendMessage` instead of + * `bot.api.sendMessage`. This prevents you from having to pass around your + * `bot` instance all over your code. + * + * Most importantly, the context object gives you a handful of really useful + * shortcuts, such as a `reply` method (see above). This method is nothing else + * than a wrapper around `ctx.api.sendMessage`β€”but with some arguments + * pre-filled for you. As you can see above, you no longer have to specify a + * `chat_id` or anything; the context object knows which chat it belongs to, so + * when you call `reply`, the context will call `sendMessage` with the correct + * `chat_id`, namely the one for the same chat that the incoming message + * originates from. This makes it very convenient to reply to a message. + * + * Middleware is an extremely powerful concept and this short explanation only + * scratched the surface of what is possible with grammY. If you want to know + * more advanced things about middleware, check out the + * [documentation](https://grammy.dev/guide/middleware) on the website. + */ +export type Middleware = + | MiddlewareFn + | MiddlewareObj; + +// === Middleware errors +/** + * This error is thrown when middleware throws. It simply wraps the original + * error (accessible via the `error` property), but also provides access to the + * respective context object that was processed while the error occurred. + */ +export class BotError extends Error { + constructor(public readonly error: unknown, public readonly ctx: C) { + super(generateBotErrorMessage(error)); + this.name = "BotError"; + if (error instanceof Error) this.stack = error.stack; + } +} +function generateBotErrorMessage(error: unknown) { + let msg: string; + if (error instanceof Error) { + msg = `${error.name} in middleware: ${error.message}`; + } else { + const type = typeof error; + msg = `Non-error value of type ${type} thrown in middleware`; + switch (type) { + case "bigint": + case "boolean": + case "number": + case "symbol": + msg += `: ${error}`; + break; + case "string": + msg += `: ${String(error).substring(0, 50)}`; + break; + default: + msg += "!"; + break; + } + } + return msg; +} + +// === Middleware base functions +function flatten(mw: Middleware): MiddlewareFn { + return typeof mw === "function" + ? mw + : (ctx, next) => mw.middleware()(ctx, next); +} +function concat( + first: MiddlewareFn, + andThen: MiddlewareFn, +): MiddlewareFn { + return async (ctx, next) => { + let nextCalled = false; + await first(ctx, async () => { + if (nextCalled) throw new Error("`next` already called before!"); + else nextCalled = true; + await andThen(ctx, next); + }); + }; +} +function pass(_ctx: C, next: NextFunction) { + return next(); +} + +const leaf: NextFunction = () => Promise.resolve(); +/** + * Runs some given middleware function with a given context object. + * + * @param middleware The middleware to run + * @param ctx The context to use + */ +export async function run( + middleware: MiddlewareFn, + ctx: C, +) { + await middleware(ctx, leaf); +} + +// === Composer +/** + * The composer is the heart of the middleware system in grammY. It is also the + * superclass of `Bot`. Whenever you call `use` or `on` or some of the other + * methods on your bot, you are in fact using the underlying composer instance + * to register your middleware. + * + * If you're just getting started, you do not need to worry about what + * middleware is, or about how to use a composer. + * + * On the other hand, if you want to dig deeper into how grammY implements + * middleware, check out the + * [documentation](https://grammy.dev/advanced/middleware) on the website. + */ +export class Composer implements MiddlewareObj { + private handler: MiddlewareFn; + + /** + * Constructs a new composer based on the provided middleware. If no + * middleware is given, the composer instance will simply make all context + * objects pass through without touching them. + * + * @param middleware The middleware to compose + */ + constructor(...middleware: Array>) { + this.handler = middleware.length === 0 + ? pass + : middleware.map(flatten).reduce(concat); + } + + middleware() { + return this.handler; + } + + /** + * Registers some middleware that receives all updates. It is installed by + * concatenating it to the end of all previously installed middleware. + * + * Often, this method is used to install middleware that behaves like a + * plugin, for example session middleware. + * ```ts + * bot.use(session()) + * ``` + * + * This method returns a new instance of composer. The returned instance can + * be further extended, and all changes will be regarded here. Confer the + * [documentation](https://grammy.dev/advanced/middleware) on the + * website if you want to know more about how the middleware system in + * grammY works, especially when it comes to chaining the method calls + * (`use( ... ).use( ... ).use( ... )`). + * + * @param middleware The middleware to register + */ + use(...middleware: Array>) { + const composer = new Composer(...middleware); + this.handler = concat(this.handler, flatten(composer)); + return composer; + } + + /** + * Registers some middleware that will only be executed for some specific + * updates, namely those matching the provided filter query. Filter queries + * are a concise way to specify which updates you are interested in. + * + * Here are some examples of valid filter queries: + * ```ts + * // All kinds of message updates + * bot.on('message', ctx => { ... }) + * + * // Only text messages + * bot.on('message:text', ctx => { ... }) + * + * // Only text messages with URL + * bot.on('message:entities:url', ctx => { ... }) + * + * // Text messages and text channel posts + * bot.on(':text', ctx => { ... }) + * + * // Messages with URL in text or caption (i.e. entities or caption entities) + * bot.on('message::url', ctx => { ... }) + * + * // Messages or channel posts with URL in text or caption + * bot.on('::url', ctx => { ... }) + * ``` + * + * You can use autocomplete in VS Code to see all available filter queries. + * Check out the + * [documentation](https://grammy.dev/guide/filter-queries) on the + * website to learn more about filter queries in grammY. + * + * It is possible to pass multiple filter queries in an array, i.e. + * ```ts + * // Matches all text messages and edited text messages that contain a URL + * bot.on(['message:entities:url', 'edited_message:entities:url'], ctx => { ... }) + * ``` + * + * Your middleware will be executed if _any of the provided filter queries_ + * matches (logical OR). + * + * If you instead want to match _all of the provided filter queries_ + * (logical AND), you can chain the `.on` calls: + * ```ts + * // Matches all messages and channel posts that both a) contain a URL and b) are forwards + * bot.on('::url').on(':forward_origin', ctx => { ... }) + * ``` + * + * @param filter The filter query to use, may also be an array of queries + * @param middleware The middleware to register behind the given filter + */ + on( + filter: Q | Q[], + ...middleware: Array>> + ): Composer> { + return this.filter(Context.has.filterQuery(filter), ...middleware); + } + + /** + * Registers some middleware that will only be executed when the message + * contains some text. Is it possible to pass a regular expression to match: + * ```ts + * // Match some text (exact match) + * bot.hears('I love grammY', ctx => ctx.reply('And grammY loves you! <3')) + * // Match a regular expression + * bot.hears(/\/echo (.+)/, ctx => ctx.reply(ctx.match[1])) + * ``` + * Note how `ctx.match` will contain the result of the regular expression. + * Here it is a `RegExpMatchArray` object, so `ctx.match[1]` refers to the + * part of the regex that was matched by `(.+)`, i.e. the text that comes + * after β€œ/echo”. + * + * You can pass an array of triggers. Your middleware will be executed if at + * least one of them matches. + * + * Both text and captions of the received messages will be scanned. For + * example, when a photo is sent to the chat and its caption matches the + * trigger, your middleware will be executed. + * + * If you only want to match text messages and not captions, you can do + * this: + * ```ts + * // Only matches text messages (and channel posts) for the regex + * bot.on(':text').hears(/\/echo (.+)/, ctx => { ... }) + * ``` + * + * @param trigger The text to look for + * @param middleware The middleware to register + */ + hears( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.text(trigger), ...middleware); + } + + /** + * Registers some middleware that will only be executed when a certain + * command is found. + * ```ts + * // Reacts to /start commands + * bot.command('start', ctx => { ... }) + * // Reacts to /help commands + * bot.command('help', ctx => { ... }) + * ``` + * + * The rest of the message (excluding the command, and trimmed) is provided + * via `ctx.match`. + * + * > **Did you know?** You can use deep linking + * > (https://core.telegram.org/bots/features#deep-linking) to let users + * > start your bot with a custom payload. As an example, send someone the + * > link https://t.me/name-of-your-bot?start=custom-payload and register a + * > start command handler on your bot with grammY. As soon as the user + * > starts your bot, you will receive `custom-payload` in the `ctx.match` + * > property! + * > ```ts + * > bot.command('start', ctx => { + * > const payload = ctx.match // will be 'custom-payload' + * > }) + * > ``` + * + * Note that commands are not matched in captions or in the middle of the + * text. + * ```ts + * bot.command('start', ctx => { ... }) + * // ... does not match: + * // A message saying: β€œsome text /start some more text” + * // A photo message with the caption β€œ/start” + * ``` + * + * By default, commands are detected in channel posts, too. This means that + * `ctx.message` is potentially `undefined`, so you should use `ctx.msg` + * instead to grab both messages and channel posts. Alternatively, if you + * want to limit your bot to finding commands only in private and group + * chats, you can use `bot.on('message').command('start', ctx => { ... })`, + * or even store a message-only version of your bot in a variable like so: + * ```ts + * const m = bot.on('message') + * + * m.command('start', ctx => { ... }) + * m.command('help', ctx => { ... }) + * // etc + * ``` + * + * If you need more freedom matching your commands, check out the `commands` + * plugin. + * + * @param command The command to look for + * @param middleware The middleware to register + */ + command( + command: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.command(command), ...middleware); + } + + /** + * Registers some middleware that will only be added when a new reaction of + * the given type is added to a message. + * ```ts + * // Reacts to new 'πŸ‘' reactions + * bot.reaction('πŸ‘', ctx => { ... }) + * // Reacts to new 'πŸ‘' or 'πŸ‘Ž' reactions + * bot.reaction(['πŸ‘', 'πŸ‘Ž'], ctx => { ... }) + * ``` + * + * > Note that you have to enable `message_reaction` updates in + * `allowed_updates` if you want your bot to receive updates about message + * reactions. + * + * `bot.reaction` will trigger if: + * - a new emoji reaction is added to a message + * - a new custom emoji reaction is added a message + * + * `bot.reaction` will not trigger if: + * - a reaction is removed + * - an anonymous reaction count is updated, such as on channel posts + * - `message_reaction` updates are not enabled for your bot + * + * @param reaction The reaction to look for + * @param middleware The middleware to register + */ + reaction( + reaction: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.reaction(reaction), ...middleware); + } + + /** + * Registers some middleware for certain chat types only. For example, you + * can use this method to only receive updates from private chats. The four + * chat types are `"channel"`, `"supergroup"`, `"group"`, and `"private"`. + * This is especially useful when combined with other filtering logic. For + * example, this is how can you respond to `/start` commands only from + * private chats: + * ```ts + * bot.chatType("private").command("start", ctx => { ... }) + * ``` + * + * Naturally, you can also use this method on its own. + * ```ts + * // Private chats only + * bot.chatType("private", ctx => { ... }); + * // Channels only + * bot.chatType("channel", ctx => { ... }); + * ``` + * + * You can pass an array of chat types if you want your middleware to run + * for any of several provided chat types. + * ```ts + * // Groups and supergroups only + * bot.chatType(["group", "supergroup"], ctx => { ... }); + * ``` + * [Remember](https://grammy.dev/guide/context#shortcuts) also that you + * can access the chat type via `ctx.chat.type`. + * + * @param chatType The chat type + * @param middleware The middleware to register + */ + chatType( + chatType: MaybeArray, + ...middleware: Array>> + ): Composer> { + return this.filter(Context.has.chatType(chatType), ...middleware); + } + + /** + * Registers some middleware for callback queries, i.e. the updates that + * Telegram delivers to your bot when a user clicks an inline button (that + * is a button under a message). + * + * This method is essentially the same as calling + * ```ts + * bot.on('callback_query:data', ctx => { ... }) + * ``` + * but it also allows you to match the query data against a given text or + * regular expression. + * + * ```ts + * // Create an inline keyboard + * const keyboard = new InlineKeyboard().text('Go!', 'button-payload') + * // Send a message with the keyboard + * await bot.api.sendMessage(chat_id, 'Press a button!', { + * reply_markup: keyboard + * }) + * // Listen to users pressing buttons with that specific payload + * bot.callbackQuery('button-payload', ctx => { ... }) + * + * // Listen to users pressing any button your bot ever sent + * bot.on('callback_query:data', ctx => { ... }) + * ``` + * + * Always remember to call `answerCallbackQuery`β€”even if you don't perform + * any action: https://core.telegram.org/bots/api#answercallbackquery + * ```ts + * bot.on('callback_query:data', async ctx => { + * await ctx.answerCallbackQuery() + * }) + * ``` + * + * You can pass an array of triggers. Your middleware will be executed if at + * least one of them matches. + * + * @param trigger The string to look for in the payload + * @param middleware The middleware to register + */ + callbackQuery( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.callbackQuery(trigger), ...middleware); + } + + /** + * Registers some middleware for game queries, i.e. the updates that + * Telegram delivers to your bot when a user clicks an inline button for the + * HTML5 games platform on Telegram. + * + * This method is essentially the same as calling + * ```ts + * bot.on('callback_query:game_short_name', ctx => { ... }) + * ``` + * but it also allows you to match the query data against a given text or + * regular expression. + * + * You can pass an array of triggers. Your middleware will be executed if at + * least one of them matches. + * + * @param trigger The string to look for in the payload + * @param middleware The middleware to register + */ + gameQuery( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.gameQuery(trigger), ...middleware); + } + + /** + * Registers middleware for inline queries. Telegram sends an inline query + * to your bot whenever a user types β€œ@your_bot_name ...” into a text field + * in Telegram. You bot will then receive the entered search query and can + * respond with a number of results (text, images, etc) that the user can + * pick from to send a message _via_ your bot to the respective chat. Check + * out https://core.telegram.org/bots/inline to read more about inline bots. + * + * > Note that you have to enable inline mode for you bot by contacting + * > @BotFather first. + * + * ```ts + * // Listen for users typing β€œ@your_bot_name query” + * bot.inlineQuery('query', async ctx => { + * // Answer the inline query, confer https://core.telegram.org/bots/api#answerinlinequery + * await ctx.answerInlineQuery( ... ) + * }) + * ``` + * + * @param trigger The inline query text to match + * @param middleware The middleware to register + */ + inlineQuery( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.inlineQuery(trigger), ...middleware); + } + + /** + * Registers middleware for the ChosenInlineResult by the given id or ids. + * ChosenInlineResult represents a result of an inline query that was chosen + * by the user and sent to their chat partner. Check out + * https://core.telegram.org/bots/api#choseninlineresult to read more about + * chosen inline results. + * + * ```ts + * bot.chosenInlineResult('id', async ctx => { + * const id = ctx.result_id; + * // Your code + * }) + * ``` + * + * @param resultId An id or array of ids + * @param middleware The middleware to register + */ + chosenInlineResult( + resultId: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter( + Context.has.chosenInlineResult(resultId), + ...middleware, + ); + } + + /** + * Registers middleware for pre-checkout queries. Telegram sends a + * pre-checkout query to your bot whenever a user has confirmed their + * payment and shipping details. You bot will then receive all information + * about the order and has to respond within 10 seconds with a confirmation + * of whether everything is alright (goods are available, etc.) and the bot + * is ready to proceed with the order. Check out + * https://core.telegram.org/bots/api#precheckoutquery to read more about + * pre-checkout queries. + * + * ```ts + * bot.preCheckoutQuery('invoice_payload', async ctx => { + * // Answer the pre-checkout query, confer https://core.telegram.org/bots/api#answerprecheckoutquery + * await ctx.answerPreCheckoutQuery( ... ) + * }) + * ``` + * + * @param trigger The string to look for in the invoice payload + * @param middleware The middleware to register + */ + preCheckoutQuery( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter( + Context.has.preCheckoutQuery(trigger), + ...middleware, + ); + } + + /** + * Registers middleware for shipping queries. If you sent an invoice + * requesting a shipping address and the parameter _is_flexible_ was + * specified, Telegram will send a shipping query to your bot whenever a + * user has confirmed their shipping details. You bot will then receive the + * shipping information and can respond with a confirmation of whether + * delivery to the specified address is possible. Check out + * https://core.telegram.org/bots/api#shippingquery to read more about + * shipping queries. + * + * ```ts + * bot.shippingQuery('invoice_payload', async ctx => { + * // Answer the shipping query, confer https://core.telegram.org/bots/api#answershippingquery + * await ctx.answerShippingQuery( ... ) + * }) + * ``` + * + * @param trigger The string to look for in the invoice payload + * @param middleware The middleware to register + */ + shippingQuery( + trigger: MaybeArray, + ...middleware: Array> + ): Composer> { + return this.filter(Context.has.shippingQuery(trigger), ...middleware); + } + + /** + * > This is an advanced method of grammY. + * + * Registers middleware behind a custom filter function that operates on the + * context object and decides whether or not to execute the middleware. In + * other words, the middleware will only be executed if the given predicate + * returns `true` for the given context object. Otherwise, it will be + * skipped and the next middleware will be executed. + * + * This method has two signatures. The first one is straightforward, it is + * the one described above. Note that the predicate may be asynchronous, + * i.e. it can return a Promise of a boolean. + * + * Alternatively, you can pass a function that has a type predicate as + * return type. This will allow you to narrow down the context object. The + * installed middleware is then able to operate on this constrained context + * object. + * ```ts + * // NORMAL USAGE + * // Only process every second update + * bot.filter(ctx => ctx.update.update_id % 2 === 0, ctx => { ... }) + * + * // TYPE PREDICATE USAGE + * function predicate(ctx): ctx is Context & { message: undefined } { + * return ctx.message === undefined + * } + * // Only process updates where `message` is `undefined` + * bot.filter(predicate, ctx => { + * const m = ctx.message // inferred as always undefined! + * const m2 = ctx.update.message // also inferred as always undefined! + * }) + * ``` + * + * @param predicate The predicate to check + * @param middleware The middleware to register + */ + filter( + predicate: (ctx: C) => ctx is D, + ...middleware: Array> + ): Composer; + filter( + predicate: (ctx: C) => MaybePromise, + ...middleware: Array> + ): Composer; + filter( + predicate: (ctx: C) => MaybePromise, + ...middleware: Array> + ) { + const composer = new Composer(...middleware); + this.branch(predicate, composer, pass); + return composer; + } + + /** + * > This is an advanced method of grammY. + * + * Registers middleware behind a custom filter function that operates on the + * context object and decides whether or not to execute the middleware. In + * other words, the middleware will only be executed if the given predicate + * returns `false` for the given context object. Otherwise, it will be + * skipped and the next middleware will be executed. Note that the predicate + * may be asynchronous, i.e. it can return a Promise of a boolean. + * + * This method is the same using `filter` (normal usage) with a negated + * predicate. + * + * @param predicate The predicate to check + * @param middleware The middleware to register + */ + drop( + predicate: (ctx: C) => MaybePromise, + ...middleware: Array> + ) { + return this.filter( + async (ctx: C) => !(await predicate(ctx)), + ...middleware, + ); + } + + /** + * > This is an advanced method of grammY. + * + * Registers some middleware that runs concurrently to the executing + * middleware stack. + * ```ts + * bot.use( ... ) // will run first + * bot.fork( ... ) // will be started second, but run concurrently + * bot.use( ... ) // will also be run second + * ``` + * In the first middleware, as soon as `next`'s Promise resolves, both forks + * have completed. + * + * Both the fork and the downstream middleware are awaited with + * `Promise.all`, so you will only be able to catch at most one error (the + * one that is thrown first). + * + * In contrast to the other middleware methods on composer, `fork` does not + * simply return the composer connected to the main middleware stack. + * Instead, it returns the created composer _of the fork_ connected to the + * middleware stack. This allows for the following pattern. + * ```ts + * // Middleware will be run concurrently! + * bot.fork().on('message', ctx => { ... }) + * ``` + * + * @param middleware The middleware to run concurrently + */ + fork(...middleware: Array>) { + const composer = new Composer(...middleware); + const fork = flatten(composer); + this.use((ctx, next) => Promise.all([next(), run(fork, ctx)])); + return composer; + } + + /** + * > This is an advanced method of grammY. + * + * Executes some middleware that can be generated on the fly for each + * context. Pass a factory function that creates some middleware (or a + * middleware array even). The factory function will be called once per + * context, and its result will be executed with the context object. + * ```ts + * // The middleware returned by `createMyMiddleware` will be used only once + * bot.lazy(ctx => createMyMiddleware(ctx)) + * ``` + * + * You may generate this middleware in an `async` fashion. + * + * You can decide to return an empty array (`[]`) if you don't want to run + * any middleware for a given context object. This is equivalent to + * returning an empty instance of `Composer`. + * + * @param middlewareFactory The factory function creating the middleware + */ + lazy( + middlewareFactory: (ctx: C) => MaybePromise>>, + ): Composer { + return this.use(async (ctx, next) => { + const middleware = await middlewareFactory(ctx); + const arr = Array.isArray(middleware) ? middleware : [middleware]; + await flatten(new Composer(...arr))(ctx, next); + }); + } + + /** + * > This is an advanced method of grammY. + * + * _Not to be confused with the `router` plugin._ + * + * This method is an alternative to the `router` plugin. It allows you to + * branch between different middleware per context object. You can pass two + * things to it: + * 1. A routing function + * 2. Different middleware identified by key + * + * The routing function decides based on the context object which middleware + * to run. Each middleware is identified by a key, so the routing function + * simply returns the key of that middleware. + * ```ts + * // Define different route handlers + * const routeHandlers = { + * evenUpdates: (ctx: Context) => { ... } + * oddUpdates: (ctx: Context) => { ... } + * } + * // Decide for a context object which one to pick + * const router = (ctx: Context) => ctx.update.update_id % 2 === 0 + * ? 'evenUpdates' + * : 'oddUpdates' + * // Route it! + * bot.route(router, routeHandlers) + * ``` + * + * Optionally, you can pass a third option that is used as fallback + * middleware if your route function returns `undefined`, or if the key + * returned by your router has no middleware associated with it. + * + * This method may need less setup than first instantiating a `Router`, but + * for more complex setups, having a `Router` may be more readable. + * + * @param router The routing function to use + * @param routeHandlers Handlers for every route + * @param fallback Optional fallback middleware if no route matches + */ + route>>( + router: (ctx: C) => MaybePromise, + routeHandlers: R, + fallback: Middleware = pass, + ): Composer { + return this.lazy(async (ctx) => { + const route = await router(ctx); + return (route === undefined || !routeHandlers[route] + ? fallback + : routeHandlers[route]) ?? []; + }); + } + + /** + * > This is an advanced method of grammY. + * + * Allows you to branch between two cases for a given context object. + * + * This method takes a predicate function that is tested once per context + * object. If it returns `true`, the first supplied middleware is executed. + * If it returns `false`, the second supplied middleware is executed. Note + * that the predicate may be asynchronous, i.e. it can return a Promise of a + * boolean. + * + * @param predicate The predicate to check + * @param trueMiddleware The middleware for the `true` case + * @param falseMiddleware The middleware for the `false` case + */ + branch( + predicate: (ctx: C) => MaybePromise, + trueMiddleware: MaybeArray>, + falseMiddleware: MaybeArray>, + ) { + return this.lazy(async (ctx) => + (await predicate(ctx)) ? trueMiddleware : falseMiddleware + ); + } + + /** + * > This is an advanced function of grammY. + * + * Installs an error boundary that catches errors that happen only inside + * the given middleware. This allows you to install custom error handlers + * that protect some parts of your bot. Errors will not be able to bubble + * out of this part of your middleware system, unless the supplied error + * handler rethrows them, in which case the next surrounding error boundary + * will catch the error. + * + * Example usage: + * ```ts + * function errHandler(err: BotError) { + * console.error('Error boundary caught error!', err) + * } + * + * const safe = + * // All passed middleware will be protected by the error boundary. + * bot.errorBoundary(errHandler, middleware0, middleware1, middleware2) + * + * // Those will also be protected! + * safe.on('message', middleware3) + * + * // No error from `middleware4` will reach the `errHandler` from above, + * // as errors are suppressed. + * + * // do nothing on error (suppress error), and run outside middleware + * const suppress = (_err: BotError, next: NextFunction) => { return next() } + * safe.errorBoundary(suppress).on('edited_message', middleware4) + * ``` + * + * Check out the + * [documentation](https://grammy.dev/guide/errors#error-boundaries) on + * the website to learn more about error boundaries. + * + * @param errorHandler The error handler to use + * @param middleware The middleware to protect + */ + errorBoundary( + errorHandler: ( + error: BotError, + next: NextFunction, + ) => MaybePromise, + ...middleware: Array> + ) { + const composer = new Composer(...middleware); + const bound = flatten(composer); + this.use(async (ctx, next) => { + let nextCalled = false; + const cont = () => ((nextCalled = true), Promise.resolve()); + try { + await bound(ctx, cont); + } catch (err) { + nextCalled = false; + await errorHandler(new BotError(err, ctx), cont); + } + if (nextCalled) await next(); + }); + return composer; + } +} + +// === Filtered context middleware types +/** + * Type of the middleware that can be passed to `bot.hears`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.hears` in a different place. For instance, this allows for more modular + * code where handlers are defined in separate files. + */ +export type HearsMiddleware = Middleware< + HearsContext +>; +/** + * Type of the middleware that can be passed to `bot.command`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.command` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type CommandMiddleware = Middleware< + CommandContext +>; +/** + * Type of the middleware that can be passed to `bot.reaction`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.reaction` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type ReactionMiddleware = Middleware< + ReactionContext +>; +/** + * Type of the middleware that can be passed to `bot.callbackQuery`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.callbackQuery` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type CallbackQueryMiddleware = Middleware< + CallbackQueryContext +>; +/** + * Type of the middleware that can be passed to `bot.gameQuery`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.gameQuery` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type GameQueryMiddleware = Middleware< + GameQueryContext +>; +/** + * Type of the middleware that can be passed to `bot.inlineQuery`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.inlineQuery` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type InlineQueryMiddleware = Middleware< + InlineQueryContext +>; +/** + * Type of the middleware that can be passed to `bot.chosenInlineResult`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.chosenInlineResult` in a different place. For instance, this allows for + * more modular code where handlers are defined in separate files. + */ +export type ChosenInlineResultMiddleware = Middleware< + ChosenInlineResultContext +>; +/** + * Type of the middleware that can be passed to `bot.preCheckoutQuery`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.preCheckoutQuery` in a different place. For instance, this allows for + * more modular code where handlers are defined in separate files. + */ +export type PreCheckoutQueryMiddleware = Middleware< + PreCheckoutQueryContext +>; +/** + * Type of the middleware that can be passed to `bot.shippingQuery`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.shippingQuery` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type ShippingQueryMiddleware = Middleware< + ShippingQueryContext +>; +/** + * Type of the middleware that can be passed to `bot.chatType`. + * + * This helper type can be used to annotate middleware functions that are + * defined in one place, so that they have the correct type when passed to + * `bot.chatType` in a different place. For instance, this allows for more + * modular code where handlers are defined in separate files. + */ +export type ChatTypeMiddleware = + Middleware>; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..afa3bc3 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,115 @@ +import { DEFAULT_UPDATE_TYPES } from "../bot.ts"; +import type { ChatPermissions, Update } from "../types.ts"; + +const ALL_UPDATE_TYPES = [ + ...DEFAULT_UPDATE_TYPES, + "chat_member", + "message_reaction", + "message_reaction_count", +] as const satisfies ReadonlyArray>; +const ALL_CHAT_PERMISSIONS = { + can_send_messages: true, + can_send_audios: true, + can_send_documents: true, + can_send_photos: true, + can_send_videos: true, + can_send_video_notes: true, + can_send_voice_notes: true, + can_send_polls: true, + can_send_other_messages: true, + can_add_web_page_previews: true, + can_react_to_messages: true, + can_change_info: true, + can_invite_users: true, + can_edit_tag: true, + can_pin_messages: true, + can_manage_topics: true, +} as const satisfies ChatPermissions; + +/** + * Types of the constants used in the Telegram Bot API. Currently holds all + * available update types as well as all chat permissions. + */ +export interface ApiConstants { + /** + * List of update types a bot receives by default. Useful if you want to + * receive all update types but `chat_member`, `message_reaction`, and + * `message_reaction_count`. + * + * ```ts + * // Built-in polling: + * bot.start({ allowed_updates: DEFAULT_UPDATE_TYPES }); + * // grammY runner: + * run(bot, { runner: { fetch: { allowed_updates: DEFAULT_UPDATE_TYPES } } }); + * // Webhooks: + * await bot.api.setWebhook(url, { allowed_updates: DEFAULT_UPDATE_TYPES }); + * ``` + * + * See the [Bot API reference](https://core.telegram.org/bots/api#update) + * for more information. + */ + DEFAULT_UPDATE_TYPES: typeof DEFAULT_UPDATE_TYPES[number]; + + /** + * List of all available update types. Useful if you want to receive all + * updates from the Bot API, rather than just those that are delivered by + * default. + * + * The main use case for this is when you want to receive `chat_member`, + * `message_reaction`, and `message_reaction_count` updates, as they need to + * be enabled first. Use it like so: + * + * ```ts + * // Built-in polling: + * bot.start({ allowed_updates: ALL_UPDATE_TYPES }); + * // grammY runner: + * run(bot, { runner: { fetch: { allowed_updates: ALL_UPDATE_TYPES } } }); + * // Webhooks: + * await bot.api.setWebhook(url, { allowed_updates: ALL_UPDATE_TYPES }); + * ``` + * + * See the [Bot API reference](https://core.telegram.org/bots/api#update) + * for more information. + */ + ALL_UPDATE_TYPES: typeof ALL_UPDATE_TYPES[number]; + + /** + * An object containing all available chat permissions. Useful if you want + * to lift restrictions from a user, as this action requires you to pass + * `true` for all permissions. Use it like so: + * + * ```ts + * // On `Bot`: + * await bot.api.restrictChatMember(chat_id, user_id, ALL_CHAT_PERMISSIONS); + * // On `Api`: + * await ctx.api.restrictChatMember(chat_id, user_id, ALL_CHAT_PERMISSIONS); + * // On `Context`: + * await ctx.restrictChatMember(user_id, ALL_CHAT_PERMISSIONS); + * await ctx.restrictAuthor(ALL_CHAT_PERMISSIONS); + * ``` + * + * See the [Bot API reference](https://core.telegram.org/bots/api#chatpermissions) + * for more information. + */ + ALL_CHAT_PERMISSIONS: keyof typeof ALL_CHAT_PERMISSIONS; +} + +interface TypeOf { + DEFAULT_UPDATE_TYPES: typeof DEFAULT_UPDATE_TYPES; + ALL_UPDATE_TYPES: typeof ALL_UPDATE_TYPES; + ALL_CHAT_PERMISSIONS: typeof ALL_CHAT_PERMISSIONS; +} +type ValuesFor = { + [K in keyof T]: K extends keyof TypeOf ? Readonly : never; +}; + +/** + * A container for constants used in the Telegram Bot API. Currently holds all + * available update types as well as all chat permissions. + */ +export const API_CONSTANTS: ValuesFor = { + DEFAULT_UPDATE_TYPES, + ALL_UPDATE_TYPES, + ALL_CHAT_PERMISSIONS, +}; +Object.freeze(API_CONSTANTS); diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..106edcf --- /dev/null +++ b/src/context.ts @@ -0,0 +1,4825 @@ +// deno-lint-ignore-file camelcase +import { type Api, type Other as OtherApi } from "./core/api.ts"; +import { type Methods, type RawApi } from "./core/client.ts"; +import { + type Filter, + type FilterCore, + type FilterQuery, + matchFilter, +} from "./filter.ts"; +import { + type AcceptedGiftTypes, + type Chat, + type ChatPermissions, + type InlineQueryResult, + type InputChecklist, + type InputFile, + type InputMedia, + type InputMediaAudio, + type InputMediaDocument, + type InputMediaLivePhoto, + type InputMediaPhoto, + type InputMediaVideo, + type InputMediaWithoutUpload, + type InputPaidMedia, + type InputPollOption, + type InputProfilePhoto, + type InputRichMessage, + type InputRichMessageWithoutUpload, + type InputStoryContent, + type KeyboardButton, + type LabeledPrice, + type Message, + type MessageEntity, + type PassportElementError, + type ReactionType, + type ReactionTypeEmoji, + type Update, + type User, + type UserFromGetMe, +} from "./types.ts"; + +// === Util types +export type MaybeArray = T | T[]; +/** permits `string` but gives hints */ +export type StringWithCommandSuggestions = + | (string & Record) + | "start" + | "help" + | "settings" + | "privacy" + | "developer_info"; + +type Other, X extends string = never> = OtherApi< + RawApi, + M, + X +>; +type SnakeToCamelCase = S extends `${infer L}_${infer R}` + ? `${L}${Capitalize>}` + : S; +type AliasProps = { + [K in string & keyof U as SnakeToCamelCase]: U[K]; +}; +type RenamedUpdate = AliasProps>; + +// === Context probing logic +interface StaticHas { + /** + * Generates a predicate function that can test context objects for matching + * the given filter query. This uses the same logic as `bot.on`. + * + * @param filter The filter query to check + */ + filterQuery( + filter: Q | Q[], + ): (ctx: C) => ctx is Filter; + /** + * Generates a predicate function that can test context objects for + * containing the given text, or for the text to match the given regular + * expression. This uses the same logic as `bot.hears`. + * + * @param trigger The string or regex to match + */ + text( + trigger: MaybeArray, + ): (ctx: C) => ctx is HearsContext; + /** + * Generates a predicate function that can test context objects for + * containing a command. This uses the same logic as `bot.command`. + * + * @param command The command to match + */ + command( + command: MaybeArray, + ): (ctx: C) => ctx is CommandContext; + /** + * Generates a predicate function that can test context objects for + * containing a message reaction update. This uses the same logic as + * `bot.reaction`. + * + * @param reaction The reaction to test against + */ + reaction( + reaction: MaybeArray, + ): (ctx: C) => ctx is ReactionContext; + /** + * Generates a predicate function that can test context objects for + * belonging to a chat with the given chat type. This uses the same logic as + * `bot.chatType`. + * + * @param chatType The chat type to match + */ + chatType( + chatType: MaybeArray, + ): (ctx: C) => ctx is ChatTypeContext; + /** + * Generates a predicate function that can test context objects for + * containing the given callback query, or for the callback query data to + * match the given regular expression. This uses the same logic as + * `bot.callbackQuery`. + * + * @param trigger The string or regex to match + */ + callbackQuery( + trigger: MaybeArray, + ): (ctx: C) => ctx is CallbackQueryContext; + /** + * Generates a predicate function that can test context objects for + * containing the given game query, or for the game name to match the given + * regular expression. This uses the same logic as `bot.gameQuery`. + * + * @param trigger The string or regex to match + */ + gameQuery( + trigger: MaybeArray, + ): (ctx: C) => ctx is GameQueryContext; + /** + * Generates a predicate function that can test context objects for + * containing the given inline query, or for the inline query to match the + * given regular expression. This uses the same logic as `bot.inlineQuery`. + * + * @param trigger The string or regex to match + */ + inlineQuery( + trigger: MaybeArray, + ): (ctx: C) => ctx is InlineQueryContext; + /** + * Generates a predicate function that can test context objects for + * containing the chosen inline result, or for the chosen inline result to + * match the given regular expression. + * + * @param trigger The string or regex to match + */ + chosenInlineResult( + trigger: MaybeArray, + ): (ctx: C) => ctx is ChosenInlineResultContext; + /** + * Generates a predicate function that can test context objects for + * containing the given pre-checkout query, or for the pre-checkout query + * payload to match the given regular expression. This uses the same logic + * as `bot.preCheckoutQuery`. + * + * @param trigger The string or regex to match + */ + preCheckoutQuery( + trigger: MaybeArray, + ): (ctx: C) => ctx is PreCheckoutQueryContext; + /** + * Generates a predicate function that can test context objects for + * containing the given shipping query, or for the shipping query to match + * the given regular expression. This uses the same logic as + * `bot.shippingQuery`. + * + * @param trigger The string or regex to match + */ + shippingQuery( + trigger: MaybeArray, + ): (ctx: C) => ctx is ShippingQueryContext; +} +const checker: StaticHas = { + filterQuery(filter: Q | Q[]) { + const pred = matchFilter(filter); + return (ctx: C): ctx is Filter => pred(ctx); + }, + text(trigger) { + const hasText = checker.filterQuery([":text", ":caption"]); + const trg = triggerFn(trigger); + return (ctx: C): ctx is HearsContext => { + if (!hasText(ctx)) return false; + const msg = ctx.message ?? ctx.channelPost; + const txt = msg.text ?? msg.caption; + return match(ctx, txt, trg); + }; + }, + command(command) { + const hasEntities = checker.filterQuery(":entities:bot_command"); + const atCommands = new Set(); + const noAtCommands = new Set(); + toArray(command).forEach((cmd) => { + if (cmd.startsWith("/")) { + throw new Error( + `Do not include '/' when registering command handlers (use '${ + cmd.substring(1) + }' not '${cmd}')`, + ); + } + const set = cmd.includes("@") ? atCommands : noAtCommands; + set.add(cmd); + }); + return (ctx: C): ctx is CommandContext => { + if (!hasEntities(ctx)) return false; + const msg = ctx.message ?? ctx.channelPost; + const txt = msg.text ?? msg.caption; + return msg.entities.some((e) => { + if (e.type !== "bot_command") return false; + if (e.offset !== 0) return false; + const cmd = txt.substring(1, e.length); + if (noAtCommands.has(cmd) || atCommands.has(cmd)) { + ctx.match = txt.substring(cmd.length + 1).trimStart(); + return true; + } + const index = cmd.indexOf("@"); + if (index === -1) return false; + const atTarget = cmd.substring(index + 1).toLowerCase(); + const username = ctx.me.username.toLowerCase(); + if (atTarget !== username) return false; + const atCommand = cmd.substring(0, index); + if (noAtCommands.has(atCommand)) { + ctx.match = txt.substring(cmd.length + 1).trimStart(); + return true; + } + return false; + }); + }; + }, + reaction(reaction) { + const hasMessageReaction = checker.filterQuery("message_reaction"); + const normalized: ReactionType[] = typeof reaction === "string" + ? [{ type: "emoji", emoji: reaction }] + : (Array.isArray(reaction) ? reaction : [reaction]).map((emoji) => + typeof emoji === "string" ? { type: "emoji", emoji } : emoji + ); + const emoji = new Set( + normalized.filter((r) => r.type === "emoji") + .map((r) => r.emoji), + ); + const customEmoji = new Set( + normalized.filter((r) => r.type === "custom_emoji") + .map((r) => r.custom_emoji_id), + ); + const paid = normalized.some((r) => r.type === "paid"); + return (ctx: C): ctx is ReactionContext => { + if (!hasMessageReaction(ctx)) return false; + const { old_reaction, new_reaction } = ctx.messageReaction; + // try to find a wanted reaction that is new and not old + for (const reaction of new_reaction) { + // first check if the reaction existed previously + let isOld = false; + if (reaction.type === "emoji") { + for (const old of old_reaction) { + if (old.type !== "emoji") continue; + if (old.emoji === reaction.emoji) { + isOld = true; + break; + } + } + } else if (reaction.type === "custom_emoji") { + for (const old of old_reaction) { + if (old.type !== "custom_emoji") continue; + if (old.custom_emoji_id === reaction.custom_emoji_id) { + isOld = true; + break; + } + } + } else if (reaction.type === "paid") { + for (const old of old_reaction) { + if (old.type !== "paid") continue; + isOld = true; + break; + } + } else { + // always regard unsupported emoji types as new + } + // disregard reaction if it is not new + if (isOld) continue; + // check if the new reaction is wanted and short-circuit + if (reaction.type === "emoji") { + if (emoji.has(reaction.emoji)) return true; + } else if (reaction.type === "custom_emoji") { + if (customEmoji.has(reaction.custom_emoji_id)) return true; + } else if (reaction.type === "paid") { + if (paid) return true; + } else { + // always regard unsupported emoji types as new + return true; + } + // new reaction not wanted, check next one + } + return false; + }; + }, + chatType(chatType: MaybeArray) { + const set = new Set(toArray(chatType)); + return (ctx: C): ctx is ChatTypeContext => + ctx.chat?.type !== undefined && set.has(ctx.chat.type); + }, + callbackQuery(trigger) { + const hasCallbackQuery = checker.filterQuery("callback_query:data"); + const trg = triggerFn(trigger); + return (ctx: C): ctx is CallbackQueryContext => + hasCallbackQuery(ctx) && match(ctx, ctx.callbackQuery.data, trg); + }, + gameQuery(trigger) { + const hasGameQuery = checker.filterQuery( + "callback_query:game_short_name", + ); + const trg = triggerFn(trigger); + return (ctx: C): ctx is GameQueryContext => + hasGameQuery(ctx) && + match(ctx, ctx.callbackQuery.game_short_name, trg); + }, + inlineQuery(trigger) { + const hasInlineQuery = checker.filterQuery("inline_query"); + const trg = triggerFn(trigger); + return (ctx: C): ctx is InlineQueryContext => + hasInlineQuery(ctx) && match(ctx, ctx.inlineQuery.query, trg); + }, + chosenInlineResult(trigger) { + const hasChosenInlineResult = checker.filterQuery( + "chosen_inline_result", + ); + const trg = triggerFn(trigger); + return ( + ctx: C, + ): ctx is ChosenInlineResultContext => + hasChosenInlineResult(ctx) && + match(ctx, ctx.chosenInlineResult.result_id, trg); + }, + preCheckoutQuery(trigger) { + const hasPreCheckoutQuery = checker.filterQuery("pre_checkout_query"); + const trg = triggerFn(trigger); + return (ctx: C): ctx is PreCheckoutQueryContext => + hasPreCheckoutQuery(ctx) && + match(ctx, ctx.preCheckoutQuery.invoice_payload, trg); + }, + shippingQuery(trigger) { + const hasShippingQuery = checker.filterQuery("shipping_query"); + const trg = triggerFn(trigger); + return (ctx: C): ctx is ShippingQueryContext => + hasShippingQuery(ctx) && + match(ctx, ctx.shippingQuery.invoice_payload, trg); + }, +}; + +// === Context class +/** + * When your bot receives a message, Telegram sends an update object to your + * bot. The update contains information about the chat, the user, and of course + * the message itself. There are numerous other updates, too: + * https://core.telegram.org/bots/api#update + * + * When grammY receives an update, it wraps this update into a context object + * for you. Context objects are commonly named `ctx`. A context object does two + * things: + * 1. **`ctx.update`** holds the update object that you can use to process the + * message. This includes providing useful shortcuts for the update, for + * instance, `ctx.msg` is a shortcut that gives you the message object from + * the updateβ€”no matter whether it is contained in `ctx.update.message`, or + * `ctx.update.edited_message`, or `ctx.update.channel_post`, or + * `ctx.update.edited_channel_post`. + * 2. **`ctx.api`** gives you access to the full Telegram Bot API so that you + * can directly call any method, such as responding via + * `ctx.api.sendMessage`. Also here, the context objects has some useful + * shortcuts for you. For instance, if you want to send a message to the same + * chat that a message comes from (i.e. just respond to a user) you can call + * `ctx.reply`. This is nothing but a wrapper for `ctx.api.sendMessage` with + * the right `chat_id` pre-filled for you. Almost all methods of the Telegram + * Bot API have their own shortcut directly on the context object, so you + * probably never really have to use `ctx.api` at all. + * + * This context object is then passed to all of the listeners (called + * middleware) that you register on your bot. Because this is so useful, the + * context object is often used to hold more information. One example are + * sessions (a chat-specific data storage that is stored in a database), and + * another example is `ctx.match` that is used by `bot.command` and other + * methods to keep information about how a regular expression was matched. + * + * Read up about middleware on the + * [website](https://grammy.dev/guide/context) if you want to know more + * about the powerful opportunities that lie in context objects, and about how + * grammY implements them. + */ +export class Context implements RenamedUpdate { + /** + * Used by some middleware to store information about how a certain string + * or regular expression was matched. + */ + public match: string | RegExpMatchArray | undefined; + + constructor( + /** + * The update object that is contained in the context. + */ + public readonly update: Update, + /** + * An API instance that allows you to call any method of the Telegram + * Bot API. + */ + public readonly api: Api, + /** + * Information about the bot itself. + */ + public readonly me: UserFromGetMe, + ) {} + + // UPDATE SHORTCUTS + + // Keep in sync with types in `filter.ts`. + /** Alias for `ctx.update.message` */ + get message() { + return this.update.message; + } + /** Alias for `ctx.update.edited_message` */ + get editedMessage() { + return this.update.edited_message; + } + /** Alias for `ctx.update.channel_post` */ + get channelPost() { + return this.update.channel_post; + } + /** Alias for `ctx.update.edited_channel_post` */ + get editedChannelPost() { + return this.update.edited_channel_post; + } + /** Alias for `ctx.update.business_connection` */ + get businessConnection() { + return this.update.business_connection; + } + /** Alias for `ctx.update.business_message` */ + get businessMessage() { + return this.update.business_message; + } + /** Alias for `ctx.update.edited_business_message` */ + get editedBusinessMessage() { + return this.update.edited_business_message; + } + /** Alias for `ctx.update.deleted_business_messages` */ + get deletedBusinessMessages() { + return this.update.deleted_business_messages; + } + /** Alias for `ctx.update.guest_message` */ + get guestMessage() { + return this.update.guest_message; + } + /** Alias for `ctx.update.message_reaction` */ + get messageReaction() { + return this.update.message_reaction; + } + /** Alias for `ctx.update.message_reaction_count` */ + get messageReactionCount() { + return this.update.message_reaction_count; + } + /** Alias for `ctx.update.inline_query` */ + get inlineQuery() { + return this.update.inline_query; + } + /** Alias for `ctx.update.chosen_inline_result` */ + get chosenInlineResult() { + return this.update.chosen_inline_result; + } + /** Alias for `ctx.update.callback_query` */ + get callbackQuery() { + return this.update.callback_query; + } + /** Alias for `ctx.update.shipping_query` */ + get shippingQuery() { + return this.update.shipping_query; + } + /** Alias for `ctx.update.pre_checkout_query` */ + get preCheckoutQuery() { + return this.update.pre_checkout_query; + } + /** Alias for `ctx.update.poll` */ + get poll() { + return this.update.poll; + } + /** Alias for `ctx.update.poll_answer` */ + get pollAnswer() { + return this.update.poll_answer; + } + /** Alias for `ctx.update.my_chat_member` */ + get myChatMember() { + return this.update.my_chat_member; + } + /** Alias for `ctx.update.chat_member` */ + get chatMember() { + return this.update.chat_member; + } + /** Alias for `ctx.update.managed_bot` */ + get managedBot() { + return this.update.managed_bot; + } + /** Alias for `ctx.update.chat_join_request` */ + get chatJoinRequest() { + return this.update.chat_join_request; + } + /** Alias for `ctx.update.chat_boost` */ + get chatBoost() { + return this.update.chat_boost; + } + /** Alias for `ctx.update.removed_chat_boost` */ + get removedChatBoost() { + return this.update.removed_chat_boost; + } + /** Alias for `ctx.update.purchased_paid_media` */ + get purchasedPaidMedia() { + return this.update.purchased_paid_media; + } + /** Alias for `ctx.update.subscription` */ + get subscription() { + return this.update.subscription; + } + + // AGGREGATION SHORTCUTS + + /** + * Get the message object from wherever possible. Alias for `this.message ?? + * this.editedMessage ?? this.channelPost ?? this.editedChannelPost ?? + * this.businessMessage ?? this.editedBusinessMessage ?? + * this.callbackQuery?.message`. + */ + get msg(): Message | undefined { + // Keep in sync with types in `filter.ts`. + return ( + this.message ?? + this.editedMessage ?? + this.channelPost ?? + this.editedChannelPost ?? + this.businessMessage ?? + this.editedBusinessMessage ?? + this.guestMessage ?? + this.callbackQuery?.message + ); + } + /** + * Get the chat object from wherever possible. Alias for `(this.msg ?? + * this.deletedBusinessMessages ?? this.messageReaction ?? + * this.messageReactionCount ?? this.myChatMember ?? this.chatMember ?? + * this.chatJoinRequest ?? this.chatBoost ?? this.removedChatBoost)?.chat`. + */ + get chat(): Chat | undefined { + // Keep in sync with types in `filter.ts`. + return ( + this.msg ?? + this.deletedBusinessMessages ?? + this.messageReaction ?? + this.messageReactionCount ?? + this.myChatMember ?? + this.chatMember ?? + this.chatJoinRequest ?? + this.chatBoost ?? + this.removedChatBoost + )?.chat; + } + /** + * Get the sender chat object from wherever possible. Alias for + * `ctx.msg?.sender_chat`. + */ + get senderChat(): Chat | undefined { + // Keep in sync with types in `filter.ts`. + return this.msg?.sender_chat; + } + /** + * Get the user object from wherever possible. Alias for + * `(this.businessConnection ?? this.messageReaction ?? this.managedBot ?? + * (this.chatBoost?.boost ?? this.removedChatBoost)?.source)?.user ?? + * (this.callbackQuery ?? this.msg ?? this.inlineQuery ?? + * this.chosenInlineResult ?? this.shippingQuery ?? this.preCheckoutQuery ?? + * this.myChatMember ?? this.chatMember ?? this.chatJoinRequest ?? + * this.purchasedPaidMedia)?.from`. + */ + get from(): User | undefined { + // Keep in sync with types in `filter.ts`. + return ( + this.businessConnection ?? + this.messageReaction ?? + this.managedBot ?? + (this.chatBoost?.boost ?? this.removedChatBoost)?.source ?? + this.subscription + )?.user ?? + ( + this.callbackQuery ?? + this.msg ?? + this.inlineQuery ?? + this.chosenInlineResult ?? + this.shippingQuery ?? + this.preCheckoutQuery ?? + this.myChatMember ?? + this.chatMember ?? + this.chatJoinRequest ?? + this.purchasedPaidMedia + )?.from; + } + + /** + * Get the message identifier from wherever possible. Alias for + * `this.msg?.message_id ?? this.messageReaction?.message_id ?? + * this.messageReactionCount?.message_id`. + */ + get msgId(): number | undefined { + // Keep in sync with types in `filter.ts`. + return this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id; + } + /** + * Gets the chat identifier from wherever possible. Alias for `this.chat?.id + * ?? this.businessConnection?.user_chat_id`. + */ + get chatId(): number | undefined { + // Keep in sync with types in `filter.ts`. + return this.chat?.id ?? this.businessConnection?.user_chat_id; + } + /** + * Get the inline message identifier from wherever possible. Alias for + * `(ctx.callbackQuery ?? ctx.chosenInlineResult)?.inline_message_id`. + */ + get inlineMessageId(): string | undefined { + return ( + this.callbackQuery?.inline_message_id ?? + this.chosenInlineResult?.inline_message_id + ); + } + /** + * Get the business connection identifier from wherever possible. Alias for + * `this.msg?.business_connection_id ?? this.businessConnection?.id ?? + * this.deletedBusinessMessages?.business_connection_id`. + */ + get businessConnectionId(): string | undefined { + return this.msg?.business_connection_id ?? + this.businessConnection?.id ?? + this.deletedBusinessMessages?.business_connection_id; + } + /** + * Get entities and their text. Extracts the text from `ctx.msg.text` or + * `ctx.msg.caption`. Returns an empty array if one of `ctx.msg`, + * `ctx.msg.text` or `ctx.msg.entities` is undefined. + * + * You can filter specific entity types by passing the `types` parameter. + * Example: + * + * ```ts + * ctx.entities() // Returns all entity types + * ctx.entities('url') // Returns only url entities + * ctx.entities(['url', 'email']) // Returns url and email entities + * ``` + * + * @param types Types of entities to return. Omit to get all entities. + * @returns Array of entities and their texts, or empty array when there's no text + */ + entities(): Array< + MessageEntity & { + /** Slice of the message text that contains this entity */ + text: string; + } + >; + entities( + types: MaybeArray, + ): Array< + MessageEntity & { + type: T; + /** Slice of the message text that contains this entity */ + text: string; + } + >; + entities(types?: MaybeArray) { + const message = this.msg; + if (message === undefined) return []; + + const text = message.text ?? message.caption; + if (text === undefined) return []; + let entities = message.entities ?? message.caption_entities; + if (entities === undefined) return []; + if (types !== undefined) { + const filters = new Set(toArray(types)); + entities = entities.filter((entity) => filters.has(entity.type)); + } + + return entities.map((entity) => ({ + ...entity, + text: text.substring(entity.offset, entity.offset + entity.length), + })); + } + /** + * Find out which reactions were added and removed in a `message_reaction` + * update. This method looks at `ctx.messageReaction` and computes the + * difference between the old reaction and the new reaction. It also groups + * the reactions by emoji reactions and custom emoji reactions. For example, + * the resulting object could look like this: + * ```ts + * { + * emoji: ['πŸ‘', 'πŸŽ‰'] + * emojiAdded: ['πŸŽ‰'], + * emojiKept: ['πŸ‘'], + * emojiRemoved: [], + * customEmoji: [], + * customEmojiAdded: [], + * customEmojiKept: [], + * customEmojiRemoved: ['id0123'], + * paid: true, + * paidAdded: false, + * paidRemoved: false, + * } + * ``` + * In the above example, a tada reaction was added by the user, and a custom + * emoji reaction with the custom emoji 'id0123' was removed in the same + * update. The user had already reacted with a thumbs up reaction and a paid + * star reaction, which they left both unchanged. As a result, the current + * reaction by the user is thumbs up, tada, and a paid reaction. Note that + * the current reaction (all emoji reactions regardless of type in one list) + * can also be obtained from `ctx.messageReaction.new_reaction`. + * + * Remember that reaction updates only include information about the + * reaction of a specific user. The respective message may have many more + * reactions by other people which will not be included in this update. + * + * @returns An object containing information about the reaction update + */ + reactions(): { + /** Emoji currently present in this user's reaction */ + emoji: ReactionTypeEmoji["emoji"][]; + /** Emoji newly added to this user's reaction */ + emojiAdded: ReactionTypeEmoji["emoji"][]; + /** Emoji not changed by the update to this user's reaction */ + emojiKept: ReactionTypeEmoji["emoji"][]; + /** Emoji removed from this user's reaction */ + emojiRemoved: ReactionTypeEmoji["emoji"][]; + /** Custom emoji currently present in this user's reaction */ + customEmoji: string[]; + /** Custom emoji newly added to this user's reaction */ + customEmojiAdded: string[]; + /** Custom emoji not changed by the update to this user's reaction */ + customEmojiKept: string[]; + /** Custom emoji removed from this user's reaction */ + customEmojiRemoved: string[]; + /** + * `true` if a paid reaction is currently present in this user's + * reaction, and `false` otherwise + */ + paid: boolean; + /** + * `true` if a paid reaction was newly added to this user's reaction, + * and `false` otherwise + */ + paidAdded: boolean; + } { + const emoji: ReactionTypeEmoji["emoji"][] = []; + const emojiAdded: ReactionTypeEmoji["emoji"][] = []; + const emojiKept: ReactionTypeEmoji["emoji"][] = []; + const emojiRemoved: ReactionTypeEmoji["emoji"][] = []; + const customEmoji: string[] = []; + const customEmojiAdded: string[] = []; + const customEmojiKept: string[] = []; + const customEmojiRemoved: string[] = []; + let paid = false; + let paidAdded = false; + const r = this.messageReaction; + if (r !== undefined) { + const { old_reaction, new_reaction } = r; + // group all current emoji in `emoji` and `customEmoji` + for (const reaction of new_reaction) { + if (reaction.type === "emoji") { + emoji.push(reaction.emoji); + } else if (reaction.type === "custom_emoji") { + customEmoji.push(reaction.custom_emoji_id); + } else if (reaction.type === "paid") { + paid = paidAdded = true; + } + } + // temporarily move all old emoji to the *Removed arrays + for (const reaction of old_reaction) { + if (reaction.type === "emoji") { + emojiRemoved.push(reaction.emoji); + } else if (reaction.type === "custom_emoji") { + customEmojiRemoved.push(reaction.custom_emoji_id); + } else if (reaction.type === "paid") { + paidAdded = false; + } + } + // temporarily move all new emoji to the *Added arrays + emojiAdded.push(...emoji); + customEmojiAdded.push(...customEmoji); + // drop common emoji from both lists and add them to `emojiKept` + for (let i = 0; i < emojiRemoved.length; i++) { + const len = emojiAdded.length; + if (len === 0) break; + const rem = emojiRemoved[i]; + for (let j = 0; j < len; j++) { + if (rem === emojiAdded[j]) { + emojiKept.push(rem); + emojiRemoved.splice(i, 1); + emojiAdded.splice(j, 1); + i--; + break; + } + } + } + // drop common custom emoji from both lists and add them to `customEmojiKept` + for (let i = 0; i < customEmojiRemoved.length; i++) { + const len = customEmojiAdded.length; + if (len === 0) break; + const rem = customEmojiRemoved[i]; + for (let j = 0; j < len; j++) { + if (rem === customEmojiAdded[j]) { + customEmojiKept.push(rem); + customEmojiRemoved.splice(i, 1); + customEmojiAdded.splice(j, 1); + i--; + break; + } + } + } + } + return { + emoji, + emojiAdded, + emojiKept, + emojiRemoved, + customEmoji, + customEmojiAdded, + customEmojiKept, + customEmojiRemoved, + paid, + paidAdded, + }; + } + + // PROBING SHORTCUTS + + /** + * `Context.has` is an object that contains a number of useful functions for + * probing context objects. Each of these functions can generate a predicate + * function, to which you can pass context objects in order to check if a + * condition holds for the respective context object. + * + * For example, you can call `Context.has.filterQuery(":text")` to generate + * a predicate function that tests context objects for containing text: + * ```ts + * const hasText = Context.has.filterQuery(":text"); + * + * if (hasText(ctx0)) {} // `ctx0` matches the filter query `:text` + * if (hasText(ctx1)) {} // `ctx1` matches the filter query `:text` + * if (hasText(ctx2)) {} // `ctx2` matches the filter query `:text` + * ``` + * These predicate functions are used internally by the has-methods that are + * installed on every context object. This means that calling + * `ctx.has(":text")` is equivalent to + * `Context.has.filterQuery(":text")(ctx)`. + */ + static has = checker; + /** + * Returns `true` if this context object matches the given filter query, and + * `false` otherwise. This uses the same logic as `bot.on`. + * + * @param filter The filter query to check + */ + has(filter: Q | Q[]): this is FilterCore { + return Context.has.filterQuery(filter)(this); + } + /** + * Returns `true` if this context object contains the given text, or if it + * contains text that matches the given regular expression. It returns + * `false` otherwise. This uses the same logic as `bot.hears`. + * + * @param trigger The string or regex to match + */ + hasText(trigger: MaybeArray): this is HearsContextCore { + return Context.has.text(trigger)(this); + } + /** + * Returns `true` if this context object contains the given command, and + * `false` otherwise. This uses the same logic as `bot.command`. + * + * @param command The command to match + */ + hasCommand( + command: MaybeArray, + ): this is CommandContextCore { + return Context.has.command(command)(this); + } + hasReaction( + reaction: MaybeArray, + ): this is ReactionContextCore { + return Context.has.reaction(reaction)(this); + } + /** + * Returns `true` if this context object belongs to a chat with the given + * chat type, and `false` otherwise. This uses the same logic as + * `bot.chatType`. + * + * @param chatType The chat type to match + */ + hasChatType( + chatType: MaybeArray, + ): this is ChatTypeContextCore { + return Context.has.chatType(chatType)(this); + } + /** + * Returns `true` if this context object contains the given callback query, + * or if the contained callback query data matches the given regular + * expression. It returns `false` otherwise. This uses the same logic as + * `bot.callbackQuery`. + * + * @param trigger The string or regex to match + */ + hasCallbackQuery( + trigger: MaybeArray, + ): this is CallbackQueryContextCore { + return Context.has.callbackQuery(trigger)(this); + } + /** + * Returns `true` if this context object contains the given game query, or + * if the contained game query matches the given regular expression. It + * returns `false` otherwise. This uses the same logic as `bot.gameQuery`. + * + * @param trigger The string or regex to match + */ + hasGameQuery( + trigger: MaybeArray, + ): this is GameQueryContextCore { + return Context.has.gameQuery(trigger)(this); + } + /** + * Returns `true` if this context object contains the given inline query, or + * if the contained inline query matches the given regular expression. It + * returns `false` otherwise. This uses the same logic as `bot.inlineQuery`. + * + * @param trigger The string or regex to match + */ + hasInlineQuery( + trigger: MaybeArray, + ): this is InlineQueryContextCore { + return Context.has.inlineQuery(trigger)(this); + } + /** + * Returns `true` if this context object contains the chosen inline result, + * or if the contained chosen inline result matches the given regular + * expression. It returns `false` otherwise. This uses the same logic as + * `bot.chosenInlineResult`. + * + * @param trigger The string or regex to match + */ + hasChosenInlineResult( + trigger: MaybeArray, + ): this is ChosenInlineResultContextCore { + return Context.has.chosenInlineResult(trigger)(this); + } + /** + * Returns `true` if this context object contains the given pre-checkout + * query, or if the contained pre-checkout query matches the given regular + * expression. It returns `false` otherwise. This uses the same logic as + * `bot.preCheckoutQuery`. + * + * @param trigger The string or regex to match + */ + hasPreCheckoutQuery( + trigger: MaybeArray, + ): this is PreCheckoutQueryContextCore { + return Context.has.preCheckoutQuery(trigger)(this); + } + /** + * Returns `true` if this context object contains the given shipping query, + * or if the contained shipping query matches the given regular expression. + * It returns `false` otherwise. This uses the same logic as + * `bot.shippingQuery`. + * + * @param trigger The string or regex to match + */ + hasShippingQuery( + trigger: MaybeArray, + ): this is ShippingQueryContextCore { + return Context.has.shippingQuery(trigger)(this); + } + + // API + + /** + * Context-aware alias for `api.sendMessage`. Use this method to send text messages. On success, the sent Message is returned. + * + * @param text Text of the message to be sent, 1-4096 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmessage + */ + reply( + text: string, + other?: Other<"sendMessage", "chat_id" | "text">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendMessage( + orThrow(this.chatId, "sendMessage"), + text, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendRichMessage`. Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned. + * + * @param rich_message The message to be sent + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendrichmessage + */ + replyWithRichMessage( + rich_message: InputRichMessage, + other?: Other<"sendRichMessage", "chat_id" | "rich_message">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendRichMessage( + orThrow(this.chatId, "sendRichMessage"), + rich_message, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.forwardMessage`. Use this method to forward messages of any kind. Service messages and messages with protected content can't be forwarded. On success, the sent Message is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#forwardmessage + */ + forwardMessage( + chat_id: number | string, + other?: Other< + "forwardMessage", + "chat_id" | "from_chat_id" | "message_id" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.forwardMessage( + chat_id, + orThrow(this.chatId, "forwardMessage"), + orThrow(this.msgId, "forwardMessage"), + { + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.forwardMessages`. Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an Array of MessageId of the sent messages is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_ids A list of 1-100 identifiers of messages in the current chat to forward. The identifiers must be specified in a strictly increasing order. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#forwardmessages + */ + forwardMessages( + chat_id: number | string, + message_ids: number[], + other?: Other< + "forwardMessages", + "chat_id" | "from_chat_id" | "message_ids" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.forwardMessages( + chat_id, + orThrow(this.chatId, "forwardMessages"), + message_ids, + { + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.copyMessage`. Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#copymessage + */ + copyMessage( + chat_id: number | string, + other?: Other<"copyMessage", "chat_id" | "from_chat_id" | "message_id">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.copyMessage( + chat_id, + orThrow(this.chatId, "copyMessage"), + orThrow(this.msgId, "copyMessage"), + { + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.copyMessages`. Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an Array of MessageId of the sent messages is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target bot, supergroup or channel in the format `@username` + * @param message_ids A list of 1-100 identifiers of messages in the current chat to copy. The identifiers must be specified in a strictly increasing order. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#copymessages + */ + copyMessages( + chat_id: number | string, + message_ids: number[], + other?: Other< + "copyMessages", + "chat_id" | "from_chat_id" | "message_ids" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.copyMessages( + chat_id, + orThrow(this.chatId, "copyMessages"), + message_ids, + { + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendPhoto`. Use this method to send photos. On success, the sent Message is returned. + * + * @param photo Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. The photo must be at most 10 MB in size. The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendphoto + */ + replyWithPhoto( + photo: InputFile | string, + other?: Other<"sendPhoto", "chat_id" | "photo">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendPhoto( + orThrow(this.chatId, "sendPhoto"), + photo, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendLivePhoto`. Use this method to send live photos. On success, the sent Message is returned. + * + * @param live_photo Live photo video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. Sending live photos by a URL is currently unsupported. + * @param photo The static photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. Sending live photos by a URL is currently unsupported. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendlivephoto + */ + replyWithLivePhoto( + live_photo: InputFile | string, + photo: InputFile | string, + other?: Other<"sendLivePhoto", "chat_id" | "live_photo" | "photo">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendLivePhoto( + orThrow(this.chatId, "sendLivePhoto"), + live_photo, + photo, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendAudio`. Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent Message is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. + * + * For sending voice messages, use the sendVoice method instead. + * + * @param audio Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendaudio + */ + replyWithAudio( + audio: InputFile | string, + other?: Other<"sendAudio", "chat_id" | "audio">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendAudio( + orThrow(this.chatId, "sendAudio"), + audio, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendDocument`. Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + * + * @param document File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#senddocument + */ + replyWithDocument( + document: InputFile | string, + other?: Other<"sendDocument", "chat_id" | "document">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendDocument( + orThrow(this.chatId, "sendDocument"), + document, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendVideo`. Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). On success, the sent Message is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. + * + * @param video Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvideo + */ + replyWithVideo( + video: InputFile | string, + other?: Other<"sendVideo", "chat_id" | "video">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendVideo( + orThrow(this.chatId, "sendVideo"), + video, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendAnimation`. Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent Message is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. + * + * @param animation Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendanimation + */ + replyWithAnimation( + animation: InputFile | string, + other?: Other<"sendAnimation", "chat_id" | "animation">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendAnimation( + orThrow(this.chatId, "sendAnimation"), + animation, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendVoice`. Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. + * + * @param voice Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvoice + */ + replyWithVoice( + voice: InputFile | string, + other?: Other<"sendVoice", "chat_id" | "voice">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendVoice( + orThrow(this.chatId, "sendVoice"), + voice, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendVideoNote`. Use this method to send video messages. On success, the sent Message is returned. + * As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + * + * @param video_note Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data.. Sending video notes by a URL is currently unsupported + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvideonote + */ + replyWithVideoNote( + video_note: InputFile | string, + other?: Other<"sendVideoNote", "chat_id" | "video_note">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendVideoNote( + orThrow(this.chatId, "sendVideoNote"), + video_note, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** @deprecated Use `replyWithPaidMedia` instead. */ + sendPaidMedia(...args: Parameters) { + return this.replyWithPaidMedia(...args); + } + + /** + * Context-aware alias for `api.sendPaidMedia`. Use this method to send paid media. On success, the sent Message is returned. + * + * @param star_count The number of Telegram Stars that must be paid to buy access to the media + * @param media An Array describing the media to be sent; up to 10 items + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendpaidmedia + */ + replyWithPaidMedia( + star_count: number, + media: InputPaidMedia[], + other?: Other<"sendPaidMedia", "chat_id" | "star_count" | "media">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendPaidMedia( + orThrow(this.chatId, "sendPaidMedia"), + star_count, + media, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: this.msg?.direct_messages_topic + ?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendMediaGroup`. Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an Array of Message objects that were sent is returned. + * + * @param media An Array describing messages to be sent, must include 2-10 items + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmediagroup + */ + replyWithMediaGroup( + media: + | ReadonlyArray + | ReadonlyArray + | ReadonlyArray< + | InputMediaLivePhoto + | InputMediaPhoto + | InputMediaVideo + >, + other?: Other<"sendMediaGroup", "chat_id" | "media">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendMediaGroup( + orThrow(this.chatId, "sendMediaGroup"), + media, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendLocation`. Use this method to send point on the map. On success, the sent Message is returned. + * + * @param latitude Latitude of the location + * @param longitude Longitude of the location + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendlocation + */ + replyWithLocation( + latitude: number, + longitude: number, + other?: Other<"sendLocation", "chat_id" | "latitude" | "longitude">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendLocation( + orThrow(this.chatId, "sendLocation"), + latitude, + longitude, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.editMessageLiveLocation`. Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to stopMessageLiveLocation. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param latitude Latitude of new location + * @param longitude Longitude of new location + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagelivelocation + */ + editMessageLiveLocation( + latitude: number, + longitude: number, + other?: Other< + "editMessageLiveLocation", + | "chat_id" + | "message_id" + | "inline_message_id" + | "latitude" + | "longitude" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.editMessageLiveLocationInline( + inlineId, + latitude, + longitude, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.editMessageLiveLocation( + orThrow(this.chatId, "editMessageLiveLocation"), + orThrow(this.msgId, "editMessageLiveLocation"), + latitude, + longitude, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.stopMessageLiveLocation`. Use this method to stop updating a live location message before live_period expires. On success, if the message is not an inline message, the edited Message is returned, otherwise True is returned. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#stopmessagelivelocation + */ + stopMessageLiveLocation( + other?: Other< + "stopMessageLiveLocation", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.stopMessageLiveLocationInline( + inlineId, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.stopMessageLiveLocation( + orThrow(this.chatId, "stopMessageLiveLocation"), + orThrow(this.msgId, "stopMessageLiveLocation"), + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendVenue`. Use this method to send information about a venue. On success, the sent Message is returned. + * + * @param latitude Latitude of the venue + * @param longitude Longitude of the venue + * @param title Name of the venue + * @param address Address of the venue + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendvenue + */ + replyWithVenue( + latitude: number, + longitude: number, + title: string, + address: string, + other?: Other< + "sendVenue", + "chat_id" | "latitude" | "longitude" | "title" | "address" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendVenue( + orThrow(this.chatId, "sendVenue"), + latitude, + longitude, + title, + address, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendContact`. Use this method to send phone contacts. On success, the sent Message is returned. + * + * @param phone_number Contact's phone number + * @param first_name Contact's first name + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendcontact + */ + replyWithContact( + phone_number: string, + first_name: string, + other?: Other<"sendContact", "chat_id" | "phone_number" | "first_name">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendContact( + orThrow(this.chatId, "sendContact"), + phone_number, + first_name, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendPoll`. Use this method to send a native poll. On success, the sent Message is returned. + * + * @param question Poll question, 1-300 characters + * @param options A list of answer options, 1-12 strings 1-100 characters each + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendpoll + */ + replyWithPoll( + question: string, + options: (string | InputPollOption)[], + other?: Other<"sendPoll", "chat_id" | "question" | "options">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendPoll( + orThrow(this.chatId, "sendPoll"), + question, + options, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendChecklist`. Use this method to send a checklist on behalf of a connected business account. On success, the sent Message is returned. + * + * @param checklist An object for the checklist to send + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchecklist + */ + replyWithChecklist( + checklist: InputChecklist, + other?: Other< + "sendChecklist", + "business_connection_id" | "chat_id" | "checklist" + >, + signal?: AbortSignal, + ) { + return this.api.sendChecklist( + orThrow(this.businessConnectionId, "sendChecklist"), + orThrow(this.chatId, "sendChecklist"), + checklist, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editMessageChecklist`. Use this method to edit a checklist on behalf of a connected business account. On success, the edited Message is returned. + * + * @param checklist An object for the new checklist + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagechecklist + */ + editMessageChecklist( + checklist: InputChecklist, + other?: Other< + "editMessageChecklist", + "business_connection_id" | "chat_id" | "messaage_id" | "checklist" + >, + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "editMessageChecklist"); + const target = msg.checklist_tasks_done?.checklist_message ?? + msg.checklist_tasks_added?.checklist_message ?? + msg; + return this.api.editMessageChecklist( + orThrow(this.businessConnectionId, "editMessageChecklist"), + orThrow(target.chat.id, "editMessageChecklist"), + orThrow(target.message_id, "editMessageChecklist"), + checklist, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.sendDice`. Use this method to send an animated emoji that will display a random value. On success, the sent Message is returned. + * + * @param emoji Emoji on which the dice throw animation is based. Currently, must be one of β€œπŸŽ²β€, β€œπŸŽ―β€, β€œπŸ€β€, β€œβš½β€, β€œπŸŽ³β€, or β€œπŸŽ°β€. Dice can have values 1-6 for β€œπŸŽ²β€, β€œπŸŽ―β€ and β€œπŸŽ³β€, values 1-5 for β€œπŸ€β€ and β€œβš½β€, and values 1-64 for β€œπŸŽ°β€. Defaults to β€œπŸŽ²β€ + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#senddice + */ + replyWithDice( + emoji: + | (string & Record) + | "🎲" + | "🎯" + | "πŸ€" + | "⚽" + | "🎳" + | "🎰", + other?: Other<"sendDice", "chat_id" | "emoji">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendDice( + orThrow(this.chatId, "sendDice"), + emoji, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.sendChatAction`. Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success. + * + * Example: The ImageBot needs some time to process a request and upload the image. Instead of sending a text message along the lines of β€œRetrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a β€œsending photo” status for the bot. + * + * We only recommend using this method when a response from the bot will take a noticeable amount of time to arrive. + * + * @param action Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for text messages, upload_photo for photos, record_video or upload_video for videos, record_voice or upload_voice for voice notes, upload_document for general files, choose_sticker for stickers, find_location for location data, record_video_note or upload_video_note for video notes. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchataction + */ + replyWithChatAction( + action: + | "typing" + | "upload_photo" + | "record_video" + | "upload_video" + | "record_voice" + | "upload_voice" + | "upload_document" + | "choose_sticker" + | "find_location" + | "record_video_note" + | "upload_video_note", + other?: Other<"sendChatAction", "chat_id" | "action">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendChatAction( + orThrow(this.chatId, "sendChatAction"), + action, + { + business_connection_id: this.businessConnectionId, + message_thread_id: msg?.message_thread_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.setMessageReaction`. Use this method to change the chosen reactions on a message. Service messages of some types can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. Bots can't use paid reactions. Returns True on success. + * + * @param reaction A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either already present on the message or explicitly allowed by chat administrators. Paid reactions can't be used by bots. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmessagereaction + */ + react( + reaction: MaybeArray, + other?: Other< + "setMessageReaction", + "chat_id" | "message_id" | "reaction" + >, + signal?: AbortSignal, + ) { + return this.api.setMessageReaction( + orThrow(this.chatId, "setMessageReaction"), + orThrow(this.msgId, "setMessageReaction"), + typeof reaction === "string" + ? [{ type: "emoji", emoji: reaction }] + : (Array.isArray(reaction) ? reaction : [reaction]) + .map((emoji) => + typeof emoji === "string" + ? { type: "emoji", emoji } + : emoji + ), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.sendMessageDraft`. Use this method to stream a partial message to a user while the message is being generated. Returns True on success. + * + * @param text Text of the message to be sent, 1-4096 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendmessagedraft + */ + replyWithDraft( + text: string, + other?: Other<"sendMessageDraft", "chat_id" | "text">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendMessageDraft( + orThrow(this.chatId, "sendMessageDraft"), + this.update.update_id, + text, + { + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `sendRichMessageDraft`. Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success. + * + * @param rich_message The partial message to be streamed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendrichmessagedraft + */ + replyWithRichMessageDraft( + rich_message: InputRichMessageWithoutUpload, + other?: Other< + "sendRichMessageDraft", + "chat_id" | "rich_message" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendRichMessageDraft( + orThrow(this.chatId, "sendMessageDraft"), + this.update.update_id, + rich_message, + { + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.getUserProfilePhotos`. Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos object. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserprofilephotos + */ + getUserProfilePhotos( + other?: Other<"getUserProfilePhotos", "user_id">, + signal?: AbortSignal, + ) { + return this.api.getUserProfilePhotos( + orThrow(this.from, "getUserProfilePhotos").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.getUserProfileAudios`. Use this method to get a list of profile audios for a user. Returns a UserProfileAudios object. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserprofileaudios + */ + getUserProfileAudios( + other?: Other<"getUserProfileAudios", "user_id">, + signal?: AbortSignal, + ) { + return this.api.getUserProfileAudios( + orThrow(this.from, "getUserProfileAudios").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.serUserEmojiStatus`. Changes the emoji status for a given user that previously allowed the bot to manage their emoji status via the Mini App method requestEmojiStatusAccess. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setuseremojistatus + */ + setUserEmojiStatus( + other?: Other<"setUserEmojiStatus", "user_id">, + signal?: AbortSignal, + ) { + return this.api.setUserEmojiStatus( + orThrow(this.from, "setUserEmojiStatus").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.getUserChatBoosts`. Use this method to get the list of boosts added to a chat by a user. Requires administrator rights in the chat. Returns a UserChatBoosts object. + * + * @param chat_id Unique identifier for the chat or username of the channel (in the format @channelusername) + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserchatboosts + */ + getUserChatBoosts(chat_id?: number | string, signal?: AbortSignal) { + return this.api.getUserChatBoosts( + chat_id ?? orThrow(this.chatId, "getUserChatBoosts"), + orThrow(this.from, "getUserChatBoosts").id, + signal, + ); + } + + /** + * Context-aware alias for `api.getUserGifts`. Returns the gifts owned and hosted by a user. Returns OwnedGifts on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getusergifts + */ + getUserGifts( + other?: Other<"getUserGifts", "user_id">, + signal?: AbortSignal, + ) { + return this.api.getUserGifts( + orThrow(this.from, "getUserGifts").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.getChatGifts`. Returns the gifts owned by a chat. Returns OwnedGifts on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatgifts + */ + getChatGifts( + other?: Other<"getChatGifts", "chat_id">, + signal?: AbortSignal, + ) { + return this.api.getChatGifts( + orThrow(this.chatId, "getChatGifts"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.getBusinessConnection`. Use this method to get information about the connection of the bot with a business account. Returns a BusinessConnection object on success. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessconnection + */ + getBusinessConnection(signal?: AbortSignal) { + return this.api.getBusinessConnection( + orThrow(this.businessConnectionId, "getBusinessConnection"), + signal, + ); + } + + /** + * Context-aware alias for `api.getManagedBotToken`. Use this method to get the token of a managed bot. Returns the token as String on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmanagedbottoken + */ + getManagedBotToken(signal?: AbortSignal) { + return this.api.getManagedBotToken( + orThrow(this.managedBot, "getManagedBotToken").bot.id, + signal, + ); + } + + /** + * Context-aware alias for `api.replaceManagedBotToken`. Use this method to revoke the current token of a managed bot and generate a new one. Returns the new token as String on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#replacemanagedbottoken + */ + replaceManagedBotToken(signal?: AbortSignal) { + return this.api.replaceManagedBotToken( + orThrow(this.managedBot, "getManagedBotToken").bot.id, + signal, + ); + } + + /** + * Context-aware alias for `api.getManagedBotAccessSettings`. Use this method to get the access settings of a managed bot. Returns a BotAccessSettings object on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getmanagedbotaccesssettings + */ + getManagedBotAccessSettings(signal?: AbortSignal) { + return this.api.getManagedBotAccessSettings( + orThrow(this.managedBot, "getManagedBotAccessSettings").bot.id, + signal, + ); + } + + /** + * Context-aware alias for `api.setManagedBotAccessSettings`. Use this method to change the access settings of a managed bot. Returns True on success. + * + * @param is_access_restricted Pass True, if only selected users can access the bot + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmanagedbotaccesssettingsrestricted + */ + setManagedBotAccessSettings( + is_access_restricted: boolean, + other?: Other< + "setManagedBotAccessSettings", + "user_id" | "is_access_restricted" + >, + signal?: AbortSignal, + ) { + return this.api.setManagedBotAccessSettings( + orThrow(this.managedBot, "setManagedBotAccessSettings").bot.id, + is_access_restricted, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.getFile`. Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a File object is returned. The file can then be downloaded via the link https://api.telegram.org/file/bot/, where is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling getFile again. + * + * Note: This function may not preserve the original file name and MIME type. You should save the file's MIME type and name (if available) when the File object is received. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getfile + */ + getFile(signal?: AbortSignal) { + const m = orThrow(this.msg, "getFile"); + const file = m.photo !== undefined // handles both photos and live photos + ? m.photo[m.photo.length - 1] + : m.animation ?? + m.audio ?? + m.document ?? + m.video ?? + m.video_note ?? + m.voice ?? + m.sticker; + return this.api.getFile(orThrow(file, "getFile").file_id, signal); + } + + /** @deprecated Use `banAuthor` instead. */ + kickAuthor(...args: Parameters) { + return this.banAuthor(...args); + } + + /** + * Context-aware alias for `api.banChatMember`. Use this method to ban a user in a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the chat on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#banchatmember + */ + banAuthor( + other?: Other<"banChatMember", "chat_id" | "user_id">, + signal?: AbortSignal, + ) { + return this.api.banChatMember( + orThrow(this.chatId, "banAuthor"), + orThrow(this.from, "banAuthor").id, + other, + signal, + ); + } + + /** @deprecated Use `banChatMember` instead. */ + kickChatMember(...args: Parameters) { + return this.banChatMember(...args); + } + + /** + * Context-aware alias for `api.banChatMember`. Use this method to ban a user in a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the chat on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#banchatmember + */ + banChatMember( + user_id: number, + other?: Other<"banChatMember", "chat_id" | "user_id">, + signal?: AbortSignal, + ) { + return this.api.banChatMember( + orThrow(this.chatId, "banChatMember"), + user_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.unbanChatMember`. Use this method to unban a previously banned user in a supergroup or channel. The user will not return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. By default, this method guarantees that after the call the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat they will also be removed from the chat. If you don't want this, use the parameter only_if_banned. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unbanchatmember + */ + unbanChatMember( + user_id: number, + other?: Other<"unbanChatMember", "chat_id" | "user_id">, + signal?: AbortSignal, + ) { + return this.api.unbanChatMember( + orThrow(this.chatId, "unbanChatMember"), + user_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.restrictChatMember`. Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate administrator rights. Pass True for all permissions to lift restrictions from a user. Returns True on success. + * + * @param permissions An object for new user permissions + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#restrictchatmember + */ + restrictAuthor( + permissions: ChatPermissions, + other?: Other< + "restrictChatMember", + "chat_id" | "user_id" | "permissions" + >, + signal?: AbortSignal, + ) { + return this.api.restrictChatMember( + orThrow(this.chatId, "restrictAuthor"), + orThrow(this.from, "restrictAuthor").id, + permissions, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.restrictChatMember`. Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate administrator rights. Pass True for all permissions to lift restrictions from a user. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param permissions An object for new user permissions + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#restrictchatmember + */ + restrictChatMember( + user_id: number, + permissions: ChatPermissions, + other?: Other< + "restrictChatMember", + "chat_id" | "user_id" | "permissions" + >, + signal?: AbortSignal, + ) { + return this.api.restrictChatMember( + orThrow(this.chatId, "restrictChatMember"), + user_id, + permissions, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.promoteChatMember`. Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Pass False for all boolean parameters to demote a user. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#promotechatmember + */ + promoteAuthor( + other?: Other<"promoteChatMember", "chat_id" | "user_id">, + signal?: AbortSignal, + ) { + return this.api.promoteChatMember( + orThrow(this.chatId, "promoteAuthor"), + orThrow(this.from, "promoteAuthor").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.promoteChatMember`. Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Pass False for all boolean parameters to demote a user. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#promotechatmember + */ + promoteChatMember( + user_id: number, + other?: Other<"promoteChatMember", "chat_id" | "user_id">, + signal?: AbortSignal, + ) { + return this.api.promoteChatMember( + orThrow(this.chatId, "promoteChatMember"), + user_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatAdministratorCustomTitle`. Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns True on success. + * + * @param custom_title New custom title for the administrator; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatadministratorcustomtitle + */ + setChatAdministratorAuthorCustomTitle( + custom_title: string, + signal?: AbortSignal, + ) { + return this.api.setChatAdministratorCustomTitle( + orThrow(this.chatId, "setChatAdministratorAuthorCustomTitle"), + orThrow(this.from, "setChatAdministratorAuthorCustomTitle").id, + custom_title, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatAdministratorCustomTitle`. Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param custom_title New custom title for the administrator; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatadministratorcustomtitle + */ + setChatAdministratorCustomTitle( + user_id: number, + custom_title: string, + signal?: AbortSignal, + ) { + return this.api.setChatAdministratorCustomTitle( + orThrow(this.chatId, "setChatAdministratorCustomTitle"), + user_id, + custom_title, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatMemberTag`. Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the β€œcan_manage_tags” administrator right. Returns True on success. + * + * @param tag New tag for the member; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setChatMemberTag + */ + setAuthorTag(tag: string, signal?: AbortSignal) { + return this.api.setChatMemberTag( + orThrow(this.chatId, "setChatMemberTag"), + orThrow(this.from, "setChatMemberTag").id, + tag, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatMemberTag`. Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the β€œcan_manage_tags” administrator right. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param tag New tag for the member; 0-16 characters, emoji are not allowed + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setChatMemberTag + */ + setChatMemberTag( + user_id: number, + tag: string, + signal?: AbortSignal, + ) { + return this.api.setChatMemberTag( + orThrow(this.chatId, "setChatMemberTag"), + user_id, + tag, + signal, + ); + } + + /** + * Context-aware alias for `api.banChatSenderChat`. Use this method to ban a channel chat in a supergroup or a channel. Until the chat is unbanned, the owner of the banned chat won't be able to send messages on behalf of any of their channels. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param sender_chat_id Unique identifier of the target sender chat + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#banchatsenderchat + */ + banChatSenderChat(sender_chat_id: number, signal?: AbortSignal) { + return this.api.banChatSenderChat( + orThrow(this.chatId, "banChatSenderChat"), + sender_chat_id, + signal, + ); + } + + /** + * Context-aware alias for `api.unbanChatSenderChat`. Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param sender_chat_id Unique identifier of the target sender chat + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unbanchatsenderchat + */ + unbanChatSenderChat( + sender_chat_id: number, + signal?: AbortSignal, + ) { + return this.api.unbanChatSenderChat( + orThrow(this.chatId, "unbanChatSenderChat"), + sender_chat_id, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatPermissions`. Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the can_restrict_members administrator rights. Returns True on success. + * + * @param permissions New default chat permissions + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatpermissions + */ + setChatPermissions( + permissions: ChatPermissions, + other?: Other<"setChatPermissions", "chat_id" | "permissions">, + signal?: AbortSignal, + ) { + return this.api.setChatPermissions( + orThrow(this.chatId, "setChatPermissions"), + permissions, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.exportChatInviteLink`. Use this method to generate a new primary invite link for a chat; any previously generated primary link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the new invite link as String on success. + * + * Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using exportChatInviteLink or by calling the getChat method. If your bot needs to generate a new primary invite link replacing its previous one, use exportChatInviteLink again. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#exportchatinvitelink + */ + exportChatInviteLink(signal?: AbortSignal) { + return this.api.exportChatInviteLink( + orThrow(this.chatId, "exportChatInviteLink"), + signal, + ); + } + + /** + * Context-aware alias for `api.createChatInviteLink`. Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. The link can be revoked using the method revokeChatInviteLink. Returns the new invite link as ChatInviteLink object. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createchatinvitelink + */ + createChatInviteLink( + other?: Other<"createChatInviteLink", "chat_id">, + signal?: AbortSignal, + ) { + return this.api.createChatInviteLink( + orThrow(this.chatId, "createChatInviteLink"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editChatInviteLink`. Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the edited invite link as a ChatInviteLink object. + * + * @param invite_link The invite link to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editchatinvitelink + */ + editChatInviteLink( + invite_link: string, + other?: Other<"editChatInviteLink", "chat_id" | "invite_link">, + signal?: AbortSignal, + ) { + return this.api.editChatInviteLink( + orThrow(this.chatId, "editChatInviteLink"), + invite_link, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.createChatSubscriptionInviteLink`. Use this method to create a subscription invite link for a channel chat. The bot must have the can_invite_users administrator rights. The link can be edited using the method editChatSubscriptionInviteLink or revoked using the method revokeChatInviteLink. Returns the new invite link as a ChatInviteLink object. + * + * @param subscription_period The number of seconds the subscription will be active for before the next payment. Currently, it must always be 2592000 (30 days). + * @param subscription_price The amount of Telegram Stars a user must pay initially and after each subsequent subscription period to be a member of the chat; 1-2500 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createchatsubscriptioninvitelink + */ + createChatSubscriptionInviteLink( + subscription_period: number, + subscription_price: number, + other?: Other< + "createChatSubscriptionInviteLink", + "chat_id" | "subscription_period" | "subscription_price" + >, + signal?: AbortSignal, + ) { + return this.api.createChatSubscriptionInviteLink( + orThrow(this.chatId, "createChatSubscriptionInviteLink"), + subscription_period, + subscription_price, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editChatSubscriptionInviteLink`. Use this method to edit a subscription invite link created by the bot. The bot must have the can_invite_users administrator rights. Returns the edited invite link as a ChatInviteLink object. + * + * @param invite_link The invite link to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editchatsubscriptioninvitelink + */ + editChatSubscriptionInviteLink( + invite_link: string, + other?: Other< + "editChatSubscriptionInviteLink", + "chat_id" | "invite_link" + >, + signal?: AbortSignal, + ) { + return this.api.editChatSubscriptionInviteLink( + orThrow(this.chatId, "editChatSubscriptionInviteLink"), + invite_link, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.revokeChatInviteLink`. Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the revoked invite link as ChatInviteLink object. + * + * @param invite_link The invite link to revoke + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#revokechatinvitelink + */ + revokeChatInviteLink(invite_link: string, signal?: AbortSignal) { + return this.api.revokeChatInviteLink( + orThrow(this.chatId, "editChatInviteLink"), + invite_link, + signal, + ); + } + + /** + * Context-aware alias for `api.approveChatJoinRequest`. Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#approvechatjoinrequest + */ + approveChatJoinRequest( + user_id: number, + signal?: AbortSignal, + ) { + return this.api.approveChatJoinRequest( + orThrow(this.chatId, "approveChatJoinRequest"), + user_id, + signal, + ); + } + + /** + * Context-aware alias for `api.declineChatJoinRequest`. Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. + * + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#declinechatjoinrequest + */ + declineChatJoinRequest( + user_id: number, + signal?: AbortSignal, + ) { + return this.api.declineChatJoinRequest( + orThrow(this.chatId, "declineChatJoinRequest"), + user_id, + signal, + ); + } + + /** + * Context-aware alias for `answerChatJoinRequestQuery`. Use this method to process a received chat join request query. Returns True on success. + * + * @param result Result of the query. Must be either β€œapprove” to allow the user to join the chat, β€œdecline” to disallow the user to join the chat, or β€œqueue” to leave the decision to other administrators. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerchatjoinrequestquery + */ + answerChatJoinRequestQuery( + result: "approve" | "decline" | "queue", + signal?: AbortSignal, + ) { + return this.api.answerChatJoinRequestQuery( + orThrow( + this.chatJoinRequest?.query_id, + "answerChatJoinRequestQuery", + ), + result, + signal, + ); + } + + /** + * Context-aware alias for `sendChatJoinRequestWebApp`. Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success. + * + * @param web_app_url The URL of the Mini App to be opened + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendchatjoinrequestwebapp + */ + replyWithChatJoinRequestWebApp(web_app_url: string, signal?: AbortSignal) { + return this.api.sendChatJoinRequestWebApp( + orThrow( + this.chatJoinRequest?.query_id, + "answerChatJoinRequestQuery", + ), + web_app_url, + signal, + ); + } + + /** + * Context-aware alias for `api.approveSuggestedPost`. Use this method to approve a suggested post in a direct messages chat. The bot must have the 'can_post_messages' administrator right in the corresponding channel chat. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#approvesuggestedpost + */ + approveSuggestedPost( + other?: Other<"approveSuggestedPost", "chat_id" | "message_id">, + signal?: AbortSignal, + ) { + return this.api.approveSuggestedPost( + orThrow(this.chatId, "approveSuggestedPost"), + orThrow(this.msgId, "approveSuggestedPost"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.declineSuggestedPost`. Use this method to decline a suggested post in a direct messages chat. The bot must have the 'can_manage_direct_messages' administrator right in the corresponding channel chat. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#declinesuggestedpost + */ + declineSuggestedPost( + other?: Other<"declineSuggestedPost", "chat_id" | "message_id">, + signal?: AbortSignal, + ) { + return this.api.declineSuggestedPost( + orThrow(this.chatId, "declineSuggestedPost"), + orThrow(this.msgId, "declineSuggestedPost"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatPhoto`. Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param photo New chat photo, uploaded using multipart/form-data + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatphoto + */ + setChatPhoto(photo: InputFile, signal?: AbortSignal) { + return this.api.setChatPhoto( + orThrow(this.chatId, "setChatPhoto"), + photo, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteChatPhoto`. Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletechatphoto + */ + deleteChatPhoto(signal?: AbortSignal) { + return this.api.deleteChatPhoto( + orThrow(this.chatId, "deleteChatPhoto"), + signal, + ); + } + + /** + * Context-aware alias for `api.setChatTitle`. Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param title New chat title, 1-255 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchattitle + */ + setChatTitle(title: string, signal?: AbortSignal) { + return this.api.setChatTitle( + orThrow(this.chatId, "setChatTitle"), + title, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatDescription`. Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success. + * + * @param description New chat description, 0-255 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatdescription + */ + setChatDescription(description: string | undefined, signal?: AbortSignal) { + return this.api.setChatDescription( + orThrow(this.chatId, "setChatDescription"), + description, + signal, + ); + } + + /** + * Context-aware alias for `api.pinChatMessage`. Use this method to add a message to the list of pinned messages in a chat. In private chats and channel direct messages chats, all non-service messages can be pinned. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to pin messages in groups and channels respectively. Returns True on success. + * + * @param message_id Identifier of a message to pin + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#pinchatmessage + */ + pinChatMessage( + message_id: number, + other?: Other<"pinChatMessage", "chat_id" | "message_id">, + signal?: AbortSignal, + ) { + return this.api.pinChatMessage( + orThrow(this.chatId, "pinChatMessage"), + message_id, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.unpinChatMessage`. Use this method to remove a message from the list of pinned messages in a chat. In private chats and channel direct messages chats, all messages can be unpinned. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to unpin messages in groups and channels respectively. Returns True on success. + * + * @param message_id Identifier of a message to unpin. If not specified, the most recent pinned message (by sending date) will be unpinned. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinchatmessage + */ + unpinChatMessage( + message_id?: number, + other?: Other<"unpinChatMessage", "chat_id" | "message_id">, + signal?: AbortSignal, + ) { + return this.api.unpinChatMessage( + orThrow(this.chatId, "unpinChatMessage"), + message_id, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.unpinAllChatMessages`. Use this method to clear the list of pinned messages in a chat. In private chats and channel direct messages chats, no additional rights are required to unpin all pinned messages. Conversely, the bot must be an administrator with the 'can_pin_messages' right or the 'can_edit_messages' right to unpin all pinned messages in groups and channels respectively. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallchatmessages + */ + unpinAllChatMessages(signal?: AbortSignal) { + return this.api.unpinAllChatMessages( + orThrow(this.chatId, "unpinAllChatMessages"), + signal, + ); + } + + /** + * Context-aware alias for `api.leaveChat`. Use this method for your bot to leave a group, supergroup or channel. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#leavechat + */ + leaveChat(signal?: AbortSignal) { + return this.api.leaveChat(orThrow(this.chatId, "leaveChat"), signal); + } + + /** + * Context-aware alias for `api.getChat`. Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchat + */ + getChat(signal?: AbortSignal) { + return this.api.getChat(orThrow(this.chatId, "getChat"), signal); + } + + /** + * Context-aware alias for `api.getChatAdministrators`. Use this method to get a list of administrators in a chat, which aren't bots. Returns an Array of ChatMember objects. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatadministrators + */ + getChatAdministrators( + other?: Other<"getChatAdministrators", "chat_id">, + signal?: AbortSignal, + ) { + return this.api.getChatAdministrators( + orThrow(this.chatId, "getChatAdministrators"), + other, + signal, + ); + } + + /** @deprecated Use `getChatMemberCount` instead. */ + getChatMembersCount(...args: Parameters) { + return this.getChatMemberCount(...args); + } + + /** + * Context-aware alias for `api.getChatMemberCount`. Use this method to get the number of members in a chat. Returns Integer on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmembercount + */ + getChatMemberCount(signal?: AbortSignal) { + return this.api.getChatMemberCount( + orThrow(this.chatId, "getChatMemberCount"), + signal, + ); + } + + /** + * Context-aware alias for `api.getChatMember`. Use this method to get information about a member of a chat. The method is guaranteed to work only if the bot is an administrator in the chat. Returns a ChatMember object on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmember + */ + getAuthor(signal?: AbortSignal) { + return this.api.getChatMember( + orThrow(this.chatId, "getAuthor"), + orThrow(this.from, "getAuthor").id, + signal, + ); + } + + /** + * Context-aware alias for `api.getChatMember`. Use this method to get information about a member of a chat. The method is guaranteed to work only if the bot is an administrator in the chat. Returns a ChatMember object on success. + * + * @param user_id Unique identifier of the target user + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmember + */ + getChatMember(user_id: number, signal?: AbortSignal) { + return this.api.getChatMember( + orThrow(this.chatId, "getChatMember"), + user_id, + signal, + ); + } + + /** + * Context-aware alias for `api.getUserPersonalChatMessages`. Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an Array of Message objects is returned. + * + * @param limit The maximum number of messages to return; 1-20 + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getuserpersonalchatmessages + */ + getUserPersonalChatMessages( + limit: number, + signal?: AbortSignal, + ) { + return this.api.getUserPersonalChatMessages( + orThrow(this.from, "getUserPersonalChatMessages").id, + limit, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatStickerSet`. Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Use the field can_set_sticker_set ly returned in getChat requests to check if the bot can use this method. Returns True on success. + * + * @param sticker_set_name Name of the sticker set to be set as the group sticker set + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatstickerset + */ + setChatStickerSet(sticker_set_name: string, signal?: AbortSignal) { + return this.api.setChatStickerSet( + orThrow(this.chatId, "setChatStickerSet"), + sticker_set_name, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteChatStickerSet`. Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Use the field can_set_sticker_set ly returned in getChat requests to check if the bot can use this method. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletechatstickerset + */ + deleteChatStickerSet(signal?: AbortSignal) { + return this.api.deleteChatStickerSet( + orThrow(this.chatId, "deleteChatStickerSet"), + signal, + ); + } + + /** + * Context-aware alias for `api.createForumTopic`. Use this method to create a topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator right. Returns information about the created topic as a ForumTopic object. + * + * @param name Topic name, 1-128 characters + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#createforumtopic + */ + createForumTopic( + name: string, + other?: Other<"createForumTopic", "chat_id" | "name">, + signal?: AbortSignal, + ) { + return this.api.createForumTopic( + orThrow(this.chatId, "createForumTopic"), + name, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editForumTopic`. Use this method to edit name and icon of a topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editforumtopic + */ + editForumTopic( + other?: Other<"editForumTopic", "chat_id" | "message_thread_id">, + signal?: AbortSignal, + ) { + const message = orThrow(this.msg, "editForumTopic"); + const thread = orThrow(message.message_thread_id, "editForumTopic"); + return this.api.editForumTopic(message.chat.id, thread, other, signal); + } + + /** + * Context-aware alias for `api.closeForumTopic`. Use this method to close an open topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#closeforumtopic + */ + closeForumTopic(signal?: AbortSignal) { + const message = orThrow(this.msg, "closeForumTopic"); + const thread = orThrow(message.message_thread_id, "closeForumTopic"); + return this.api.closeForumTopic(message.chat.id, thread, signal); + } + + /** + * Context-aware alias for `api.reopenForumTopic`. Use this method to reopen a closed topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#reopenforumtopic + */ + reopenForumTopic(signal?: AbortSignal) { + const message = orThrow(this.msg, "reopenForumTopic"); + const thread = orThrow(message.message_thread_id, "reopenForumTopic"); + return this.api.reopenForumTopic(message.chat.id, thread, signal); + } + + /** + * Context-aware alias for `api.deleteForumTopic`. Use this method to delete a forum topic along with all its messages in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_delete_messages administrator rights. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteforumtopic + */ + deleteForumTopic(signal?: AbortSignal) { + const message = orThrow(this.msg, "deleteForumTopic"); + const thread = orThrow(message.message_thread_id, "deleteForumTopic"); + return this.api.deleteForumTopic(message.chat.id, thread, signal); + } + + /** + * Context-aware alias for `api.unpinAllForumTopicMessages`. Use this method to clear the list of pinned messages in a forum topic in a forum supergroup chat or a private chat with a user. In the case of a supergroup chat the bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallforumtopicmessages + */ + unpinAllForumTopicMessages(signal?: AbortSignal) { + const message = orThrow(this.msg, "unpinAllForumTopicMessages"); + const thread = orThrow( + message.message_thread_id, + "unpinAllForumTopicMessages", + ); + return this.api.unpinAllForumTopicMessages( + message.chat.id, + thread, + signal, + ); + } + + /** + * Context-aware alias for `api.editGeneralForumTopic`. Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param name New topic name, 1-128 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editgeneralforumtopic + */ + editGeneralForumTopic(name: string, signal?: AbortSignal) { + return this.api.editGeneralForumTopic( + orThrow(this.chatId, "editGeneralForumTopic"), + name, + signal, + ); + } + + /** + * Context-aware alias for `api.closeGeneralForumTopic`. Use this method to close an open 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#closegeneralforumtopic + */ + closeGeneralForumTopic(signal?: AbortSignal) { + return this.api.closeGeneralForumTopic( + orThrow(this.chatId, "closeGeneralForumTopic"), + signal, + ); + } + + /** + * Context-aware alias for `api.reopenGeneralForumTopic`. Use this method to reopen a closed 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically unhidden if it was hidden. Returns True on success. * + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#reopengeneralforumtopic + */ + reopenGeneralForumTopic(signal?: AbortSignal) { + return this.api.reopenGeneralForumTopic( + orThrow(this.chatId, "reopenGeneralForumTopic"), + signal, + ); + } + + /** + * Context-aware alias for `api.hideGeneralForumTopic`. Use this method to hide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically closed if it was open. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#hidegeneralforumtopic + */ + hideGeneralForumTopic(signal?: AbortSignal) { + return this.api.hideGeneralForumTopic( + orThrow(this.chatId, "hideGeneralForumTopic"), + signal, + ); + } + + /** + * Context-aware alias for `api.unhideGeneralForumTopic`. Use this method to unhide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unhidegeneralforumtopic + */ + unhideGeneralForumTopic(signal?: AbortSignal) { + return this.api.unhideGeneralForumTopic( + orThrow(this.chatId, "unhideGeneralForumTopic"), + signal, + ); + } + + /** + * Context-aware alias for `api.unpinAllGeneralForumTopicMessages`. Use this method to clear the list of pinned messages in a General forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages + */ + unpinAllGeneralForumTopicMessages(signal?: AbortSignal) { + return this.api.unpinAllGeneralForumTopicMessages( + orThrow(this.chatId, "unpinAllGeneralForumTopicMessages"), + signal, + ); + } + + /** + * Context-aware alias for `api.answerCallbackQuery`. Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. + * + * Alternatively, the user can be redirected to the specified Game URL. For this option to work, you must first create a game for your bot via @BotFather and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answercallbackquery + */ + answerCallbackQuery( + other?: string | Other<"answerCallbackQuery", "callback_query_id">, + signal?: AbortSignal, + ) { + return this.api.answerCallbackQuery( + orThrow(this.callbackQuery, "answerCallbackQuery").id, + typeof other === "string" ? { text: other } : other, + signal, + ); + } + + /** + * Context-aware alias for `ctx.answerGuestQuery`. Use this method to reply to a received guest message. On success, a SentGuestMessage object is returned. + * + * @param result An object describing the message to be sent + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerguestquery + */ + answerGuestQuery( + result: InlineQueryResult, + signal?: AbortSignal, + ) { + return this.api.answerGuestQuery( + orThrow(this.guestMessage?.guest_query_id, "answerGuestQuery"), + result, + signal, + ); + } + + /** + * Context-aware alias for `api.setChatMenuButton`. Use this method to change the bot's menu button in a private chat, or the default menu button. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setchatmenubutton + */ + setChatMenuButton( + other?: Other<"setChatMenuButton">, + signal?: AbortSignal, + ) { + return this.api.setChatMenuButton(other, signal); + } + + /** + * Context-aware alias for `api.getChatMenuButton`. Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns MenuButton on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getchatmenubutton + */ + getChatMenuButton( + other?: Other<"getChatMenuButton">, + signal?: AbortSignal, + ) { + return this.api.getChatMenuButton(other, signal); + } + + /** + * Context-aware alias for `api.setMyDefaultAdministratorRights`. Use this method to the change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setmydefaultadministratorrights + */ + setMyDefaultAdministratorRights( + other?: Other<"setMyDefaultAdministratorRights">, + signal?: AbortSignal, + ) { + return this.api.setMyDefaultAdministratorRights(other, signal); + } + + /** + * Context-aware alias for `api.getMyDefaultAdministratorRights`. Use this method to get the current default administrator rights of the bot. Returns ChatAdministratorRights on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + */ + getMyDefaultAdministratorRights( + other?: Other<"getMyDefaultAdministratorRights">, + signal?: AbortSignal, + ) { + return this.api.getMyDefaultAdministratorRights(other, signal); + } + + /** + * Context-aware alias for `api.editMessageText`. Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param text_or_rich_message New text or rich content of the message, a string maps to the `text` parameter and an object maps to the `rich_message` parameter + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagetext + */ + editMessageText( + text: string | InputRichMessage, + other?: Other< + "editMessageText", + | "chat_id" + | "message_id" + | "inline_message_id" + | "text" + | "rich_message" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.editMessageTextInline( + inlineId, + text, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.editMessageText( + orThrow(this.chatId, "editMessageText"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "editMessageText", + ), + text, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.editMessageCaption`. Use this method to edit captions of messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagecaption + */ + editMessageCaption( + other?: Other< + "editMessageCaption", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.editMessageCaptionInline( + inlineId, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.editMessageCaption( + orThrow(this.chatId, "editMessageCaption"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "editMessageCaption", + ), + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.editMessageMedia`. Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param media An object for a new media content of the message + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagemedia + */ + editMessageMedia( + media: InputMedia, + other?: Other< + "editMessageMedia", + "chat_id" | "message_id" | "inline_message_id" | "media" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.editMessageMediaInline( + inlineId, + media, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.editMessageMedia( + orThrow(this.chatId, "editMessageMedia"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "editMessageMedia", + ), + media, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.editMessageReplyMarkup`. Use this method to edit only the reply markup of messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editmessagereplymarkup + */ + editMessageReplyMarkup( + other?: Other< + "editMessageReplyMarkup", + "chat_id" | "message_id" | "inline_message_id" + >, + signal?: AbortSignal, + ) { + const inlineId = this.inlineMessageId; + return inlineId !== undefined + ? this.api.editMessageReplyMarkupInline( + inlineId, + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ) + : this.api.editMessageReplyMarkup( + orThrow(this.chatId, "editMessageReplyMarkup"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "editMessageReplyMarkup", + ), + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.stopPoll`. Use this method to stop a poll which was sent by the bot. On success, the stopped Poll is returned. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#stoppoll + */ + stopPoll( + other?: Other<"stopPoll", "chat_id" | "message_id">, + signal?: AbortSignal, + ) { + return this.api.stopPoll( + orThrow(this.chatId, "stopPoll"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "stopPoll", + ), + { business_connection_id: this.businessConnectionId, ...other }, + signal, + ); + } + + /** + * Context-aware alias for `api.editEphemeralMessageText`. Use this method to edit an ephemeral text message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param text New text of the message, 1-4096 characters after entity parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagetext + */ + editEphemeralMessageText( + text: string, + other?: Other< + "editEphemeralMessageText", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "text" + >, + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "editEphemeralMessageText"); + return this.api.editEphemeralMessageText( + msg.chat.id, + orThrow(msg.receiver_user, "editEphemeralMessageText").id, + orThrow(msg.ephemeral_message_id, "editEphemeralMessageText"), + text, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editEphemeralMessageMedia`. Use this method to edit the media of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param media An object for the new media content of the message. A new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagemedia + */ + editEphemeralMessageMedia( + media: InputMediaWithoutUpload, + other?: Other< + "editEphemeralMessageMedia", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "media" + >, + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "editEphemeralMessageMedia"); + return this.api.editEphemeralMessageMedia( + msg.chat.id, + orThrow(msg.receiver_user, "editEphemeralMessageMedia").id, + orThrow(msg.ephemeral_message_id, "editEphemeralMessageMedia"), + media, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editEphemeralMessageCaption`. Use this method to edit the caption of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param caption New caption of the message, 0-1024 characters after entities parsing + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagecaption + */ + editEphemeralMessageCaption( + caption: string, + other?: Other< + "editEphemeralMessageCaption", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" | "caption" + >, + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "editEphemeralMessageCaption"); + return this.api.editEphemeralMessageCaption( + msg.chat.id, + orThrow(msg.receiver_user, "editEphemeralMessageCaption").id, + orThrow(msg.ephemeral_message_id, "editEphemeralMessageCaption"), + caption, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editEphemeralMessageReplyMarkup`. Use this method to edit only the reply markup of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned. + * + * @param chat_id Unique identifier for the target chat or username of the target supergroup in the format `@username` + * @param receiver_user_id Identifier of the user who received the message + * @param ephemeral_message_id Identifier of the ephemeral message to edit + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editephemeralmessagecaption + */ + editEphemeralMessageReplyMarkup( + other?: Other< + "editEphemeralMessageReplyMarkup", + "chat_id" | "receiver_user_id" | "ephemeral_message_id" + >, + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "editEphemeralMessageReplyMarkup"); + return this.api.editEphemeralMessageReplyMarkup( + msg.chat.id, + orThrow(msg.receiver_user, "editEphemeralMessageReplyMarkup").id, + orThrow( + msg.ephemeral_message_id, + "editEphemeralMessageReplyMarkup", + ), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteMessage`. Use this method to delete a message, including service messages, with the following limitations: + * - A message can only be deleted if it was sent less than 48 hours ago. + * - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. + * - Bots can delete outgoing messages in private chats, groups, and supergroups. + * - Bots can delete incoming messages in private chats. + * - Bots granted can_post_messages permissions can delete outgoing messages in channels. + * - If the bot is an administrator of a group, it can delete any message there. + * - If the bot has can_delete_messages administrator right in a supergroup or a channel, it can delete any message there. + * - If the bot has can_manage_direct_messages administrator right in a channel, it can delete any message in the corresponding direct messages chat. + * Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessage + */ + deleteMessage(signal?: AbortSignal) { + return this.api.deleteMessage( + orThrow(this.chatId, "deleteMessage"), + orThrow( + this.msg?.message_id ?? this.messageReaction?.message_id ?? + this.messageReactionCount?.message_id, + "deleteMessage", + ), + signal, + ); + } + + /** + * Context-aware alias for `api.deleteMessages`. Use this method to delete multiple messages simultaneously. Returns True on success. + * + * @param chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) + * @param message_ids A list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessages + */ + deleteMessages(message_ids: number[], signal?: AbortSignal) { + return this.api.deleteMessages( + orThrow(this.chatId, "deleteMessages"), + message_ids, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteEphemeralMessage`. Use this method to delete an ephemeral message. Note that it is not guaranteed that the user will receive the message deletion event, especially if they are offline. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteephemeralmessage + */ + deleteEphemeralMessage( + signal?: AbortSignal, + ) { + const msg = orThrow(this.msg, "deleteEphemeralMessage"); + return this.api.deleteEphemeralMessage( + msg.chat.id, + orThrow(msg.receiver_user, "deleteEphemeralMessage").id, + orThrow(msg.ephemeral_message_id, "deleteEphemeralMessage"), + signal, + ); + } + + /** + * Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessagereaction + */ + deleteMessageReaction( + other?: Other< + "deleteMessageReaction", + "chat_id" | "message_id" | "user_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + const reaction = orThrow(this.messageReaction, "deleteMessageReaction"); + if (reaction.user !== undefined) { + return this.deleteMessageReactionUser( + reaction.user.id, + other, + signal, + ); + } else if (reaction.actor_chat !== undefined) { + return this.deleteMessageReactionChat( + reaction.actor_chat.id, + other, + signal, + ); + } else { + throw new Error( + "Missing information from message_reaction update for API call to deleteMessageReaction", + ); + } + } + + /** + * Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param user_id Identifier of the user whose reaction will be removed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessagereaction + */ + deleteMessageReactionUser( + user_id: number, + other?: Other< + "deleteMessageReaction", + "chat_id" | "message_id" | "user_id" + >, + signal?: AbortSignal, + ) { + return this.api.deleteMessageReactionUser( + orThrow(this.chatId, "deleteMessageReactionUser"), + orThrow(this.msgId, "deleteMessageReactionUser"), + user_id, + other, + signal, + ); + } + + /** + * Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param actor_chat_id Identifier of the chat whose reaction will be removed + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletemessagereaction + */ + deleteMessageReactionChat( + actor_chat_id: number, + other?: Other< + "deleteMessageReaction", + "chat_id" | "message_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + return this.api.deleteMessageReactionChat( + orThrow(this.chatId, "deleteMessageReactionChat"), + orThrow(this.msgId, "deleteMessageReactionChat"), + actor_chat_id, + other, + signal, + ); + } + + /** + * Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteallmessagereactions + */ + deleteAllMessageReactions( + other?: Other< + "deleteAllMessageReactions", + "chat_id" | "message_id" | "user_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + const chatId = orThrow(this.chatId, "deleteAllMessageReactions"); + const actor = this.messageReaction?.actor_chat ?? this.senderChat ?? + this.pollAnswer?.voter_chat; + if (actor !== undefined) { + return this.api.deleteAllMessageReactionsChat( + chatId, + actor.id, + other, + signal, + ); + } + const userId = orThrow(this.from, "deleteAllMessageReactions").id; + return this.api.deleteAllMessageReactionsUser( + chatId, + userId, + other, + signal, + ); + } + + /** + * Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param user_id Identifier of the user whose reactions will be removed, if the reactions were added by a user + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteallmessagereactions + */ + deleteAllMessageReactionsUser( + user_id: number, + other?: Other< + "deleteAllMessageReactions", + "chat_id" | "message_id" | "user_id" + >, + signal?: AbortSignal, + ) { + return this.api.deleteAllMessageReactionsUser( + orThrow(this.chatId, "deleteAllMessageReactionsUser"), + user_id, + other, + signal, + ); + } + + /** + * Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + * + * @param actor_chat_id Identifier of the chat whose reactions will be removed, if the reactions were added by a chat + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deleteallmessagereactions + */ + deleteAllMessageReactionsChat( + actor_chat_id: number, + other?: Other< + "deleteAllMessageReactions", + "chat_id" | "message_id" | "actor_chat_id" + >, + signal?: AbortSignal, + ) { + return this.api.deleteAllMessageReactionsChat( + orThrow(this.chatId, "deleteAllMessageReactionsChat"), + actor_chat_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteBusinessMessages`. Delete messages on behalf of a business account. Requires the can_delete_outgoing_messages business bot right to delete messages sent by the bot itself, or the can_delete_all_messages business bot right to delete any message. Returns True on success. + * + * @param message_ids A list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletebusinessmessages + */ + deleteBusinessMessages(message_ids: number[], signal?: AbortSignal) { + return this.api.deleteBusinessMessages( + orThrow(this.businessConnectionId, "deleteBusinessMessages"), + message_ids, + signal, + ); + } + + /** + * Context-aware alias for `api.setBusinessAccountName`. Changes the first and last name of a managed business account. Requires the can_change_name business bot right. Returns True on success. + * + * @param first_name The new value of the first name for the business account; 1-64 characters + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountname + */ + setBusinessAccountName( + first_name: string, + other: Other< + "setBusinessAccountName", + "business_connection_id" | "first_name" + >, + signal?: AbortSignal, + ) { + return this.api.setBusinessAccountName( + orThrow(this.businessConnectionId, "setBusinessAccountName"), + first_name, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.setBusinessAccountUsername`. Changes the username of a managed business account. Requires the can_change_username business bot right. Returns True on success. + * + * @param username The new value of the username for the business account; 0-32 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountusername + */ + setBusinessAccountUsername(username: string, signal?: AbortSignal) { + return this.api.setBusinessAccountUsername( + orThrow(this.businessConnectionId, "setBusinessAccountUsername"), + username, + signal, + ); + } + + /** + * Context-aware alias for `api.setBusinessAccountBio`. Changes the bio of a managed business account. Requires the can_change_bio business bot right. Returns True on success. + * + * @param bio The new value of the bio for the business account; 0-140 characters + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountbio + */ + setBusinessAccountBio(bio: string, signal?: AbortSignal) { + return this.api.setBusinessAccountBio( + orThrow(this.businessConnectionId, "setBusinessAccountBio"), + bio, + signal, + ); + } + + /** + * Context-aware alias for `api.setBusinessAccountProfilePhoto`. CsetBusinessAccountProfilePhotohanges the profile photo of a managed business account. Requires the can_edit_profile_photo business bot right. Returns True on success. + * + * @param photo The new profile photo to set + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountprofilephoto + */ + setBusinessAccountProfilePhoto( + photo: InputProfilePhoto, + other: Other< + "setBusinessAccountProfilePhoto", + "business_connection_id" | "photo" + >, + signal?: AbortSignal, + ) { + return this.api.setBusinessAccountProfilePhoto( + orThrow( + this.businessConnectionId, + "setBusinessAccountProfilePhoto", + ), + photo, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.removeBusinessAccountProfilePhoto`. Removes the current profile photo of a managed business account. Requires the can_edit_profile_photo business bot right. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removebusinessaccountprofilephoto + */ + removeBusinessAccountProfilePhoto( + other: Other< + "removeBusinessAccountProfilePhoto", + "business_connection_id" + >, + signal?: AbortSignal, + ) { + return this.api.removeBusinessAccountProfilePhoto( + orThrow( + this.businessConnectionId, + "removeBusinessAccountProfilePhoto", + ), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.setBusinessAccountGiftSettings`. Changes the privacy settings pertaining to incoming gifts in a managed business account. Requires the can_change_gift_settings business bot right. Returns True on success. + * + * @param show_gift_button Pass True, if a button for sending a gift to the user or by the business account must always be shown in the input field + * @param accepted_gift_types Types of gifts accepted by the business account + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setbusinessaccountgiftsettings + */ + setBusinessAccountGiftSettings( + show_gift_button: boolean, + accepted_gift_types: AcceptedGiftTypes, + signal?: AbortSignal, + ) { + return this.api.setBusinessAccountGiftSettings( + orThrow( + this.businessConnectionId, + "setBusinessAccountGiftSettings", + ), + show_gift_button, + accepted_gift_types, + signal, + ); + } + + /** + * Context-aware alias for `api.getBusinessAccountStarBalance`. Returns the amount of Telegram Stars owned by a managed business account. Requires the can_view_gifts_and_stars business bot right. Returns StarAmount on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessaccountstarbalance + */ + getBusinessAccountStarBalance(signal?: AbortSignal) { + return this.api.getBusinessAccountStarBalance( + orThrow(this.businessConnectionId, "getBusinessAccountStarBalance"), + signal, + ); + } + + /** + * Context-aware alias for `api.transferBusinessAccountStars`. Transfers Telegram Stars from the business account balance to the bot's balance. Requires the can_transfer_stars business bot right. Returns True on success. + * + * @param star_count Number of Telegram Stars to transfer; 1-10000 + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#transferbusinessaccountstars + */ + transferBusinessAccountStars(star_count: number, signal?: AbortSignal) { + return this.api.transferBusinessAccountStars( + orThrow(this.businessConnectionId, "transferBusinessAccountStars"), + star_count, + signal, + ); + } + + /** + * Context-aware alias for `api.getBusinessAccountGifts`. Returns the gifts received and owned by a managed business account. Requires the can_view_gifts_and_stars business bot right. Returns OwnedGifts on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getbusinessaccountgifts + */ + getBusinessAccountGifts( + other: Other<"getBusinessAccountGifts", "business_connection_id">, + signal?: AbortSignal, + ) { + return this.api.getBusinessAccountGifts( + orThrow(this.businessConnectionId, "getBusinessAccountGifts"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.convertGiftToStars`. Converts a given regular gift to Telegram Stars. Requires the can_convert_gifts_to_stars business bot right. Returns True on success. + * + * @param owned_gift_id Unique identifier of the regular gift that should be converted to Telegram Stars + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#convertgifttostars + */ + convertGiftToStars( + owned_gift_id: string, + signal?: AbortSignal, + ) { + return this.api.convertGiftToStars( + orThrow(this.businessConnectionId, "convertGiftToStars"), + owned_gift_id, + signal, + ); + } + + /** + * Context-aware alias for `api.upgradeGift`. Upgrades a given regular gift to a unique gift. Requires the can_transfer_and_upgrade_gifts business bot right. Additionally requires the can_transfer_stars business bot right if the upgrade is paid. Returns True on success. + * + * @param owned_gift_id Unique identifier of the regular gift that should be upgraded to a unique one + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#upgradegift + */ + upgradeGift( + owned_gift_id: string, + other: Other< + "getBusinessAccountGifts", + "business_connection_id" | "owned_gift_id" + >, + signal?: AbortSignal, + ) { + return this.api.upgradeGift( + orThrow(this.businessConnectionId, "upgradeGift"), + owned_gift_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.transferGift`. Transfers an owned unique gift to another user. Requires the can_transfer_and_upgrade_gifts business bot right. Requires can_transfer_stars business bot right if the transfer is paid. Returns True on success. + * + * @param owned_gift_id Unique identifier of the regular gift that should be transferred + * @param new_owner_chat_id Unique identifier of the chat which will own the gift. The chat must be active in the last 24 hours. + * @param star_count The amount of Telegram Stars that will be paid for the transfer from the business account balance. If positive, then the can_transfer_stars business bot right is required. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#transfergift + */ + transferGift( + owned_gift_id: string, + new_owner_chat_id: number, + star_count: number, + signal?: AbortSignal, + ) { + return this.api.transferGift( + orThrow(this.businessConnectionId, "transferGift"), + owned_gift_id, + new_owner_chat_id, + star_count, + signal, + ); + } + + /** + * Context-aware alias for `api.postStory`. Posts a story on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns Story on success. + * + * @param content Content of the story + * @param active_period Period after which the story is moved to the archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or 2 * 86400 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#poststory + */ + postStory( + content: InputStoryContent, + active_period: number, + other: Other< + "postStory", + "business_connection_id" | "content" | "active_period" + >, + signal?: AbortSignal, + ) { + return this.api.postStory( + orThrow(this.businessConnectionId, "postStory"), + content, + active_period, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.repostStory`. Reposts a story on behalf of a business account from another business account. Both business accounts must be managed by the same bot, and the story on the source account must have been posted (or reposted) by the bot. Requires the can_manage_stories business bot right for both business accounts. Returns Story on success. + * + * @param active_period Period after which the story is moved to the archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or 2 * 86400 + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#repoststory + */ + repostStory( + active_period: number, + other: Other< + "repostStory", + | "business_connection_id" + | "from_chat_id" + | "from_story_id" + | "active_period" + >, + signal?: AbortSignal, + ) { + const story = orThrow(this.msg?.story, "repostStory"); + return this.api.repostStory( + orThrow(this.businessConnectionId, "repostStory"), + story.chat.id, + story.id, + active_period, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.editStory`. Edits a story previously posted by the bot on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns Story on success. + * + * @param story_id Unique identifier of the story to edit + * @param content Content of the story + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#editstory + */ + editStory( + story_id: number, + content: InputStoryContent, + other: Other< + "editStory", + "business_connection_id" | "story_id" | "content" + >, + signal?: AbortSignal, + ) { + return this.api.editStory( + orThrow(this.businessConnectionId, "editStory"), + story_id, + content, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.deleteStory`. Deletes a story previously posted by the bot on behalf of a managed business account. Requires the can_manage_stories business bot right. Returns True on success. + * + * @param story_id Unique identifier of the story to delete + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#deletestory + */ + deleteStory(story_id: number, signal?: AbortSignal) { + return this.api.deleteStory( + orThrow(this.businessConnectionId, "deleteStory"), + story_id, + signal, + ); + } + + /** + * Context-aware alias for `api.sendSticker`. Use this method to send static .WEBP, animated .TGS, or video .WEBM stickers. On success, the sent Message is returned. + * + * @param sticker Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP sticker from the Internet, or upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data. Video and animated stickers can't be sent via an HTTP URL. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendsticker + */ + replyWithSticker( + sticker: InputFile | string, + other?: Other<"sendSticker", "chat_id" | "sticker">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendSticker( + orThrow(this.chatId, "sendSticker"), + sticker, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects. + * + * @param custom_emoji_ids A list of custom emoji identifiers + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#getcustomemojistickers + */ + getCustomEmojiStickers(signal?: AbortSignal) { + type Emoji = MessageEntity.CustomEmojiMessageEntity; + return this.api.getCustomEmojiStickers( + (this.msg?.entities ?? []) + .filter((e): e is Emoji => e.type === "custom_emoji") + .map((e) => e.custom_emoji_id), + signal, + ); + } + + /** + * Context-aware alias for `api.sendGift`. Sends a gift to the given user. The gift can't be converted to Telegram Stars by the receiver. Returns True on success. + * + * @param gift_id Identifier of the gift + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgift + */ + replyWithGift( + gift_id: string, + other?: Other<"sendGift", "user_id" | "chat_id" | "gift_id">, + signal?: AbortSignal, + ) { + return this.api.sendGift( + orThrow(this.from, "sendGift").id, + gift_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.giftPremiumSubscription`. Gifts a Telegram Premium subscription to the given user. Returns True on success. + * + * @param month_count Number of months the Telegram Premium subscription will be active for the user; must be one of 3, 6, or 12 + * @param star_count Number of Telegram Stars to pay for the Telegram Premium subscription; must be 1000 for 3 months, 1500 for 6 months, and 2500 for 12 months + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#giftpremiumsubscription + */ + giftPremiumSubscription( + month_count: 3 | 6 | 12, + star_count: 1000 | 1500 | 2500, + other?: Other< + "giftPremiumSubscription", + "user_id" | "month_count" | "star_count" + >, + signal?: AbortSignal, + ) { + return this.api.giftPremiumSubscription( + orThrow(this.from, "giftPremiumSubscription").id, + month_count, + star_count, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.sendGift`. Sends a gift to the given channel chat. The gift can't be converted to Telegram Stars by the receiver. Returns True on success. + * + * @param gift_id Identifier of the gift + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgift + */ + replyWithGiftToChannel( + gift_id: string, + other?: Other<"sendGift", "user_id" | "chat_id" | "gift_id">, + signal?: AbortSignal, + ) { + return this.api.sendGiftToChannel( + orThrow(this.chat, "sendGift").id, + gift_id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.answerInlineQuery`. Use this method to send answers to an inline query. On success, True is returned. + * No more than 50 results per query are allowed. + * + * Example: An inline bot that sends YouTube videos can ask the user to connect the bot to their YouTube account to adapt search results accordingly. To do this, it displays a 'Connect your YouTube account' button above the results, or even before showing any. The user presses the button, switches to a private chat with the bot and, in doing so, passes a start parameter that instructs the bot to return an OAuth link. Once done, the bot can offer a switch_inline button so that the user can easily return to the chat where they wanted to use the bot's inline capabilities. + * + * @param results An Array of results for the inline query + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerinlinequery + */ + answerInlineQuery( + results: readonly InlineQueryResult[], + other?: Other<"answerInlineQuery", "inline_query_id" | "results">, + signal?: AbortSignal, + ) { + return this.api.answerInlineQuery( + orThrow(this.inlineQuery, "answerInlineQuery").id, + results, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.savePreparedInlineMessage`. Stores a message that can be sent by a user of a Mini App. Returns a PreparedInlineMessage object. + * + * @param result An object describing the message to be sent + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#savepreparedinlinemessage + */ + savePreparedInlineMessage( + result: InlineQueryResult, + other?: Other<"savePreparedInlineMessage", "user_id" | "result">, + signal?: AbortSignal, + ) { + return this.api.savePreparedInlineMessage( + orThrow(this.from, "savePreparedInlineMessage").id, + result, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.savePreparedKeyboardButton`. Stores a keyboard button that can be used by a user within a Mini App. Returns a PreparedKeyboardButton object. + * + * @param button An object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#savepreparedkeyboardbutton + */ + savePreparedKeyboardButton( + button: + | KeyboardButton.RequestUsersButton + | KeyboardButton.RequestChatButton + | KeyboardButton.RequestManagedBotButton, + signal?: AbortSignal, + ) { + return this.api.savePreparedKeyboardButton( + orThrow(this.from, "savePreparedKeyboardButton").id, + button, + signal, + ); + } + + /** + * Context-aware alias for `api.sendInvoice`. Use this method to send invoices. On success, the sent Message is returned. + * + * @param title Product name, 1-32 characters + * @param description Product description, 1-255 characters + * @param payload Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + * @param currency Three-letter ISO 4217 currency code, see more on currencies + * @param prices Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendinvoice + */ + replyWithInvoice( + title: string, + description: string, + payload: string, + currency: string, + prices: readonly LabeledPrice[], + other?: Other< + "sendInvoice", + | "chat_id" + | "title" + | "description" + | "payload" + | "currency" + | "prices" + >, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendInvoice( + orThrow(this.chatId, "sendInvoice"), + title, + description, + payload, + currency, + prices, + { + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + direct_messages_topic_id: msg?.direct_messages_topic?.topic_id, + ...other, + }, + signal, + ); + } + + /** + * Context-aware alias for `api.answerShippingQuery`. If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. Use this method to reply to shipping queries. On success, True is returned. + * + * @param shipping_query_id Unique identifier for the query to be answered + * @param ok Pass True if delivery to the specified address is possible and False if there are any problems (for example, if delivery to the specified address is not possible) + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answershippingquery + */ + answerShippingQuery( + ok: boolean, + other?: Other<"answerShippingQuery", "shipping_query_id" | "ok">, + signal?: AbortSignal, + ) { + return this.api.answerShippingQuery( + orThrow(this.shippingQuery, "answerShippingQuery").id, + ok, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.answerPreCheckoutQuery`. Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to respond to such pre-checkout queries. On success, True is returned. Note: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent. + * + * @param ok Specify True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#answerprecheckoutquery + */ + answerPreCheckoutQuery( + ok: boolean, + other?: + | string + | Other<"answerPreCheckoutQuery", "pre_checkout_query_id" | "ok">, + signal?: AbortSignal, + ) { + return this.api.answerPreCheckoutQuery( + orThrow(this.preCheckoutQuery, "answerPreCheckoutQuery").id, + ok, + typeof other === "string" ? { error_message: other } : other, + signal, + ); + } + + /** + * Context-aware alias for `api.refundStarPayment`. Refunds a successful payment in Telegram Stars. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#refundstarpayment + */ + refundStarPayment(signal?: AbortSignal) { + return this.api.refundStarPayment( + orThrow(this.from, "refundStarPayment").id, + orThrow(this.msg?.successful_payment, "refundStarPayment") + .telegram_payment_charge_id, + signal, + ); + } + + /** + * Context-aware alias for `api.editUserStarSubscription`. Allows the bot to cancel or re-enable extension of a subscription paid in Telegram Stars. Returns True on success. + * + * @param telegram_payment_charge_id Telegram payment identifier for the subscription + * @param is_canceled Pass True to cancel extension of the user subscription; the subscription must be active up to the end of the current subscription period. Pass False to allow the user to re-enable a subscription that was previously canceled by the bot. + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#edituserstarsubscription + */ + editUserStarSubscription( + telegram_payment_charge_id: string, + is_canceled: boolean, + signal?: AbortSignal, + ) { + return this.api.editUserStarSubscription( + orThrow(this.from, "editUserStarSubscription").id, + telegram_payment_charge_id, + is_canceled, + signal, + ); + } + + /** + * Context-aware alias for `api.verifyUser`. Verifies a user on behalf of the organization which is represented by the bot. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#verifyuser + */ + verifyUser( + other?: Other<"verifyUser">, + signal?: AbortSignal, + ) { + return this.api.verifyUser( + orThrow(this.from, "verifyUser").id, + other, + signal, + ); + } + + /** + * Context-aware alias for `api.verifyChat`. Verifies a chat on behalf of the organization which is represented by the bot. Returns True on success. + * + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#verifychat + */ + verifyChat( + other?: Other<"verifyChat">, + signal?: AbortSignal, + ) { + return this.api.verifyChat( + orThrow(this.chatId, "verifyChat"), + other, + signal, + ); + } + + /** + * Context-aware alias for `api.removeUserVerification`. Removes verification from a user who is currently verified on behalf of the organization represented by the bot. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removeuserverification + */ + removeUserVerification(signal?: AbortSignal) { + return this.api.removeUserVerification( + orThrow(this.from, "removeUserVerification").id, + signal, + ); + } + + /** + * Context-aware alias for `api.removeChatVerification`. Removes verification from a chat that is currently verified on behalf of the organization represented by the bot. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#removechatverification + */ + removeChatVerification( + signal?: AbortSignal, + ) { + return this.api.removeChatVerification( + orThrow(this.chatId, "removeChatVerification"), + signal, + ); + } + + /** + * Context-aware alias for `api.readBusinessMessage`. Marks incoming message as read on behalf of a business account. Requires the can_read_messages business bot right. Returns True on success. + * + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#readbusinessmessage + */ + readBusinessMessage(signal?: AbortSignal) { + return this.api.readBusinessMessage( + orThrow(this.businessConnectionId, "readBusinessMessage"), + orThrow(this.chatId, "readBusinessMessage"), + orThrow(this.msgId, "readBusinessMessage"), + signal, + ); + } + + /** + * Context-aware alias for `api.setPassportDataErrors`. Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed (the contents of the field for which you returned the error must change). Returns True on success. + * + * Use this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted document is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message to make sure the user knows how to correct the issues. + * + * @param errors An Array describing the errors + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#setpassportdataerrors + */ + setPassportDataErrors( + errors: readonly PassportElementError[], + signal?: AbortSignal, + ) { + return this.api.setPassportDataErrors( + orThrow(this.from, "setPassportDataErrors").id, + errors, + signal, + ); + } + + /** + * Context-aware alias for `api.sendGame`. Use this method to send a game. On success, the sent Message is returned. + * + * @param game_short_name Short name of the game, serves as the unique identifier for the game. Set up your games via BotFather. + * @param other Optional remaining parameters, confer the official reference below + * @param signal Optional `AbortSignal` to cancel the request + * + * **Official reference:** https://core.telegram.org/bots/api#sendgame + */ + replyWithGame( + game_short_name: string, + other?: Other<"sendGame", "chat_id" | "game_short_name">, + signal?: AbortSignal, + ) { + const msg = this.msg; + return this.api.sendGame( + orThrow(this.chatId, "sendGame"), + game_short_name, + { + business_connection_id: this.businessConnectionId, + ...(msg?.is_topic_message + ? { message_thread_id: msg.message_thread_id } + : {}), + ...other, + }, + signal, + ); + } +} + +// === Filtered context types +type HearsContextCore = + & FilterCore<":text" | ":caption"> + & NarrowMatchCore; +/** + * Type of the context object that is available inside the handlers for + * `bot.hears`. + * + * This helper type can be used to narrow down context objects the same way how + * `bot.hears` does it. This allows you to annotate context objects in + * middleware that is not directly passed to `bot.hears`, hence not inferring + * the correct type automatically. That way, handlers can be defined in separate + * files and still have the correct types. + */ +export type HearsContext = Filter< + NarrowMatch, + ":text" | ":caption" +>; + +type CommandContextCore = + & FilterCore<":entities:bot_command"> + & NarrowMatchCore; +/** + * Type of the context object that is available inside the handlers for + * `bot.command`. + * + * This helper type can be used to narrow down context objects the same way how + * `bot.command` does it. This allows you to annotate context objects in + * middleware that is not directly passed to `bot.command`, hence not inferring + * the correct type automatically. That way, handlers can be defined in separate + * files and still have the correct types. + */ +export type CommandContext = Filter< + NarrowMatch, + ":entities:bot_command" +>; +type NarrowMatchCore = { match: T }; +type NarrowMatch = { + [K in keyof C]: K extends "match" ? (T extends C[K] ? T : never) : C[K]; +}; + +type CallbackQueryContextCore = FilterCore<"callback_query:data">; +/** + * Type of the context object that is available inside the handlers for + * `bot.callbackQuery`. + * + * This helper type can be used to annotate narrow down context objects the same + * way `bot.callbackQuery` does it. This allows you to how context objects in + * middleware that is not directly passed to `bot.callbackQuery`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type CallbackQueryContext = Filter< + NarrowMatch, + "callback_query:data" +>; + +type GameQueryContextCore = FilterCore<"callback_query:game_short_name">; +/** + * Type of the context object that is available inside the handlers for + * `bot.gameQuery`. + * + * This helper type can be used to narrow down context objects the same way how + * `bot.gameQuery` does it. This allows you to annotate context objects in + * middleware that is not directly passed to `bot.gameQuery`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type GameQueryContext = Filter< + NarrowMatch, + "callback_query:game_short_name" +>; + +type InlineQueryContextCore = FilterCore<"inline_query">; +/** + * Type of the context object that is available inside the handlers for + * `bot.inlineQuery`. + * + * This helper type can be used to narrow down context objects the same way how + * annotate `bot.inlineQuery` does it. This allows you to context objects in + * middleware that is not directly passed to `bot.inlineQuery`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type InlineQueryContext = Filter< + NarrowMatch, + "inline_query" +>; + +type ReactionContextCore = FilterCore<"message_reaction">; +/** + * Type of the context object that is available inside the handlers for + * `bot.reaction`. + * + * This helper type can be used to narrow down context objects the same way how + * annotate `bot.reaction` does it. This allows you to context objects in + * middleware that is not directly passed to `bot.reaction`, hence not inferring + * the correct type automatically. That way, handlers can be defined in separate + * files and still have the correct types. + */ +export type ReactionContext = Filter; + +type ChosenInlineResultContextCore = FilterCore<"chosen_inline_result">; +/** + * Type of the context object that is available inside the handlers for + * `bot.chosenInlineResult`. + * + * This helper type can be used to narrow down context objects the same way how + * annotate `bot.chosenInlineResult` does it. This allows you to context objects in + * middleware that is not directly passed to `bot.chosenInlineResult`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type ChosenInlineResultContext = Filter< + NarrowMatch, + "chosen_inline_result" +>; + +type PreCheckoutQueryContextCore = FilterCore<"pre_checkout_query">; +/** + * Type of the context object that is available inside the handlers for + * `bot.preCheckoutQuery`. + * + * This helper type can be used to narrow down context objects the same way how + * annotate `bot.preCheckoutQuery` does it. This allows you to context objects in + * middleware that is not directly passed to `bot.preCheckoutQuery`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type PreCheckoutQueryContext = Filter< + NarrowMatch, + "pre_checkout_query" +>; + +type ShippingQueryContextCore = FilterCore<"shipping_query">; +/** + * Type of the context object that is available inside the handlers for + * `bot.shippingQuery`. + * + * This helper type can be used to narrow down context objects the same way how + * annotate `bot.shippingQuery` does it. This allows you to context objects in + * middleware that is not directly passed to `bot.shippingQuery`, hence not + * inferring the correct type automatically. That way, handlers can be defined + * in separate files and still have the correct types. + */ +export type ShippingQueryContext = Filter< + NarrowMatch, + "shipping_query" +>; + +type ChatTypeContextCore = T extends unknown ? + & Record<"update", ChatTypeUpdate> // ctx.update + & ChatType // ctx.chat + & Record<"chatId", number> // ctx.chatId + & ChatFrom // ctx.from + & ChatTypeRecord<"msg", T> // ctx.msg + & AliasProps> // ctx.message etc + : never; +/** + * Type of the context object that is available inside the handlers for + * `bot.chatType`. + * + * This helper type can be used to narrow down context objects the same way how + * `bot.chatType` does it. This allows you to annotate context objects in + * middleware that is not directly passed to `bot.chatType`, hence not inferring + * the correct type automatically. That way, handlers can be defined in separate + * files and still have the correct types. + */ +export type ChatTypeContext = + T extends unknown ? C & ChatTypeContextCore : never; +type ChatTypeUpdate = + & ChatTypeRecord< + | "message" + | "edited_message" + | "channel_post" + | "edited_channel_post" + | "my_chat_member" + | "chat_member" + | "chat_join_request", + T + > + & Partial>> + & ConstrainUpdatesByChatType; +type ConstrainUpdatesByChatType = Record< + [T] extends ["channel"] ? "message" | "edited_message" + : "channel_post" | "edited_channel_post", + undefined +>; + +type ChatTypeRecord = Partial< + Record> +>; +interface ChatType { + chat: { type: T }; +} +interface ChatFrom { + // deno-lint-ignore ban-types + from: [T] extends ["private"] ? {} : unknown; +} + +// === Util functions +function orThrow(value: T | undefined, method: string): T { + if (value === undefined) { + throw new Error(`Missing information for API call to ${method}`); + } + return value; +} + +function triggerFn(trigger: MaybeArray) { + return toArray(trigger).map((t) => + typeof t === "string" + ? (txt: string) => (txt === t ? t : null) + : (txt: string) => txt.match(t) + ); +} + +function match( + ctx: C, + content: string, + triggers: Array<(content: string) => string | RegExpMatchArray | null>, +): boolean { + for (const t of triggers) { + const res = t(content); + if (res) { + ctx.match = res; + return true; + } + } + return false; +} +function toArray(e: MaybeArray): E[] { + return Array.isArray(e) ? e : [e]; +} diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..4db9fc1 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,106 @@ +import { type ApiError, type ResponseParameters } from "../types.ts"; +import { debug as d } from "../platform.deno.ts"; +const debug = d("grammy:warn"); + +/** + * This class represents errors that are thrown by grammY because the Telegram + * Bot API responded with an error. + * + * Instances of this class hold the information that the Telegram backend + * returned. + * + * If this error is thrown, grammY could successfully communicate with the + * Telegram Bot API servers, however, an error code was returned for the + * respective method call. + */ +export class GrammyError extends Error implements ApiError { + /** Flag that this request was unsuccessful. Always `false`. */ + public readonly ok: false = false; + /** An integer holding Telegram's error code. Subject to change. */ + public readonly error_code: number; + /** A human-readable description of the error. */ + public readonly description: string; + /** Further parameters that may help to automatically handle the error. */ + public readonly parameters: ResponseParameters; + constructor( + message: string, + err: ApiError, + /** The called method name which caused this error to be thrown. */ + public readonly method: string, + /** The payload that was passed when calling the method. */ + public readonly payload: Record, + ) { + super(`${message} (${err.error_code}: ${err.description})`); + this.name = "GrammyError"; + this.error_code = err.error_code; + this.description = err.description; + this.parameters = err.parameters ?? {}; + } +} +export function toGrammyError( + err: ApiError, + method: string, + payload: Record, +) { + switch (err.error_code) { + case 401: + debug( + "Error 401 means that your bot token is wrong, talk to https://t.me/BotFather to check it.", + ); + break; + case 409: + debug( + "Error 409 means that you are running your bot several times on long polling. Consider revoking the bot token if you believe that no other instance is running.", + ); + break; + } + return new GrammyError( + `Call to '${method}' failed!`, + err, + method, + payload, + ); +} + +/** + * This class represents errors that are thrown by grammY because an HTTP call + * to the Telegram Bot API failed. + * + * Instances of this class hold the error object that was created because the + * fetch call failed. It can be inspected to determine why exactly the network + * request failed. + * + * If an [API transformer + * function](https://grammy.dev/advanced/transformers) throws an error, + * grammY will regard this as if the network request failed. The contained error + * will then be the error that was thrown by the transformer function. + */ +export class HttpError extends Error { + constructor( + message: string, + /** The thrown error object. */ + public readonly error: unknown, + ) { + super(message); + this.name = "HttpError"; + } +} + +function isTelegramError( + err: unknown, +): err is { status: number; statusText: string } { + return ( + typeof err === "object" && err !== null && + "status" in err && "statusText" in err + ); +} +export function toHttpError( + method: string, + sensitiveLogs: boolean, + err: unknown, +) { + let msg = `Network request for '${method}' failed!`; + if (isTelegramError(err)) msg += ` (${err.status}: ${err.statusText})`; + if (sensitiveLogs && err instanceof Error) msg += ` ${err.message}`; + return new HttpError(msg, err); +} diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..3788190 --- /dev/null +++ b/src/filter.ts @@ -0,0 +1,797 @@ +// deno-lint-ignore-file camelcase no-explicit-any +import { type Context } from "./context.ts"; +import { type Message, type MessageEntity, type Update } from "./types.ts"; + +type FilterFunction = (ctx: C) => ctx is D; + +const filterQueryCache = new Map boolean>(); + +// === Obtain O(1) filter function from query +/** + * > This is an advanced function of grammY. + * + * Takes a filter query and turns it into a predicate function that can check in + * constant time whether a given context object satisfies the query. The created + * predicate can be passed to `bot.filter` and will narrow down the context + * accordingly. + * + * This function is used internally by `bot.on` but exposed for advanced usage + * like the following. + * ```ts + * // Listens for updates except forwards of messages or channel posts + * bot.drop(matchFilter(':forward_origin'), ctx => { ... }) + * ``` + * + * Check out the + * [documentation](https://grammy.dev/ref/core/composer#on) + * of `bot.on` for examples. In addition, the + * [website](https://grammy.dev/guide/filter-queries) contains more + * information about how filter queries work in grammY. + * + * @param filter A filter query or an array of filter queries + */ +export function matchFilter( + filter: Q | Q[], +): FilterFunction> { + const queries = Array.isArray(filter) ? filter : [filter]; + const key = queries.join(","); + const predicate = filterQueryCache.get(key) ?? (() => { + const parsed = parse(queries); + const pred = compile(parsed); + filterQueryCache.set(key, pred); + return pred; + })(); + return (ctx: C): ctx is Filter => predicate(ctx); +} + +export function parse(filter: FilterQuery | FilterQuery[]): string[][] { + return Array.isArray(filter) + ? filter.map((q) => q.split(":")) + : [filter.split(":")]; +} + +function compile(parsed: string[][]): (ctx: Context) => boolean { + const preprocessed = parsed.flatMap((q) => check(q, preprocess(q))); + const ltree = treeify(preprocessed); + const predicate = arborist(ltree); // arborists check trees + return (ctx) => !!predicate(ctx.update, ctx); +} + +export function preprocess(filter: string[]): string[][] { + const valid: any = UPDATE_KEYS; + const expanded = [filter] + // expand L1 + .flatMap((q) => { + const [l1, l2, l3] = q; + // only expand if shortcut is given + if (!(l1 in L1_SHORTCUTS)) return [q]; + // only expand for at least one non-empty part + if (!l1 && !l2 && !l3) return [q]; + // perform actual expansion + const targets = L1_SHORTCUTS[l1 as L1Shortcuts]; + const expanded = targets.map((s) => [s, l2, l3]); + // assume that bare L1 expansions are always correct + if (l2 === undefined) return expanded; + // only filter out invalid expansions if we don't do this later + if (l2 in L2_SHORTCUTS && (l2 || l3)) return expanded; + // filter out invalid expansions, e.g. `channel_post:new_chat_member` for empty L1 + return expanded.filter(([s]) => !!valid[s]?.[l2]); + }) + // expand L2 + .flatMap((q) => { + const [l1, l2, l3] = q; + // only expand if shortcut is given + if (!(l2 in L2_SHORTCUTS)) return [q]; + // only expand for at least one non-empty part + if (!l2 && !l3) return [q]; + // perform actual expansion + const targets = L2_SHORTCUTS[l2 as L2Shortcuts]; + const expanded = targets.map((s) => [l1, s, l3]); + // assume that bare L2 expansions are always correct + if (l3 === undefined) return expanded; + // filter out invalid expansions + return expanded.filter(([, s]) => !!valid[l1]?.[s]?.[l3]); + }); + if (expanded.length === 0) { + throw new Error( + `Shortcuts in '${ + filter.join(":") + }' do not expand to any valid filter query`, + ); + } + return expanded; +} + +function check(original: string[], preprocessed: string[][]): string[][] { + if (preprocessed.length === 0) throw new Error("Empty filter query given"); + const errors = preprocessed + .map(checkOne) + .filter((r): r is string => r !== true); + if (errors.length === 0) return preprocessed; + else if (errors.length === 1) throw new Error(errors[0]); + else { + throw new Error( + `Invalid filter query '${ + original.join(":") + }'. There are ${errors.length} errors after expanding the contained shortcuts: ${ + errors.join("; ") + }`, + ); + } +} +function checkOne(filter: string[]): string | true { + const [l1, l2, l3, ...n] = filter; + if (l1 === undefined) return "Empty filter query given"; + if (!(l1 in UPDATE_KEYS)) { + const permitted = Object.keys(UPDATE_KEYS); + return `Invalid L1 filter '${l1}' given in '${filter.join(":")}'. \ +Permitted values are: ${permitted.map((k) => `'${k}'`).join(", ")}.`; + } + if (l2 === undefined) return true; + const l1Obj: any = UPDATE_KEYS[l1 as keyof S]; + if (!(l2 in l1Obj)) { + const permitted = Object.keys(l1Obj); + return `Invalid L2 filter '${l2}' given in '${filter.join(":")}'. \ +Permitted values are: ${permitted.map((k) => `'${k}'`).join(", ")}.`; + } + if (l3 === undefined) return true; + const l2Obj = l1Obj[l2]; + if (!(l3 in l2Obj)) { + const permitted = Object.keys(l2Obj); + return `Invalid L3 filter '${l3}' given in '${filter.join(":")}'. ${ + permitted.length === 0 + ? `No further filtering is possible after '${l1}:${l2}'.` + : `Permitted values are: ${ + permitted.map((k) => `'${k}'`).join(", ") + }.` + }`; + } + if (n.length === 0) return true; + return `Cannot filter further than three levels, ':${ + n.join(":") + }' is invalid!`; +} +interface LTree { + [l1: string]: { [l2: string]: Set }; +} +function treeify(paths: string[][]): LTree { + const tree: LTree = {}; + for (const [l1, l2, l3] of paths) { + const subtree = (tree[l1] ??= {}); + if (l2 !== undefined) { + const set = (subtree[l2] ??= new Set()); + if (l3 !== undefined) set.add(l3); + } + } + return tree; +} + +type Pred = (obj: any, ctx: Context) => boolean; +function or(left: Pred, right: Pred): Pred { + return (obj, ctx) => left(obj, ctx) || right(obj, ctx); +} +function concat(get: Pred, test: Pred): Pred { + return (obj, ctx) => { + const nextObj = get(obj, ctx); + return nextObj && test(nextObj, ctx); + }; +} +function leaf(pred: Pred): Pred { + return (obj, ctx) => pred(obj, ctx) != null; +} +function arborist(tree: LTree): Pred { + const l1Predicates = Object.entries(tree).map(([l1, subtree]) => { + const l1Pred: Pred = (obj) => obj[l1]; + const l2Predicates = Object.entries(subtree).map(([l2, set]) => { + const l2Pred: Pred = (obj) => obj[l2]; + const l3Predicates = Array.from(set).map((l3) => { + const l3Pred: Pred = l3 === "me" // special handling for `me` shortcut + ? (obj, ctx) => { + const me = ctx.me.id; + return testMaybeArray(obj, (u) => u.id === me); + } + : (obj) => + testMaybeArray(obj, (e) => e[l3] || e.type === l3); + return l3Pred; + }); + return l3Predicates.length === 0 + ? leaf(l2Pred) + : concat(l2Pred, l3Predicates.reduce(or)); + }); + return l2Predicates.length === 0 + ? leaf(l1Pred) + : concat(l1Pred, l2Predicates.reduce(or)); + }); + if (l1Predicates.length === 0) { + throw new Error("Cannot create filter function for empty query"); + } + return l1Predicates.reduce(or); +} + +function testMaybeArray(t: T | T[], pred: (t: T) => boolean): boolean { + const p = (x: T) => x != null && pred(x); + return Array.isArray(t) ? t.some(p) : p(t); +} + +// === Define a structure to validate the queries +type NestedObj = Record>>; // for validation only +// L3 +const ENTITY_KEYS = { + mention: {}, + hashtag: {}, + cashtag: {}, + bot_command: {}, + url: {}, + email: {}, + phone_number: {}, + bold: {}, + italic: {}, + underline: {}, + strikethrough: {}, + spoiler: {}, + blockquote: {}, + expandable_blockquote: {}, + code: {}, + pre: {}, + text_link: {}, + text_mention: {}, + custom_emoji: {}, + date_time: {}, +} as const satisfies Record; +const USER_KEYS = { + me: {}, + is_bot: {}, + is_premium: {}, + added_to_attachment_menu: {}, +} as const; +const FORWARD_ORIGIN_KEYS = { + user: {}, + hidden_user: {}, + chat: {}, + channel: {}, +} as const; +const STICKER_KEYS = { + is_video: {}, + is_animated: {}, + premium_animation: {}, +} as const; +const REACTION_KEYS = { + emoji: {}, + custom_emoji: {}, + paid: {}, +} as const; +const GIFT_INFO_KEYS = { + can_be_upgraded: {}, + is_upgrade_separate: {}, + is_private: {}, +}; + +// L2 +const COMMON_MESSAGE_KEYS = { + forward_origin: FORWARD_ORIGIN_KEYS, + is_topic_message: {}, + is_automatic_forward: {}, + guest_query_id: {}, + business_connection_id: {}, + + text: {}, + rich_message: {}, + animation: {}, + audio: {}, + document: {}, + live_photo: {}, + paid_media: {}, + photo: {}, + sticker: STICKER_KEYS, + story: {}, + video: {}, + video_note: {}, + voice: {}, + contact: {}, + dice: {}, + game: {}, + poll: {}, + venue: {}, + location: {}, + + entities: ENTITY_KEYS, + caption_entities: ENTITY_KEYS, + caption: {}, + + link_preview_options: { + url: {}, + prefer_small_media: {}, + prefer_large_media: {}, + show_above_text: {}, + }, + effect_id: {}, + paid_star_count: {}, + has_media_spoiler: {}, + + new_chat_title: {}, + new_chat_photo: {}, + delete_chat_photo: {}, + message_auto_delete_timer_changed: {}, + pinned_message: {}, + invoice: {}, + proximity_alert_triggered: {}, + chat_background_set: {}, + giveaway_created: {}, + giveaway: { only_new_members: {}, has_public_winners: {} }, + giveaway_winners: { only_new_members: {}, was_refunded: {} }, + giveaway_completed: {}, + gift: GIFT_INFO_KEYS, + gift_upgrade_sent: GIFT_INFO_KEYS, + unique_gift: { transfer_star_count: {} }, + paid_message_price_changed: {}, + video_chat_scheduled: {}, + video_chat_started: {}, + video_chat_ended: {}, + video_chat_participants_invited: {}, + web_app_data: {}, +} as const; +const MESSAGE_KEYS = { + ...COMMON_MESSAGE_KEYS, + + direct_messages_topic: {}, + + chat_owner_left: { new_owner: {} }, + chat_owner_changed: {}, + new_chat_members: USER_KEYS, + left_chat_member: USER_KEYS, + group_chat_created: {}, + supergroup_chat_created: {}, + migrate_to_chat_id: {}, + migrate_from_chat_id: {}, + successful_payment: {}, + refunded_payment: {}, + users_shared: {}, + chat_shared: {}, + connected_website: {}, + managed_bot_created: {}, + write_access_allowed: {}, + passport_data: {}, + boost_added: {}, + forum_topic_created: { is_name_implicit: {} }, + forum_topic_edited: { name: {}, icon_custom_emoji_id: {} }, + forum_topic_closed: {}, + forum_topic_reopened: {}, + general_forum_topic_hidden: {}, + general_forum_topic_unhidden: {}, + + checklist: { others_can_add_tasks: {}, others_can_mark_tasks_as_done: {} }, + checklist_tasks_done: {}, + checklist_tasks_added: {}, + community_chat_added: {}, + community_chat_removed: {}, + poll_option_added: {}, + poll_option_deleted: {}, + + suggested_post_info: {}, + suggested_post_approved: {}, + suggested_post_approval_failed: {}, + suggested_post_declined: {}, + suggested_post_paid: {}, + suggested_post_refunded: {}, + + sender_boost_count: {}, +} as const satisfies Partial>; +const CHANNEL_POST_KEYS = { + ...COMMON_MESSAGE_KEYS, + channel_chat_created: {}, + direct_message_price_changed: {}, + is_paid_post: {}, +} as const satisfies Partial>; +const BUSINESS_CONNECTION_KEYS = { + can_reply: {}, + is_enabled: {}, +} as const; +const MESSAGE_REACTION_KEYS = { + old_reaction: REACTION_KEYS, + new_reaction: REACTION_KEYS, +} as const; +const MESSAGE_REACTION_COUNT_UPDATED_KEYS = { + reactions: REACTION_KEYS, +} as const; +const CALLBACK_QUERY_KEYS = { data: {}, game_short_name: {} } as const; +const CHAT_MEMBER_UPDATED_KEYS = { from: USER_KEYS } as const; + +// L1 +const UPDATE_KEYS = { + message: MESSAGE_KEYS, + edited_message: MESSAGE_KEYS, + channel_post: CHANNEL_POST_KEYS, + edited_channel_post: CHANNEL_POST_KEYS, + business_connection: BUSINESS_CONNECTION_KEYS, + business_message: MESSAGE_KEYS, + edited_business_message: MESSAGE_KEYS, + deleted_business_messages: {}, + guest_message: MESSAGE_KEYS, + inline_query: {}, + chosen_inline_result: {}, + callback_query: CALLBACK_QUERY_KEYS, + shipping_query: {}, + pre_checkout_query: {}, + poll: {}, + poll_answer: {}, + my_chat_member: CHAT_MEMBER_UPDATED_KEYS, + chat_member: CHAT_MEMBER_UPDATED_KEYS, + managed_bot: {}, + chat_join_request: {}, + message_reaction: MESSAGE_REACTION_KEYS, + message_reaction_count: MESSAGE_REACTION_COUNT_UPDATED_KEYS, + chat_boost: {}, + removed_chat_boost: {}, + purchased_paid_media: {}, + subscription: { state: { canceled: {}, active: {}, failed: {} } }, +} as const satisfies Record, NestedObj>; + +// === Build up all possible filter queries from the above validation structure +type KeyOf = string & keyof T; // Emulate `keyofStringsOnly` + +// Suggestion building base structure +type S = typeof UPDATE_KEYS; + +// E.g. 'message' suggestions +type L1S = KeyOf; +// E.g. 'message:entities' suggestions +type L2S = L1 extends unknown ? `${L1}:${KeyOf}` + : never; +// E.g. 'message:entities:url' suggestions +type L3S = L1 extends unknown ? L3S_ : never; +type L3S_< + L1 extends L1S, + L2 extends KeyOf = KeyOf, +> = L2 extends unknown ? `${L1}:${L2}:${KeyOf}` : never; +// Suggestions for all three combined +type L123 = L1S | L2S | L3S; +// E.g. 'message::url' generation +type InjectShortcuts = Q extends + `${infer L1}:${infer L2}:${infer L3}` + ? `${CollapseL1}:${CollapseL2}:${L3}` + : Q extends `${infer L1}:${infer L2}` + ? `${CollapseL1}:${CollapseL2}` + : CollapseL1; +// Add L1 shortcuts +type CollapseL1< + Q extends string, + L extends L1Shortcuts = Exclude, +> = + | Q + | (L extends string ? Q extends typeof L1_SHORTCUTS[L][number] ? L + : never + : never); +// Add L2 shortcuts +type CollapseL2< + Q extends string, + L extends L2Shortcuts = Exclude, +> = + | Q + | (L extends string ? Q extends typeof L2_SHORTCUTS[L][number] ? L + : never + : never); +// All queries +type ComputeFilterQueryList = InjectShortcuts; + +/** + * Represents a filter query that can be passed to `bot.on`. There are three + * different kinds of filter queries: Level 1, Level 2, and Level 3. Check out + * the [website](https://grammy.dev/guide/filter-queries) to read about how + * filter queries work in grammY, and how to use them. + * + * Here are three brief examples: + * ```ts + * // Listen for messages of any type (Level 1) + * bot.on('message', ctx => { ... }) + * // Listen for audio messages only (Level 2) + * bot.on('message:audio', ctx => { ... }) + * // Listen for text messages that have a URL entity (Level 3) + * bot.on('message:entities:url', ctx => { ... }) + * ``` + */ +export type FilterQuery = ComputeFilterQueryList; + +// === Infer the present/absent properties on a context object based on a query +// Note: L3 filters are not represented in types + +/** + * Any kind of value that appears in the Telegram Bot API. When intersected with + * an optional field, it effectively removes `| undefined`. + */ +// deno-lint-ignore ban-types +type NotUndefined = {}; + +/** + * Given a FilterQuery, returns an object that, when intersected with an Update, + * marks those properties as required that are guaranteed to exist. + */ +type RunQuery = L1Discriminator>; + +// gets all L1 query snippets +type L1Parts = Q extends `${infer L1}:${string}` ? L1 : Q; +// gets all L2 query snippets for the given L1 part, or `never` +type L2Parts< + Q extends string, + L1 extends string, +> = Q extends `${L1}:${infer L2}:${string}` ? L2 + : Q extends `${L1}:${infer L2}` ? L2 + : never; + +// build up all combinations of all L1 fields +type L1Discriminator = Combine< + L1Fragment, + L1 +>; +// maps each L1 part of the filter query to an object +type L1Fragment = L1 extends unknown + ? Record>> + : never; + +// build up all combinations of all L2 fields +type L2Discriminator = [L2] extends + [never] ? L2ShallowFragment // short-circuit L1 queries (L2 is never), only add twins + : Combine, L2>; +// maps each L2 part of the filter query to an object and handles siblings +type L2Fragment = L2 extends unknown + ? Record, NotUndefined> + : never; +// does the same as L1Fragment but without combining L2 properties +type L2ShallowFragment = Record< + AddTwins, + NotUndefined +>; + +// define additional fields on U with value `undefined` +type Combine = U extends unknown + ? U & Partial, undefined>> + : never; + +/** + * This type infers which properties will be present on the given context object + * provided it matches the given filter query. If the filter query is a union + * type, the produced context object will be a union of possible combinations, + * hence allowing you to narrow down manually which of the properties are + * present. + * + * In some sense, this type computes `matchFilter` on the type level. + */ +export type Filter = PerformQuery< + C, + RunQuery> +>; +// same as Filter but stop before intersecting with Context +export type FilterCore = PerformQueryCore< + RunQuery> +>; + +// apply a query result by intersecting it with Update, and then injecting into C +type PerformQuery = U extends unknown + ? FilteredContext + : never; +type PerformQueryCore = U extends unknown + ? FilteredContextCore + : never; + +// set the given update into a given context object, and adjust the aliases +type FilteredContext = + & C + & FilteredContextCore; + +// generate a structure with all aliases for a narrowed update +type FilteredContextCore = + & Record<"update", U> + & Shortcuts; + +// helper type to infer shortcuts on context object based on present properties, +// must be in sync with shortcut impl! +interface Shortcuts { + message: [U["message"]] extends [object] ? U["message"] : undefined; + editedMessage: [U["edited_message"]] extends [object] ? U["edited_message"] + : undefined; + channelPost: [U["channel_post"]] extends [object] ? U["channel_post"] + : undefined; + editedChannelPost: [U["edited_channel_post"]] extends [object] + ? U["edited_channel_post"] + : undefined; + businessConnection: [U["business_connection"]] extends [object] + ? U["business_connection"] + : undefined; + businessMessage: [U["business_message"]] extends [object] + ? U["business_message"] + : undefined; + editedBusinessMessage: [U["edited_business_message"]] extends [object] + ? U["edited_business_message"] + : undefined; + deletedBusinessMessages: [U["deleted_business_messages"]] extends [object] + ? U["deleted_business_messages"] + : undefined; + guestMessage: [U["guest_message"]] extends [object] ? U["guest_message"] + : undefined; + messageReaction: [U["message_reaction"]] extends [object] + ? U["message_reaction"] + : undefined; + messageReactionCount: [U["message_reaction_count"]] extends [object] + ? U["message_reaction_count"] + : undefined; + inlineQuery: [U["inline_query"]] extends [object] ? U["inline_query"] + : undefined; + chosenInlineResult: [U["chosen_inline_result"]] extends [object] + ? U["chosen_inline_result"] + : undefined; + callbackQuery: [U["callback_query"]] extends [object] ? U["callback_query"] + : undefined; + shippingQuery: [U["shipping_query"]] extends [object] ? U["shipping_query"] + : undefined; + preCheckoutQuery: [U["pre_checkout_query"]] extends [object] + ? U["pre_checkout_query"] + : undefined; + poll: [U["poll"]] extends [object] ? U["poll"] : undefined; + pollAnswer: [U["poll_answer"]] extends [object] ? U["poll_answer"] + : undefined; + myChatMember: [U["my_chat_member"]] extends [object] ? U["my_chat_member"] + : undefined; + chatMember: [U["chat_member"]] extends [object] ? U["chat_member"] + : undefined; + managedBot: [U["managed_bot"]] extends [object] ? U["managed_bot"] + : undefined; + chatJoinRequest: [U["chat_join_request"]] extends [object] + ? U["chat_join_request"] + : undefined; + chatBoost: [U["chat_boost"]] extends [object] ? U["chat_boost"] : undefined; + removedChatBoost: [U["removed_chat_boost"]] extends [object] + ? U["removed_chat_boost"] + : undefined; + purchasedPaidMedia: [U["purchased_paid_media"]] extends [object] + ? U["purchased_paid_media"] + : undefined; + subscription: [U["subscription"]] extends [object] ? U["subscription"] + : undefined; + msg: [U["message"]] extends [object] ? U["message"] + : [U["edited_message"]] extends [object] ? U["edited_message"] + : [U["channel_post"]] extends [object] ? U["channel_post"] + : [U["edited_channel_post"]] extends [object] ? U["edited_channel_post"] + : [U["business_message"]] extends [object] ? U["business_message"] + : [U["edited_business_message"]] extends [object] + ? U["edited_business_message"] + : [U["guest_message"]] extends [object] ? U["guest_message"] + : [U["callback_query"]] extends [object] + ? U["callback_query"]["message"] + : undefined; + chat: [U["callback_query"]] extends [object] + ? NonNullable["chat"] | undefined + : [Shortcuts["msg"]] extends [object] ? Shortcuts["msg"]["chat"] + : [U["deleted_business_messages"]] extends [object] + ? U["deleted_business_messages"]["chat"] + : [U["message_reaction"]] extends [object] + ? U["message_reaction"]["chat"] + : [U["message_reaction_count"]] extends [object] + ? U["message_reaction_count"]["chat"] + : [U["my_chat_member"]] extends [object] ? U["my_chat_member"]["chat"] + : [U["chat_member"]] extends [object] ? U["chat_member"]["chat"] + : [U["chat_join_request"]] extends [object] + ? U["chat_join_request"]["chat"] + : [U["chat_boost"]] extends [object] ? U["chat_boost"]["chat"] + : [U["removed_chat_boost"]] extends [object] + ? U["removed_chat_boost"]["chat"] + : undefined; + senderChat: [Shortcuts["msg"]] extends [object] + ? Shortcuts["msg"]["sender_chat"] + : undefined; + from: [U["business_connection"]] extends [object] + ? U["business_connection"]["user"] + : [U["message_reaction"]] extends [object] + ? U["message_reaction"]["user"] + : [U["managed_bot"]] extends [object] ? U["managed_bot"]["user"] + : [U["chat_boost"]] extends [object] + ? U["chat_boost"]["boost"]["source"]["user"] + : [U["removed_chat_boost"]] extends [object] + ? U["removed_chat_boost"]["source"]["user"] + : [U["subscription"]] extends [object] ? U["subscription"]["user"] + : [U["callback_query"]] extends [object] ? U["callback_query"]["from"] + : [Shortcuts["msg"]] extends [object] ? Shortcuts["msg"]["from"] + : [U["inline_query"]] extends [object] ? U["inline_query"]["from"] + : [U["chosen_inline_result"]] extends [object] + ? U["chosen_inline_result"]["from"] + : [U["shipping_query"]] extends [object] ? U["shipping_query"]["from"] + : [U["pre_checkout_query"]] extends [object] + ? U["pre_checkout_query"]["from"] + : [U["my_chat_member"]] extends [object] ? U["my_chat_member"]["from"] + : [U["chat_member"]] extends [object] ? U["chat_member"]["from"] + : [U["chat_join_request"]] extends [object] + ? U["chat_join_request"]["from"] + : undefined; + msgId: [U["callback_query"]] extends [object] ? number | undefined + : [Shortcuts["msg"]] extends [object] ? number + : [U["message_reaction"]] extends [object] ? number + : [U["message_reaction_count"]] extends [object] ? number + : undefined; + chatId: [U["callback_query"]] extends [object] ? number | undefined + : [Shortcuts["chat"]] extends [object] ? number + : [U["business_connection"]] extends [object] ? number + : undefined; + // inlineMessageId: disregarded here because always optional on both types + businessConnectionId: [U["callback_query"]] extends [object] + ? string | undefined + : [Shortcuts["msg"]] extends [object] ? string | undefined + : [U["business_connection"]] extends [object] ? string + : [U["deleted_business_messages"]] extends [object] ? string + : undefined; +} + +// === Define some helpers for handling shortcuts, e.g. in 'edit:photo' +const L1_SHORTCUTS = { + "": ["message", "channel_post"], + msg: ["message", "channel_post"], + edit: ["edited_message", "edited_channel_post"], +} as const; +const L2_SHORTCUTS = { + "": ["entities", "caption_entities"], + media: ["photo", "live_photo", "video"], + file: [ + "photo", + "live_photo", + "animation", + "audio", + "document", + "video", + "video_note", + "voice", + "sticker", + ], +} as const; +type L1Shortcuts = KeyOf; +type L2Shortcuts = KeyOf; + +type ExpandShortcuts = Q extends + `${infer L1}:${infer L2}:${infer L3}` + ? `${ExpandL1}:${ExpandL2}:${L3}` + : Q extends `${infer L1}:${infer L2}` ? `${ExpandL1}:${ExpandL2}` + : ExpandL1; +type ExpandL1 = S extends L1Shortcuts + ? typeof L1_SHORTCUTS[S][number] + : S; +type ExpandL2 = S extends L2Shortcuts + ? typeof L2_SHORTCUTS[S][number] + : S; + +// === Define some helpers for when one property implies the existence of others + +// merges twins based on L1 with those based on L1 and L2 +type AddTwins = + | TwinsFromL1 + | TwinsFromL2; + +// yields twins based on a given L1 property +type TwinsFromL1 = L1 extends + KeyOf ? L1Equivalents[L1] + : L2; +type L1Equivalents = { + message: "from"; + edited_message: "from" | "edit_date"; + channel_post: "sender_chat"; + edited_channel_post: "sender_chat" | "edit_date"; + business_message: "from"; + edited_business_message: "from" | "edit_date"; +}; + +// yields twins based on given L1 and L2 properties +type TwinsFromL2 = L1 extends + KeyOf + ? L2 extends KeyOf ? L2Equivalents[L1][L2] : L2 + : L2; +type L2Equivalents = { + message: MessageEquivalents; + edited_message: MessageEquivalents; + channel_post: MessageEquivalents; + edited_channel_post: MessageEquivalents; + business_message: MessageEquivalents; + edited_business_message: MessageEquivalents; + guest_message: MessageEquivalents; +}; +type MessageEquivalents = { + live_photo: "photo"; + animation: "document"; + entities: "text"; + caption_entities: "caption"; + is_topic_message: "message_thread_id"; +}; diff --git a/src/frameworks.ts b/src/frameworks.ts new file mode 100644 index 0000000..7b03331 --- /dev/null +++ b/src/frameworks.ts @@ -0,0 +1,660 @@ +import { type Update } from "../types.ts"; + +const SECRET_HEADER = "X-Telegram-Bot-Api-Secret-Token"; +const SECRET_HEADER_LOWERCASE = SECRET_HEADER.toLowerCase(); +const WRONG_TOKEN_ERROR = "secret token is wrong"; + +const ok = () => new Response(null, { status: 200 }); +const okJson = (json: string) => + new Response(json, { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +const unauthorized = () => + new Response('"unauthorized"', { + status: 401, + statusText: WRONG_TOKEN_ERROR, + }); + +type MaybePromise = T | Promise; + +/** + * Abstraction over a request-response cycle, providing access to the update, as + * well as a mechanism for responding to the request and to end it. + */ +export interface ReqResHandler { + /** + * The update object sent from Telegram, usually resolves the request's JSON + * body + */ + update: MaybePromise; + /** + * X-Telegram-Bot-Api-Secret-Token header of the request, or undefined if + * not present + */ + header?: string; + /** + * Ends the request immediately without body, called after every request + * unless a webhook reply was performed + */ + end?: () => void; + /** + * Sends the specified JSON as a payload in the body, used for webhook + * replies + */ + respond: (json: string) => unknown | Promise; + /** + * Responds that the request is unauthorized due to mismatching + * X-Telegram-Bot-Api-Secret-Token headers + */ + unauthorized: () => unknown | Promise; + /** + * Some frameworks (e.g. Deno's std/http `listenAndServe`) assume that + * handler returns something + */ + handlerReturn?: Promise; +} + +/** + * Middleware for a web framework. Creates a request-response handler for a + * request. The handler will be used to integrate with the compatible framework. + */ +// deno-lint-ignore no-explicit-any +export type FrameworkAdapter = (...args: any[]) => ReqResHandler; + +export type LambdaAdapter = ( + event: { + body?: string; + headers: Record; + }, + _context: unknown, + callback: ( + arg0: unknown, + arg1: Record, + ) => Promise, +) => ReqResHandler; + +export type LambdaAsyncAdapter = ( + event: { + body?: string; + headers: Record; + }, + _context: unknown, +) => ReqResHandler; + +export type AzureAdapter = (context: { + res?: { + // deno-lint-ignore no-explicit-any + [key: string]: any; + }; +}, request: { + body?: unknown; + headers?: Record; +}) => ReqResHandler; +export type AzureAdapterV4 = ( + request: { + headers: { get(name: string): string | null }; + json(): Promise; + }, +) => ReqResHandler<{ status: number; body?: string } | { jsonBody: string }>; + +export type BunAdapter = (request: { + headers: Headers; + json: () => Promise; +}) => ReqResHandler; + +export type CloudflareAdapter = (event: { + request: Body & { + method: string; + url: string; + headers: Headers; + }; + respondWith: (response: Promise) => void; +}) => ReqResHandler; + +export type CloudflareModuleAdapter = ( + request: Body & { + method: string; + url: string; + headers: Headers; + }, +) => ReqResHandler; + +export type ElysiaAdapter = (ctx: { + body: unknown; + headers: Record; + set: { + headers: Record; + status?: string | number; + }; +}) => ReqResHandler; + +export type ExpressAdapter = (req: { + body: Update; + header: (header: string) => string | undefined; +}, res: { + end: (cb?: () => void) => typeof res; + set: (field: string, value?: string | string[]) => typeof res; + send: (json: string) => typeof res; + status: (code: number) => typeof res; +}) => ReqResHandler; + +export type FastifyAdapter = (request: { + body: unknown; + // deno-lint-ignore no-explicit-any + headers: any; +}, reply: { + status: (code: number) => typeof reply; + headers: (headers: Record) => typeof reply; + code: (code: number) => typeof reply; + send: { + (): typeof reply; + (json: string): typeof reply; + }; +}) => ReqResHandler; + +export type HonoAdapter = (c: { + req: { + json: () => Promise; + header: (header: string) => string | undefined; + }; + body(data: string): Response; + body(data: null, status: 204): Response; + // deno-lint-ignore no-explicit-any + status: (status: any) => void; + json: (json: string) => Response; +}) => ReqResHandler; + +export type HttpAdapter = (req: { + headers: Record; + on: (event: string, listener: (chunk: unknown) => void) => typeof req; + once: (event: string, listener: () => void) => typeof req; +}, res: { + writeHead: { + (status: number): typeof res; + (status: number, headers: Record): typeof res; + }; + end: (json?: string) => void; +}) => ReqResHandler; + +export type KoaAdapter = (ctx: { + get: (header: string) => string | undefined; + set: (key: string, value: string) => void; + status: number; + body: string; + request: { + body?: unknown; + }; + response: { + body: unknown; + status: number; + }; +}) => ReqResHandler; + +export type NextAdapter = (req: { + body: Update; + headers: Record; +}, res: { + end: (cb?: () => void) => typeof res; + status: (code: number) => typeof res; + // deno-lint-ignore no-explicit-any + json: (json: string) => any; + // deno-lint-ignore no-explicit-any + send: (json: string) => any; +}) => ReqResHandler; + +export type NHttpAdapter = (rev: { + body: unknown; + headers: { + get: (header: string) => string | null; + }; + response: { + sendStatus: (status: number) => void; + status: (status: number) => { + send: (json: string) => void; + }; + }; +}) => ReqResHandler; + +export type OakAdapter = (ctx: { + request: { + body: { + json: () => Promise; + }; + headers: { + get: (header: string) => string | null; + }; + }; + response: { + status: number; + type: string | undefined; + body: unknown; + }; +}) => ReqResHandler; + +export type ServeHttpAdapter = ( + requestEvent: { + request: Request; + respondWith: (response: Response) => void; + }, +) => ReqResHandler; + +export type StdHttpAdapter = ( + req: Request, +) => ReqResHandler; + +export type SveltekitAdapter = ( + { request }: { request: Request }, +) => ReqResHandler; + +export type WorktopAdapter = (req: { + json: () => Promise; + headers: { + get: (header: string) => string | null; + }; +}, res: { + end: (data: BodyInit | null) => void; + send: (status: number, json: string) => void; +}) => ReqResHandler; + +/** AWS lambda serverless functions */ +const awsLambda: LambdaAdapter = (event, _context, callback) => ({ + get update() { + return JSON.parse(event.body ?? "{}"); + }, + header: event.headers[SECRET_HEADER] ?? + event.headers[SECRET_HEADER_LOWERCASE], + end: () => callback(null, { statusCode: 200 }), + respond: (json) => + callback(null, { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: json, + }), + unauthorized: () => callback(null, { statusCode: 401 }), +}); + +/** AWS lambda async/await serverless functions */ +const awsLambdaAsync: LambdaAsyncAdapter = (event, _context) => { + // deno-lint-ignore no-explicit-any + let resolveResponse: (response: any) => void; + + return { + get update() { + return JSON.parse(event.body ?? "{}"); + }, + header: event.headers[SECRET_HEADER] ?? + event.headers[SECRET_HEADER_LOWERCASE], + end: () => resolveResponse({ statusCode: 200 }), + respond: (json) => + resolveResponse({ + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: json, + }), + unauthorized: () => resolveResponse({ statusCode: 401 }), + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +/** Azure Functions v3 and v4 */ +const azure: AzureAdapter = (context, request) => ({ + get update() { + return request.body as Update; + }, + header: request.headers?.[SECRET_HEADER_LOWERCASE], + end: () => (context.res = { + status: 200, + body: "", + }), + respond: (json) => { + context.res?.set?.("Content-Type", "application/json"); + context.res?.send?.(json); + }, + unauthorized: () => { + context.res?.send?.(401, WRONG_TOKEN_ERROR); + }, +}); +const azureV4: AzureAdapterV4 = (request) => { + type Res = NonNullable< + Awaited["handlerReturn"]> + >; + let resolveResponse: (response: Res) => void; + return { + get update() { + return request.json() as Promise; + }, + header: request.headers.get(SECRET_HEADER) || undefined, + end: () => resolveResponse({ status: 204 }), + respond: (json) => resolveResponse({ jsonBody: json }), + unauthorized: () => + resolveResponse({ status: 401, body: WRONG_TOKEN_ERROR }), + handlerReturn: new Promise((resolve) => resolveResponse = resolve), + }; +}; + +/** Bun.serve */ +const bun: BunAdapter = (request) => { + let resolveResponse: (response: Response) => void; + return { + get update() { + return request.json() as Promise; + }, + header: request.headers.get(SECRET_HEADER) || undefined, + end: () => { + resolveResponse(ok()); + }, + respond: (json) => { + resolveResponse(okJson(json)); + }, + unauthorized: () => { + resolveResponse(unauthorized()); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +/** Native CloudFlare workers (service worker) */ +const cloudflare: CloudflareAdapter = (event) => { + let resolveResponse: (response: Response) => void; + event.respondWith( + new Promise((resolve) => { + resolveResponse = resolve; + }), + ); + return { + get update() { + return event.request.json() as Promise; + }, + header: event.request.headers.get(SECRET_HEADER) || undefined, + end: () => { + resolveResponse(ok()); + }, + respond: (json) => { + resolveResponse(okJson(json)); + }, + unauthorized: () => { + resolveResponse(unauthorized()); + }, + }; +}; + +/** Native CloudFlare workers (module worker) */ +const cloudflareModule: CloudflareModuleAdapter = (request) => { + let resolveResponse: (res: Response) => void; + return { + get update() { + return request.json() as Promise; + }, + header: request.headers.get(SECRET_HEADER) || undefined, + end: () => { + resolveResponse(ok()); + }, + respond: (json) => { + resolveResponse(okJson(json)); + }, + unauthorized: () => { + resolveResponse(unauthorized()); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +/** express web framework */ +const express: ExpressAdapter = (req, res) => ({ + get update() { + return req.body as Update; + }, + header: req.header(SECRET_HEADER), + end: () => res.end(), + respond: (json) => { + res.set("Content-Type", "application/json"); + res.send(json); + }, + unauthorized: () => { + res.status(401).send(WRONG_TOKEN_ERROR); + }, +}); + +/** fastify web framework */ +const fastify: FastifyAdapter = (request, reply) => ({ + get update() { + return request.body as Update; + }, + header: request.headers[SECRET_HEADER_LOWERCASE], + end: () => reply.send(""), + respond: (json) => + reply.headers({ "Content-Type": "application/json" }).send(json), + unauthorized: () => reply.code(401).send(WRONG_TOKEN_ERROR), +}); + +/** hono web framework */ +const hono: HonoAdapter = (c) => { + let resolveResponse: (response: Response) => void; + return { + get update() { + return c.req.json() as Promise; + }, + header: c.req.header(SECRET_HEADER), + end: () => { + resolveResponse(c.body("")); + }, + respond: (json) => { + resolveResponse(c.json(json)); + }, + unauthorized: () => { + c.status(401); + resolveResponse(c.body("")); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +/** Node.js native 'http' and 'https' modules */ +const http: HttpAdapter = (req, res) => { + const secretHeaderFromRequest = req.headers[SECRET_HEADER_LOWERCASE]; + return { + get update() { + return new Promise((resolve, reject) => { + // deno-lint-ignore no-explicit-any + type Chunk = any; + const chunks: Chunk[] = []; + req.on("data", (chunk: Chunk) => chunks.push(chunk)) + .once("end", () => { + // @ts-ignore `Buffer` is Node-only + // deno-lint-ignore no-node-globals + const raw = Buffer.concat(chunks).toString("utf-8"); + try { + resolve(JSON.parse(raw)); + } catch (err) { + reject(err); + } + }) + .once("error", reject); + }) as Promise; + }, + header: Array.isArray(secretHeaderFromRequest) + ? secretHeaderFromRequest[0] + : secretHeaderFromRequest, + end: () => res.end(), + respond: (json) => + res + .writeHead(200, { "Content-Type": "application/json" }) + .end(json), + unauthorized: () => res.writeHead(401).end(WRONG_TOKEN_ERROR), + }; +}; + +/** koa web framework */ +const koa: KoaAdapter = (ctx) => ({ + get update() { + return ctx.request.body as Update; + }, + header: ctx.get(SECRET_HEADER) || undefined, + end: () => { + ctx.body = ""; + }, + respond: (json) => { + ctx.set("Content-Type", "application/json"); + ctx.response.body = json; + }, + unauthorized: () => { + ctx.status = 401; + }, +}); + +/** Next.js Serverless Functions */ +const nextJs: NextAdapter = (request, response) => ({ + get update() { + return request.body as Update; + }, + header: request.headers[SECRET_HEADER_LOWERCASE] as string, + end: () => response.end(), + respond: (json) => response.status(200).json(json), + unauthorized: () => response.status(401).send(WRONG_TOKEN_ERROR), +}); + +/** nhttp web framework */ +const nhttp: NHttpAdapter = (rev) => ({ + get update() { + return rev.body as Update; + }, + header: rev.headers.get(SECRET_HEADER) || undefined, + end: () => rev.response.sendStatus(200), + respond: (json) => rev.response.status(200).send(json), + unauthorized: () => rev.response.status(401).send(WRONG_TOKEN_ERROR), +}); + +/** oak web framework */ +const oak: OakAdapter = (ctx) => ({ + get update() { + return ctx.request.body.json() as Promise; + }, + header: ctx.request.headers.get(SECRET_HEADER) || undefined, + end: () => { + ctx.response.status = 200; + }, + respond: (json) => { + ctx.response.type = "json"; + ctx.response.body = json; + }, + unauthorized: () => { + ctx.response.status = 401; + }, +}); + +/** Deno.serve */ +const serveHttp: ServeHttpAdapter = (requestEvent) => ({ + get update() { + return requestEvent.request.json() as Promise; + }, + header: requestEvent.request.headers.get(SECRET_HEADER) || undefined, + end: () => requestEvent.respondWith(ok()), + respond: (json) => requestEvent.respondWith(okJson(json)), + unauthorized: () => requestEvent.respondWith(unauthorized()), +}); + +/** std/http web server */ +const stdHttp: StdHttpAdapter = (req) => { + let resolveResponse: (response: Response) => void; + return { + get update() { + return req.json() as Promise; + }, + header: req.headers.get(SECRET_HEADER) || undefined, + end: () => { + if (resolveResponse) resolveResponse(ok()); + }, + respond: (json) => { + if (resolveResponse) resolveResponse(okJson(json)); + }, + unauthorized: () => { + if (resolveResponse) resolveResponse(unauthorized()); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +/** Sveltekit Serverless Functions */ +const sveltekit: SveltekitAdapter = ({ request }) => { + let resolveResponse: (res: Response) => void; + return { + get update() { + return request.json() as Promise; + }, + header: request.headers.get(SECRET_HEADER) || undefined, + end: () => { + if (resolveResponse) resolveResponse(ok()); + }, + respond: (json) => { + if (resolveResponse) resolveResponse(okJson(json)); + }, + unauthorized: () => { + if (resolveResponse) resolveResponse(unauthorized()); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; +/** worktop Cloudflare workers framework */ +const worktop: WorktopAdapter = (req, res) => ({ + get update() { + return req.json() as Promise; + }, + header: req.headers.get(SECRET_HEADER) ?? undefined, + end: () => res.end(null), + respond: (json) => res.send(200, json), + unauthorized: () => res.send(401, WRONG_TOKEN_ERROR), +}); + +const elysia: ElysiaAdapter = (ctx) => { + // @note upgrade target to use modern code? + // const { promise, resolve } = Promise.withResolvers(); + + let resolveResponse: (result: string) => void; + + return { + // @note technically the type shouldn't be limited to Promise, because it's fine to await plain values as well + get update() { + return ctx.body as Update; + }, + header: ctx.headers[SECRET_HEADER_LOWERCASE], + end() { + resolveResponse(""); + }, + respond(json) { + // @note since json is passed as string here, we gotta define proper content-type + ctx.set.headers["content-type"] = "application/json"; + resolveResponse(json); + }, + unauthorized() { + ctx.set.status = 401; + resolveResponse(""); + }, + handlerReturn: new Promise((res) => resolveResponse = res), + }; +}; + +// Please open a pull request if you want to add another adapter +export const adapters = { + "aws-lambda": awsLambda, + "aws-lambda-async": awsLambdaAsync, + azure, + "azure-v4": azureV4, + bun, + cloudflare, + "cloudflare-mod": cloudflareModule, + elysia, + express, + fastify, + hono, + http, + https: http, + koa, + "next-js": nextJs, + nhttp, + oak, + serveHttp, + "std/http": stdHttp, + sveltekit, + worktop, +}; diff --git a/src/inline_query.ts b/src/inline_query.ts new file mode 100644 index 0000000..74cfc6f --- /dev/null +++ b/src/inline_query.ts @@ -0,0 +1,733 @@ +import { + type InlineQueryResult, + type InlineQueryResultArticle, + type InlineQueryResultAudio, + type InlineQueryResultCachedAudio, + type InlineQueryResultCachedDocument, + type InlineQueryResultCachedGif, + type InlineQueryResultCachedMpeg4Gif, + type InlineQueryResultCachedPhoto, + type InlineQueryResultCachedSticker, + type InlineQueryResultCachedVideo, + type InlineQueryResultCachedVoice, + type InlineQueryResultContact, + type InlineQueryResultDocument, + type InlineQueryResultGame, + type InlineQueryResultGif, + type InlineQueryResultLocation, + type InlineQueryResultMpeg4Gif, + type InlineQueryResultPhoto, + type InlineQueryResultVenue, + type InlineQueryResultVideo, + type InlineQueryResultVoice, + type InputContactMessageContent, + type InputInvoiceMessageContent, + type InputLocationMessageContent, + type InputRichMessage, + type InputRichMessageContent, + type InputTextMessageContent, + type InputVenueMessageContent, + type LabeledPrice, +} from "../types.ts"; + +type InlineQueryResultOptions = Omit< + T, + "type" | "id" | "input_message_content" | K +>; + +type OptionalKeys = { [K in keyof T]-?: undefined extends T[K] ? K : never }; +type OptionalFields = Pick[keyof T]>; +type PartialKeys = + & Omit + & Partial>; + +function inputMessage(queryTemplate: R) { + return { + ...queryTemplate, + ...inputMessageMethods(queryTemplate), + }; +} +function inputMessageMethods( + queryTemplate: Omit, +) { + return { + text( + message_text: string, + options: OptionalFields = {}, + ) { + const content: InputTextMessageContent = { + message_text, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + rich( + rich_message: InputRichMessage, + options: OptionalFields = {}, + ) { + const content: InputRichMessageContent = { + rich_message, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + location( + latitude: number, + longitude: number, + options: OptionalFields = {}, + ) { + const content: InputLocationMessageContent = { + latitude, + longitude, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + venue( + title: string, + latitude: number, + longitude: number, + address: string, + options: OptionalFields, + ) { + const content: InputVenueMessageContent = { + title, + latitude, + longitude, + address, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + contact( + first_name: string, + phone_number: string, + options: OptionalFields = {}, + ) { + const content: InputContactMessageContent = { + first_name, + phone_number, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + invoice( + title: string, + description: string, + payload: string, + provider_token: string, + currency: string, + prices: LabeledPrice[], + options: OptionalFields = {}, + ) { + const content: InputInvoiceMessageContent = { + title, + description, + payload, + provider_token, + currency, + prices, + ...options, + }; + return { ...queryTemplate, input_message_content: content } as R; + }, + }; +} + +/** + * Holds a number of helper methods for building `InlineQueryResult*` objects. + * + * For example, letting the user pick one out of three photos can be done like + * this. + * + * ```ts + * const results = [ + * InlineQueryResultBuilder.photo('id0', 'https://grammy.dev/images/Y.png'), + * InlineQueryResultBuilder.photo('id1', 'https://grammy.dev/images/Y.png'), + * InlineQueryResultBuilder.photo('id2', 'https://grammy.dev/images/Y.png'), + * ]; + * await ctx.answerInlineQuery(results) + * ``` + * + * If you want the message content to be different from the content in the + * inline query result, you can perform another method call on the resulting + * objects. + * + * ```ts + * const results = [ + * InlineQueryResultBuilder.photo("id0", "https://grammy.dev/images/Y.png") + * .text("Picked photo 0!"), + * InlineQueryResultBuilder.photo("id1", "https://grammy.dev/images/Y.png") + * .text("Picked photo 1!"), + * InlineQueryResultBuilder.photo("id2", "https://grammy.dev/images/Y.png") + * .text("Picked photo 2!"), + * ]; + * await ctx.answerInlineQuery(results) + * ``` + * + * Be sure to check the + * [documentation](https://core.telegram.org/bots/api#inline-mode) on inline + * mode. + */ +export const InlineQueryResultBuilder = { + /** + * Builds an InlineQueryResultArticle object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultarticle. Requires you + * to specify the actual message content by calling another function on the + * object returned from this method. + * + * @param id Unique identifier for this result, 1-64 Bytes + * @param title Title of the result + * @param options Remaining options + */ + article( + id: string, + title: string, + options: InlineQueryResultOptions< + InlineQueryResultArticle, + "title" + > = {}, + ) { + return inputMessageMethods( + { type: "article", id, title, ...options }, + ); + }, + /** + * Builds an InlineQueryResultAudio object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultaudio. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title + * @param audio_url A valid URL for the audio file + * @param options Remaining options + */ + audio( + id: string, + title: string, + audio_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultAudio, + "title" | "audio_url" + > = {}, + ) { + return inputMessage({ + type: "audio", + id, + title, + audio_url: typeof audio_url === "string" + ? audio_url + : audio_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedAudio object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedaudio. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param audio_file_id A valid file identifier for the audio file + * @param options Remaining options + */ + audioCached( + id: string, + audio_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedAudio, + "audio_file_id" + > = {}, + ) { + return inputMessage( + { type: "audio", id, audio_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultContact object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcontact. + * + * @param id Unique identifier for this result, 1-64 Bytes + * @param phone_number Contact's phone number + * @param first_name Contact's first name + * @param options Remaining options + */ + contact( + id: string, + phone_number: string, + first_name: string, + options: InlineQueryResultOptions< + InlineQueryResultContact, + "phone_number" | "first_name" + > = {}, + ) { + return inputMessage( + { type: "contact", id, phone_number, first_name, ...options }, + ); + }, + /** + * Builds an InlineQueryResultDocument object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultdocument with + * mime_type set to "application/pdf". + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param document_url A valid URL for the file + * @param options Remaining options + */ + documentPdf( + id: string, + title: string, + document_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultDocument, + "mime_type" | "title" | "document_url" + > = {}, + ) { + return inputMessage({ + type: "document", + mime_type: "application/pdf", + id, + title, + document_url: typeof document_url === "string" + ? document_url + : document_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultDocument object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultdocument with + * mime_type set to "application/zip". + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param document_url A valid URL for the file + * @param options Remaining options + */ + documentZip( + id: string, + title: string, + document_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultDocument, + "mime_type" | "title" | "document_url" + > = {}, + ) { + return inputMessage({ + type: "document", + mime_type: "application/zip", + id, + title, + document_url: typeof document_url === "string" + ? document_url + : document_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedDocument object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcacheddocument. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param document_file_id A valid file identifier for the file + * @param options Remaining options + */ + documentCached( + id: string, + title: string, + document_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedDocument, + "title" | "document_file_id" + > = {}, + ) { + return inputMessage( + { type: "document", id, title, document_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultGame object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultgame. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param game_short_name Short name of the game + * @param options Remaining options + */ + game( + id: string, + game_short_name: string, + options: InlineQueryResultOptions< + InlineQueryResultGame, + "game_short_name" + > = {}, + ) { + return { type: "game", id, game_short_name, ...options }; + }, + /** + * Builds an InlineQueryResultGif object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultgif. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param gif_url A valid URL for the GIF file. File size must not exceed 1MB + * @param thumbnail_url URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result + * @param options Remaining options + */ + gif( + id: string, + gif_url: string | URL, + thumbnail_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultGif, + "gif_url" | "thumbnail_url" + > = {}, + ) { + return inputMessage({ + type: "gif", + id, + gif_url: typeof gif_url === "string" ? gif_url : gif_url.href, + thumbnail_url: typeof thumbnail_url === "string" + ? thumbnail_url + : thumbnail_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedGif object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedgif. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param gif_file_id A valid file identifier for the GIF file + * @param options Remaining options + */ + gifCached( + id: string, + gif_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedGif, + "gif_file_id" + > = {}, + ) { + return inputMessage( + { type: "gif", id, gif_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultLocation object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultlocation. + * + * @param id Unique identifier for this result, 1-64 Bytes + * @param title Location title + * @param latitude Location latitude in degrees + * @param longitude Location longitude in degrees + * @param options Remaining options + */ + location( + id: string, + title: string, + latitude: number, + longitude: number, + options: InlineQueryResultOptions< + InlineQueryResultLocation, + "title" | "latitude" | "longitude" + > = {}, + ) { + return inputMessage( + { type: "location", id, title, latitude, longitude, ...options }, + ); + }, + /** + * Builds an InlineQueryResultMpeg4Gif object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param mpeg4_url A valid URL for the MPEG4 file. File size must not exceed 1MB + * @param thumbnail_url URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result + * @param options Remaining options + */ + mpeg4gif( + id: string, + mpeg4_url: string | URL, + thumbnail_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultMpeg4Gif, + "mpeg4_url" | "thumbnail_url" + > = {}, + ) { + return inputMessage({ + type: "mpeg4_gif", + id, + mpeg4_url: typeof mpeg4_url === "string" + ? mpeg4_url + : mpeg4_url.href, + thumbnail_url: typeof thumbnail_url === "string" + ? thumbnail_url + : thumbnail_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedMpeg4Gif object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedmpeg4gif. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param mpeg4_file_id A valid file identifier for the MPEG4 file + * @param options Remaining options + */ + mpeg4gifCached( + id: string, + mpeg4_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedMpeg4Gif, + "mpeg4_file_id" + > = {}, + ) { + return inputMessage( + { type: "mpeg4_gif", id, mpeg4_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultPhoto object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultphoto with the + * thumbnail defaulting to the photo itself. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param photo_url A valid URL of the photo. Photo must be in JPEG format. Photo size must not exceed 5MB + * @param options Remaining options + */ + photo( + id: string, + photo_url: string | URL, + options: InlineQueryResultOptions< + // do not require thumbnail, default to the photo itself + PartialKeys, + "photo_url" + > = {}, + ) { + const photoUrl = typeof photo_url === "string" + ? photo_url + : photo_url.href; + return inputMessage({ + type: "photo", + id, + photo_url: photoUrl, + thumbnail_url: photoUrl, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedPhoto object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedphoto. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param photo_file_id A valid file identifier of the photo + * @param options Remaining options + */ + photoCached( + id: string, + photo_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedPhoto, + "photo_file_id" + > = {}, + ) { + return inputMessage( + { type: "photo", id, photo_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultCachedSticker object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedsticker. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param sticker_file_id A valid file identifier of the sticker + * @param options Remaining options + */ + stickerCached( + id: string, + sticker_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedSticker, + "sticker_file_id" + > = {}, + ) { + return inputMessage( + { type: "sticker", id, sticker_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultVenue object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultvenue. + * + * @param id Unique identifier for this result, 1-64 Bytes + * @param title Title of the venue + * @param latitude Latitude of the venue location in degrees + * @param longitude Longitude of the venue location in degrees + * @param address Address of the venue + * @param options Remaining options + */ + venue( + id: string, + title: string, + latitude: number, + longitude: number, + address: string, + options: InlineQueryResultOptions< + InlineQueryResultVenue, + "title" | "latitude" | "longitude" | "address" + > = {}, + ) { + return inputMessage({ + type: "venue", + id, + title, + latitude, + longitude, + address, + ...options, + }); + }, + /** + * Builds an InlineQueryResultVideo object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultvideo with mime_type + * set to "text/html". This will send an embedded video player. Requires you + * to specify the actual message content by calling another function on the + * object returned from this method. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param video_url A valid URL for the embedded video player + * @param thumbnail_url URL of the thumbnail (JPEG only) for the video + * @param options Remaining options + */ + videoHtml( + id: string, + title: string, + video_url: string | URL, + thumbnail_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultVideo, + "mime_type" | "title" | "video_url" | "thumbnail_url" + > = {}, + ) { + // require input message content by only returning methods + return inputMessageMethods({ + type: "video", + mime_type: "text/html", + id, + title, + video_url: typeof video_url === "string" + ? video_url + : video_url.href, + thumbnail_url: typeof thumbnail_url === "string" + ? thumbnail_url + : thumbnail_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultVideo object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultvideo with mime_type + * set to "video/mp4". + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param video_url A valid URL for the video file + * @param thumbnail_url URL of the thumbnail (JPEG only) for the video + * @param options Remaining options + */ + videoMp4( + id: string, + title: string, + video_url: string | URL, + thumbnail_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultVideo, + "mime_type" | "title" | "video_url" | "thumbnail_url" + > = {}, + ) { + return inputMessage({ + type: "video", + mime_type: "video/mp4", + id, + title, + video_url: typeof video_url === "string" + ? video_url + : video_url.href, + thumbnail_url: typeof thumbnail_url === "string" + ? thumbnail_url + : thumbnail_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedVideo object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedvideo. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Title for the result + * @param video_file_id A valid file identifier for the video file + * @param options Remaining options + */ + videoCached( + id: string, + title: string, + video_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedVideo, + "title" | "video_file_id" + > = {}, + ) { + return inputMessage( + { type: "video", id, title, video_file_id, ...options }, + ); + }, + /** + * Builds an InlineQueryResultVoice object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultvoice. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Voice message title + * @param voice_url A valid URL for the voice recording + * @param options Remaining options + */ + voice( + id: string, + title: string, + voice_url: string | URL, + options: InlineQueryResultOptions< + InlineQueryResultVoice, + "title" | "voice_url" + > = {}, + ) { + return inputMessage({ + type: "voice", + id, + title, + voice_url: typeof voice_url === "string" + ? voice_url + : voice_url.href, + ...options, + }); + }, + /** + * Builds an InlineQueryResultCachedVoice object as specified by + * https://core.telegram.org/bots/api#inlinequeryresultcachedvoice. + * + * @param id Unique identifier for this result, 1-64 bytes + * @param title Voice message title + * @param voice_file_id A valid file identifier for the voice message + * @param options Remaining options + */ + voiceCached( + id: string, + title: string, + voice_file_id: string, + options: InlineQueryResultOptions< + InlineQueryResultCachedVoice, + "title" | "voice_file_id" + > = {}, + ) { + return inputMessage( + { type: "voice", id, title, voice_file_id, ...options }, + ); + }, +}; diff --git a/src/input_media.ts b/src/input_media.ts new file mode 100644 index 0000000..6ce459b --- /dev/null +++ b/src/input_media.ts @@ -0,0 +1,105 @@ +import { + type InputFile, + type InputMediaAnimation, + type InputMediaAudio, + type InputMediaDocument, + type InputMediaPhoto, + type InputMediaVideo, +} from "../types.ts"; + +type InputMediaOptions = Omit; + +/** + * Holds a number of helper methods for building `InputMedia*` objects. They are + * useful when sending media groups and when editing media messages. + * + * For example, media groups can be sent like this. + * + * ```ts + * const paths = [ + * '/tmp/pic0.jpg', + * '/tmp/pic1.jpg', + * '/tmp/pic2.jpg', + * ] + * const files = paths.map((path) => new InputFile(path)) + * const media = files.map((file) => InputMediaBuilder.photo(file)) + * await bot.api.sendMediaGroup(chatId, media) + * ``` + * + * Media can be edited like this. + * + * ```ts + * const file = new InputFile('/tmp/pic0.jpg') + * const media = InputMediaBuilder.photo(file, { + * caption: 'new caption' + * }) + * await bot.api.editMessageMedia(chatId, messageId, media) + * ``` + */ +export const InputMediaBuilder = { + /** + * Creates a new `InputMediaPhoto` object as specified by + * https://core.telegram.org/bots/api#inputmediaphoto. + * + * @param media An `InputFile` instance or a file identifier + * @param options Remaining optional options + */ + photo( + media: string | InputFile, + options: InputMediaOptions = {}, + ): InputMediaPhoto { + return { type: "photo", media, ...options }; + }, + /** + * Creates a new `InputMediaVideo` object as specified by + * https://core.telegram.org/bots/api#inputmediavideo. + * + * @param media An `InputFile` instance or a file identifier + * @param options Remaining optional options + */ + video( + media: string | InputFile, + options: InputMediaOptions = {}, + ): InputMediaVideo { + return { type: "video", media, ...options }; + }, + /** + * Creates a new `InputMediaAnimation` object as specified by + * https://core.telegram.org/bots/api#inputmediaanimation. + * + * @param media An `InputFile` instance or a file identifier + * @param options Remaining optional options + */ + animation( + media: string | InputFile, + options: InputMediaOptions = {}, + ): InputMediaAnimation { + return { type: "animation", media, ...options }; + }, + /** + * Creates a new `InputMediaAudio` object as specified by + * https://core.telegram.org/bots/api#inputmediaaudio. + * + * @param media An `InputFile` instance or a file identifier + * @param options Remaining optional options + */ + audio( + media: string | InputFile, + options: InputMediaOptions = {}, + ): InputMediaAudio { + return { type: "audio", media, ...options }; + }, + /** + * Creates a new `InputMediaDocument` object as specified by + * https://core.telegram.org/bots/api#inputmediadocument. + * + * @param media An `InputFile` instance or a file identifier + * @param options Remaining optional options + */ + document( + media: string | InputFile, + options: InputMediaOptions = {}, + ): InputMediaDocument { + return { type: "document", media, ...options }; + }, +}; diff --git a/src/keyboard.ts b/src/keyboard.ts new file mode 100644 index 0000000..505b698 --- /dev/null +++ b/src/keyboard.ts @@ -0,0 +1,1328 @@ +import { + type CopyTextButton, + type InlineKeyboardButton, + type KeyboardButton, + type KeyboardButtonPollType, + type KeyboardButtonRequestChat, + type KeyboardButtonRequestManagedBot, + type KeyboardButtonRequestUsers, + type LoginUrl, + type SwitchInlineQueryChosenChat, + type WebAppInfo, +} from "../types.ts"; + +type KeyboardButtonSource = string | KeyboardButton; +type KeyboardSource = KeyboardButtonSource[][] | Keyboard; +/** + * Use this class to simplify building a custom keyboard (something like this: + * https://core.telegram.org/bots/features#keyboards). + * + * ```ts + * // Build a custom keyboard: + * const keyboard = new Keyboard() + * .text('A').text('B').row() + * .text('C').text('D') + * + * // Now you can send it like so: + * await ctx.reply('Here is your custom keyboard!', { + * reply_markup: keyboard + * }) + * ``` + * + * If you already have some source data which you would like to turn into a + * keyboard button object, you can use the static equivalents which every button + * has. You can use them to create a two-dimensional keyboard button array. The + * resulting array can be turned into a keyboard instance. + * + * ```ts + * const button = Keyboard.text('push my buttons') + * const array = [[button]] + * const keyboard = Keyboard.from(array) + * ``` + * + * If you want to create text buttons only, you can directly use a + * two-dimensional string array and turn it into a keyboard. + * + * ```ts + * const data = [['A', 'B'], ['C', 'D']] + * const keyboard = Keyboard.from(data) + * ``` + * + * Be sure to check out the + * [documentation](https://grammy.dev/plugins/keyboard#custom-keyboards) on + * custom keyboards in grammY. + */ +export class Keyboard { + /** + * Requests clients to always show the keyboard when the regular keyboard is + * hidden. Defaults to false, in which case the custom keyboard can be + * hidden and opened with a keyboard icon. + */ + public is_persistent?: boolean; + /** + * Show the current keyboard only to those users that are mentioned in the + * text of the message object. + */ + public selective?: boolean; + /** + * Hide the keyboard after a button is pressed. + */ + public one_time_keyboard?: boolean; + /** + * Resize the current keyboard according to its buttons. Usually, this will + * make the keyboard smaller. + */ + public resize_keyboard?: boolean; + /** + * Placeholder to be shown in the input field when the keyboard is active. + */ + public input_field_placeholder?: string; + + /** + * Initialize a new `Keyboard` with an optional two-dimensional array of + * `KeyboardButton` objects. This is the nested array that holds the custom + * keyboard. It will be extended every time you call one of the provided + * methods. + * + * @param keyboard An optional initial two-dimensional button array + */ + constructor(public readonly keyboard: KeyboardButton[][] = [[]]) {} + /** + * Allows you to add your own `KeyboardButton` objects if you already have + * them for some reason. You most likely want to call one of the other + * methods. + * + * @param buttons The buttons to add + */ + add(...buttons: KeyboardButton[]) { + this.keyboard[this.keyboard.length - 1]?.push(...buttons); + return this; + } + /** + * Adds a 'line break'. Call this method to make sure that the next added + * buttons will be on a new row. + * + * You may pass a number of `KeyboardButton` objects if you already have the + * instances for some reason. You most likely don't want to pass any + * arguments to `row`. + * + * @param buttons A number of buttons to add to the next row + */ + row(...buttons: KeyboardButton[]) { + this.keyboard.push(buttons); + return this; + } + /** + * Adds a new text button. This button will simply send the given text as a + * text message back to your bot if a user clicks on it. + * + * @param text The text to display, and optional styling information + * @param options Optional styling information + */ + text( + text: string, + options?: + | KeyboardButton.CommonButton["style"] + | Omit, + ) { + return this.add(Keyboard.text(text, options)); + } + /** + * Creates a new text button. This button will simply send the given text as + * a text message back to your bot if a user clicks on it. + * + * @param text The text to display, and optional styling information + * @param options Optional styling information + */ + static text( + text: string, + options?: + | KeyboardButton.CommonButton["style"] + | Omit, + ): KeyboardButton.CommonButton { + return typeof options === "string" + ? { text, style: options } + : { text, ...options }; + } + /** + * Adds a new request users button. When the user presses the button, a list + * of suitable users will be opened. Tapping on any number of users will + * send their identifiers to the bot in a β€œusers_shared” service message. + * Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + requestUsers( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = {}, + ) { + return this.add(Keyboard.requestUsers(text, requestId, options)); + } + /** + * Creates a new request users button. When the user presses the button, a + * list of suitable users will be opened. Tapping on any number of users + * will send their identifiers to the bot in a β€œusers_shared” service + * message. Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + static requestUsers( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = {}, + ): KeyboardButton.RequestUsersButton { + const request_users = { request_id: requestId, ...options }; + return typeof text === "string" + ? { text, request_users } + : { ...text, request_users }; + } + /** + * Adds a new request chat button. When the user presses the button, a list + * of suitable users will be opened. Tapping on a chat will send its + * identifier to the bot in a β€œchat_shared” service message. Available in + * private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + requestChat( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = { + chat_is_channel: false, + }, + ) { + return this.add(Keyboard.requestChat(text, requestId, options)); + } + /** + * Creates a new request chat button. When the user presses the button, a + * list of suitable users will be opened. Tapping on a chat will send its + * identifier to the bot in a β€œchat_shared” service message. Available in + * private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + static requestChat( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = { + chat_is_channel: false, + }, + ): KeyboardButton.RequestChatButton { + const request_chat = { request_id: requestId, ...options }; + return typeof text === "string" + ? { text, request_chat } + : { ...text, request_chat }; + } + /** + * Adds a new contact request button. The user's phone number will be sent + * as a contact when the button is pressed. Available in private chats only. + * + * @param text The text to display, and optional styling information + */ + requestContact(text: string | KeyboardButton.CommonButton) { + return this.add(Keyboard.requestContact(text)); + } + /** + * Creates a new contact request button. The user's phone number will be + * sent as a contact when the button is pressed. Available in private chats + * only. + * + * @param text The text to display, and optional styling information + */ + static requestContact( + text: string | KeyboardButton.CommonButton, + ): KeyboardButton.RequestContactButton { + const request_contact = true; + return typeof text === "string" + ? { text, request_contact } + : { ...text, request_contact }; + } + /** + * Adds a new location request button. The user's current location will be + * sent when the button is pressed. Available in private chats only. + * + * @param text The text to display, and optional styling information + */ + requestLocation(text: string | KeyboardButton.CommonButton) { + return this.add(Keyboard.requestLocation(text)); + } + /** + * Creates a new location request button. The user's current location will + * be sent when the button is pressed. Available in private chats only. + * + * @param text The text to display, and optional styling information + */ + static requestLocation( + text: string | KeyboardButton.CommonButton, + ): KeyboardButton.RequestLocationButton { + const request_location = true; + return typeof text === "string" + ? { text, request_location } + : { ...text, request_location }; + } + /** + * Adds a new poll request button. The user will be asked to create a poll + * and send it to the bot when the button is pressed. Available in private + * chats only. + * + * @param text The text to display, and optional styling information + * @param type The type of permitted polls to create, omit if the user may + * send a poll of any type + */ + requestPoll( + text: string | KeyboardButton.CommonButton, + type?: KeyboardButtonPollType["type"], + ) { + return this.add(Keyboard.requestPoll(text, type)); + } + /** + * Creates a new poll request button. The user will be asked to create a + * poll and send it to the bot when the button is pressed. Available in + * private chats only. + * + * @param text The text to display, and optional styling information + * @param type The type of permitted polls to create, omit if the user may + * send a poll of any type + */ + static requestPoll( + text: string | KeyboardButton.CommonButton, + type?: KeyboardButtonPollType["type"], + ): KeyboardButton.RequestPollButton { + const request_poll = { type }; + return typeof text === "string" + ? { text, request_poll } + : { ...text, request_poll }; + } + /** + * Adds a new managed bot request button. The user will be asked to create + * and share a bot that will be managed by the current bot when the button + * is pressed. Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + requestManagedBot( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = {}, + ) { + return this.add(Keyboard.requestManagedBot(text, requestId, options)); + } + /** + * Creates a new managed bot request button. The user will be asked to + * create and share a bot that will be managed by the current bot when the + * button is pressed. Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param requestId A signed 32-bit identifier of the request + * @param options Options object for further requirements + */ + static requestManagedBot( + text: string | KeyboardButton.CommonButton, + requestId: number, + options: Omit = {}, + ): KeyboardButton.RequestManagedBotButton { + const request_managed_bot = { request_id: requestId, ...options }; + return typeof text === "string" + ? { text, request_managed_bot } + : { ...text, request_managed_bot }; + } + /** + * Adds a new web app button. The Web App that will be launched when the + * user presses the button. The Web App will be able to send a + * β€œweb_app_data” service message. Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param url An HTTPS URL of a Web App to be opened with additional data + */ + webApp(text: string | KeyboardButton.CommonButton, url: string) { + return this.add(Keyboard.webApp(text, url)); + } + /** + * Creates a new web app button. The Web App that will be launched when the + * user presses the button. The Web App will be able to send a + * β€œweb_app_data” service message. Available in private chats only. + * + * @param text The text to display, and optional styling information + * @param url An HTTPS URL of a Web App to be opened with additional data + */ + static webApp( + text: string | KeyboardButton.CommonButton, + url: string, + ): KeyboardButton.WebAppButton { + const web_app = { url }; + return typeof text === "string" + ? { text, web_app } + : { ...text, web_app }; + } + /** + * Adds a style to the last added button of the keyboard. + * + * ```ts + * const keyboard = new Keyboard() + * .text('blue button') + * .style('primary') + * ``` + * + * @param style Style of the button + */ + style(style: KeyboardButton.CommonButton["style"]) { + const rows = this.keyboard.length; + if (rows === 0) { + throw new Error("Need to add a button before applying a style!"); + } + const lastRow = this.keyboard[rows - 1]; + const cols = lastRow.length; + if (cols === 0) { + throw new Error("Need to add a button before applying a style!"); + } + let lastButton = lastRow[cols - 1]; + if (typeof lastButton === "string") { + lastButton = { text: lastButton }; + lastRow[cols - 1] = lastButton; + } + lastButton.style = style; + return this; + } + /** + * Adds a danger style to the last added button of the keyboard. Alias for + * `.style('danger')`. + * + * ```ts + * const keyboard = new Keyboard() + * .text('red button') + * .danger() + * ``` + */ + danger() { + return this.style("danger"); + } + /** + * Adds a success style to the last added button of the keyboard. Alias for + * `.style('success')`. + * + * ```ts + * const keyboard = new Keyboard() + * .text('green button') + * .success() + * ``` + */ + success() { + return this.style("success"); + } + /** + * Adds a primary style to the last added button of the keyboard. Alias for + * `.style('primary')`. + * + * ```ts + * const keyboard = new Keyboard() + * .text('blue button') + * .primary() + * ``` + */ + primary() { + return this.style("primary"); + } + /** + * Adds a custom emoji icon to the last added button of the keyboard. + * + * ```ts + * const keyboard = new Keyboard() + * .text('button with icon') + * .icon(myCustomEmojiIconIdentifier) + * ``` + * + * @param icon Unique identifier of the custom emoji shown before the text of the button + */ + icon(icon: KeyboardButton.CommonButton["icon_custom_emoji_id"]) { + const rows = this.keyboard.length; + if (rows === 0) { + throw new Error("Need to add a button before adding an icon!"); + } + const lastRow = this.keyboard[rows - 1]; + const cols = lastRow.length; + if (cols === 0) { + throw new Error("Need to add a button before adding an icon!"); + } + let lastButton = lastRow[cols - 1]; + if (typeof lastButton === "string") { + lastButton = { text: lastButton }; + lastRow[cols - 1] = lastButton; + } + lastButton.icon_custom_emoji_id = icon; + return this; + } + /** + * Make the current keyboard persistent. See + * https://grammy.dev/plugins/keyboard#persistent-keyboards for more + * details. + * + * Keyboards are not persistent by default, use this function to enable it + * (without any parameters or pass `true`). Pass `false` to force the + * keyboard to not persist. + * + * @param isEnabled `true` if the keyboard should persist, and `false` otherwise + */ + persistent(isEnabled = true) { + this.is_persistent = isEnabled; + return this; + } + /** + * Make the current keyboard selective. See + * https://grammy.dev/plugins/keyboard#selectively-send-custom-keyboards + * for more details. + * + * Keyboards are non-selective by default, use this function to enable it + * (without any parameters or pass `true`). Pass `false` to force the + * keyboard to be non-selective. + * + * @param isEnabled `true` if the keyboard should be selective, and `false` otherwise + */ + selected(isEnabled = true) { + this.selective = isEnabled; + return this; + } + /** + * Make the current keyboard one-time. See + * https://grammy.dev/plugins/keyboard#one-time-custom-keyboards for + * more details. + * + * Keyboards are non-one-time by default, use this function to enable it + * (without any parameters or pass `true`). Pass `false` to force the + * keyboard to be non-one-time. + * + * @param isEnabled `true` if the keyboard should be one-time, and `false` otherwise + */ + oneTime(isEnabled = true) { + this.one_time_keyboard = isEnabled; + return this; + } + /** + * Make the current keyboard resized. See + * https://grammy.dev/plugins/keyboard#resize-custom-keyboard for more + * details. + * + * Keyboards are non-resized by default, use this function to enable it + * (without any parameters or pass `true`). Pass `false` to force the + * keyboard to be non-resized. + * + * @param isEnabled `true` if the keyboard should be resized, and `false` otherwise + */ + resized(isEnabled = true) { + this.resize_keyboard = isEnabled; + return this; + } + /** + * Set the current keyboard's input field placeholder. See + * https://grammy.dev/plugins/keyboard#input-field-placeholder for more + * details. + * + * @param value The placeholder text + */ + placeholder(value: string) { + this.input_field_placeholder = value; + return this; + } + /** + * Creates a new keyboard that contains the transposed grid of buttons of + * this keyboard. This means that the resulting keyboard has the rows and + * columns flipped. + * + * Note that buttons can only span multiple columns, but never multiple + * rows. This means that if the given arrays have different lengths, some + * buttons might flow up in the layout. In these cases, transposing a + * keyboard a second time will not undo the first transposition. + * + * Here are some examples. + * + * ``` + * original transposed + * [ a ] ~> [ a ] + * + * [ a ] + * [a b c] ~> [ b ] + * [ c ] + * + * [ a b ] [a c e] + * [ c d ] ~> [ b d ] + * [ e ] + * + * [ a b ] [a c d] + * [ c ] ~> [ b e ] + * [d e f] [ f ] + * ``` + */ + toTransposed() { + const original = this.keyboard; + const transposed = transpose(original); + return this.clone(transposed); + } + /** + * Creates a new keyboard with the same buttons but reflowed into a given + * number of columns as if the buttons were text elements. Optionally, you + * can specify if the flow should make sure to fill up the last row. + * + * This method is idempotent, so calling it a second time will effectively + * clone this keyboard without reordering the buttons. + * + * Here are some examples. + * + * ``` + * original flowed + * [ a ] ~> [ a ] (4 columns) + * + * [ a ] + * [a b c] ~> [ b ] (1 column) + * [ c ] + * + * [ a b ] [a b c] + * [ c d ] ~> [ d e ] (3 columns) + * [ e ] + * + * [ a b ] [abcde] + * [ c ] ~> [ f ] (5 columns) + * [d e f] + * + * [a b c] [ a ] + * [d e f] ~> [b c d] (3 columns, { fillLastRow: true }) + * [g h i] [e f g] + * [ j ] [h i j] + * ``` + * + * @param columns Maximum number of buttons per row + * @param options Optional flowing behavior + */ + toFlowed(columns: number, options: FlowOptions = {}) { + const original = this.keyboard; + const flowed = reflow(original, columns, options); + return this.clone(flowed); + } + /** + * Creates and returns a deep copy of this keyboard. + * + * Optionally takes a new grid of buttons to replace the current buttons. If + * specified, only the options will be cloned, and the given buttons will be + * used instead. + */ + clone(keyboard: KeyboardButton[][] = this.keyboard) { + const clone = new Keyboard(keyboard.map((row) => row.slice())); + clone.is_persistent = this.is_persistent; + clone.selective = this.selective; + clone.one_time_keyboard = this.one_time_keyboard; + clone.resize_keyboard = this.resize_keyboard; + clone.input_field_placeholder = this.input_field_placeholder; + return clone; + } + /** + * Appends the buttons of the given keyboards to this keyboard. If other + * options are specified in these keyboards, they will be ignored. + * + * @param sources A number of keyboards to append + */ + append(...sources: KeyboardSource[]) { + for (const source of sources) { + const keyboard = Keyboard.from(source); + this.keyboard.push(...keyboard.keyboard.map((row) => row.slice())); + } + return this; + } + /** + * Returns the keyboard that was build. Note that it doesn't return + * `resize_keyboard` or other options that may be set. You don't usually + * need to call this method. It is no longer useful. + */ + build() { + return this.keyboard; + } + /** + * Turns a two-dimensional keyboard button array into a keyboard instance. + * You can use the static button builder methods to create keyboard button + * objects. + * + * @param source A two-dimensional button array + */ + static from(source: KeyboardSource): Keyboard { + if (source instanceof Keyboard) return source.clone(); + function toButton(btn: KeyboardButtonSource) { + return typeof btn === "string" ? Keyboard.text(btn) : btn; + } + return new Keyboard(source.map((row) => row.map(toButton))); + } +} + +type InlineKeyboardSource = InlineKeyboardButton[][] | InlineKeyboard; +/** + * Use this class to simplify building an inline keyboard (something like this: + * https://core.telegram.org/bots/features#inline-keyboards). + * + * ```ts + * // Build an inline keyboard: + * const keyboard = new InlineKeyboard() + * .text('A').text('B', 'callback-data').row() + * .text('C').text('D').row() + * .url('Telegram', 'telegram.org') + * + * // Send the keyboard: + * await ctx.reply('Here is your inline keyboard!', { + * reply_markup: keyboard + * }) + * ``` + * + * If you already have some source data which you would like to turn into an + * inline button object, you can use the static equivalents which every inline + * button has. You can use them to create a two-dimensional inline button array. + * The resulting array can be turned into a keyboard instance. + * + * ```ts + * const button = InlineKeyboard.text('GO', 'go') + * const array = [[button]] + * const keyboard = InlineKeyboard.from(array) + * ``` + * + * Be sure to to check the + * [documentation](https://grammy.dev/plugins/keyboard#inline-keyboards) on + * inline keyboards in grammY. + */ +export class InlineKeyboard { + /** + * Initialize a new `InlineKeyboard` with an optional two-dimensional array + * of `InlineKeyboardButton` objects. This is the nested array that holds + * the inline keyboard. It will be extended every time you call one of the + * provided methods. + * + * @param inline_keyboard An optional initial two-dimensional button array + */ + constructor( + public readonly inline_keyboard: InlineKeyboardButton[][] = [[]], + ) {} + /** + * Allows you to add your own `InlineKeyboardButton` objects if you already + * have them for some reason. You most likely want to call one of the other + * methods. + * + * @param buttons The buttons to add + */ + add(...buttons: InlineKeyboardButton[]) { + this.inline_keyboard[this.inline_keyboard.length - 1]?.push(...buttons); + return this; + } + /** + * Adds a 'line break'. Call this method to make sure that the next added + * buttons will be on a new row. + * + * You may pass a number of `InlineKeyboardButton` objects if you already + * have the instances for some reason. You most likely don't want to pass + * any arguments to `row`. + * + * @param buttons A number of buttons to add to the next row + */ + row(...buttons: InlineKeyboardButton[]) { + this.inline_keyboard.push(buttons); + return this; + } + /** + * Adds a new URL button. Telegram clients will open the provided URL when + * the button is pressed. + * + * @param text The text to display, and optional styling information + * @param url HTTP or tg:// url to be opened when the button is pressed. Links tg://user?id= can be used to mention a user by their ID without using a username, if this is allowed by their privacy settings. + */ + url( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + url: string, + ) { + return this.add(InlineKeyboard.url(text, url)); + } + /** + * Creates a new URL button. Telegram clients will open the provided URL + * when the button is pressed. + * + * @param text The text to display, and optional styling information + * @param url HTTP or tg:// url to be opened when the button is pressed. Links tg://user?id= can be used to mention a user by their ID without using a username, if this is allowed by their privacy settings. + */ + static url( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + url: string, + ): InlineKeyboardButton.UrlButton { + return typeof text === "string" ? { text, url } : { ...text, url }; + } + /** + * Adds a new callback query button. The button contains a text and a custom + * payload. This payload will be sent back to your bot when the button is + * pressed. If you omit the payload, the display text will be sent back to + * your bot. + * + * Your bot will receive an update every time a user presses any of the text + * buttons. You can listen to these updates like this: + * ```ts + * // Specific buttons: + * bot.callbackQuery('button-data', ctx => { ... }) + * // Any button of any inline keyboard: + * bot.on('callback_query:data', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param data The callback data to send back to your bot (default = text) + */ + text( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + data = typeof text === "string" ? text : text.text, + ) { + return this.add(InlineKeyboard.text(text, data)); + } + /** + * Creates a new callback query button. The button contains a text and a + * custom payload. This payload will be sent back to your bot when the + * button is pressed. If you omit the payload, the display text will be sent + * back to your bot. + * + * Your bot will receive an update every time a user presses any of the text + * buttons. You can listen to these updates like this: + * ```ts + * // Specific buttons: + * bot.callbackQuery('button-data', ctx => { ... }) + * // Any button of any inline keyboard: + * bot.on('callback_query:data', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param data The callback data to send back to your bot (default = text) + */ + static text( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + data = typeof text === "string" ? text : text.text, + ): InlineKeyboardButton.CallbackButton { + return typeof text === "string" + ? { text, callback_data: data } + : { ...text, callback_data: data }; + } + /** + * Adds a new web app button, confer https://core.telegram.org/bots/webapps + * + * @param text The text to display, and optional styling information + * @param url An HTTPS URL of a Web App to be opened with additional data + */ + webApp( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + url: string | WebAppInfo, + ) { + return this.add(InlineKeyboard.webApp(text, url)); + } + /** + * Creates a new web app button, confer https://core.telegram.org/bots/webapps + * + * @param text The text to display, and optional styling information + * @param url An HTTPS URL of a Web App to be opened with additional data + */ + static webApp( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + url: string | WebAppInfo, + ): InlineKeyboardButton.WebAppButton { + const web_app = typeof url === "string" ? { url } : url; + return typeof text === "string" + ? { text, web_app } + : { ...text, web_app }; + } + /** + * Adds a new login button. This can be used as a replacement for the + * Telegram Login Widget. You must specify an HTTPS URL used to + * automatically authorize the user. + * + * @param text The text to display, and optional styling information + * @param loginUrl The login URL as string or `LoginUrl` object + */ + login( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + loginUrl: string | LoginUrl, + ) { + return this.add(InlineKeyboard.login(text, loginUrl)); + } + /** + * Creates a new login button. This can be used as a replacement for the + * Telegram Login Widget. You must specify an HTTPS URL used to + * automatically authorize the user. + * + * @param text The text to display, and optional styling information + * @param loginUrl The login URL as string or `LoginUrl` object + */ + static login( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + loginUrl: string | LoginUrl, + ): InlineKeyboardButton.LoginButton { + const login_url = typeof loginUrl === "string" + ? { url: loginUrl } + : loginUrl; + return typeof text === "string" + ? { text, login_url } + : { ...text, login_url }; + } + /** + * Adds a new inline query button. Telegram clients will let the user pick a + * chat when this button is pressed. This will start an inline query. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The (optional) inline query string to prefill + */ + switchInline( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query = "", + ) { + return this.add(InlineKeyboard.switchInline(text, query)); + } + /** + * Creates a new inline query button. Telegram clients will let the user pick a + * chat when this button is pressed. This will start an inline query. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The (optional) inline query string to prefill + */ + static switchInline( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query = "", + ): InlineKeyboardButton.SwitchInlineButton { + return typeof text === "string" + ? { text, switch_inline_query: query } + : { ...text, switch_inline_query: query }; + } + /** + * Adds a new inline query button that acts on the current chat. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. This will start an inline + * query. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The (optional) inline query string to prefill + */ + switchInlineCurrent( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query = "", + ) { + return this.add(InlineKeyboard.switchInlineCurrent(text, query)); + } + /** + * Creates a new inline query button that acts on the current chat. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. This will start an inline + * query. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The (optional) inline query string to prefill + */ + static switchInlineCurrent( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query = "", + ): InlineKeyboardButton.SwitchInlineCurrentChatButton { + return typeof text === "string" + ? { text, switch_inline_query_current_chat: query } + : { ...text, switch_inline_query_current_chat: query }; + } + /** + * Adds a new inline query button. Telegram clients will let the user pick a + * chat when this button is pressed. This will start an inline query. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The query object describing which chats can be picked + */ + switchInlineChosen( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query: SwitchInlineQueryChosenChat = {}, + ) { + return this.add(InlineKeyboard.switchInlineChosen(text, query)); + } + /** + * Creates a new inline query button. Telegram clients will let the user pick a + * chat when this button is pressed. This will start an inline query. The + * selected chat will be prefilled with the name of your bot. You may + * provide a text that is specified along with it. + * + * Your bot will in turn receive updates for inline queries. You can listen + * to inline query updates like this: + * ```ts + * bot.on('inline_query', ctx => { ... }) + * ``` + * + * @param text The text to display, and optional styling information + * @param query The query object describing which chats can be picked + */ + static switchInlineChosen( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + query: SwitchInlineQueryChosenChat = {}, + ): InlineKeyboardButton.SwitchInlineChosenChatButton { + return typeof text === "string" + ? { text, switch_inline_query_chosen_chat: query } + : { ...text, switch_inline_query_chosen_chat: query }; + } + /** + * Adds a new copy text button. When clicked, the specified text will be + * copied to the clipboard. + * + * @param text The text to display, and optional styling information + * @param copyText The text to be copied to the clipboard + */ + copyText( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + copyText: string | CopyTextButton, + ) { + return this.add(InlineKeyboard.copyText(text, copyText)); + } + /** + * Creates a new copy text button. When clicked, the specified text will be + * copied to the clipboard. + * + * @param text The text to display, and optional styling information + * @param copyText The text to be copied to the clipboard + */ + static copyText( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + copyText: string | CopyTextButton, + ): InlineKeyboardButton.CopyTextButtonButton { + const copy_text = typeof copyText === "string" + ? { text: copyText } + : copyText; + return typeof text === "string" + ? { text, copy_text } + : { ...text, copy_text }; + } + /** + * Adds a new game query button, confer + * https://core.telegram.org/bots/api#games + * + * This type of button must always be the first button in the first row. + * + * @param text The text to display, and optional styling information + */ + game(text: string | InlineKeyboardButton.AbstractInlineKeyboardButton) { + return this.add(InlineKeyboard.game(text)); + } + /** + * Creates a new game query button, confer + * https://core.telegram.org/bots/api#games + * + * This type of button must always be the first button in the first row. + * + * @param text The text to display, and optional styling information + */ + static game( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + ): InlineKeyboardButton.GameButton { + const callback_game = {}; + return typeof text === "string" + ? { text, callback_game } + : { ...text, callback_game }; + } + /** + * Adds a new payment button, confer + * https://core.telegram.org/bots/api#payments + * + * This type of button must always be the first button in the first row and + * can only be used in invoice messages. + * + * @param text The text to display, and optional styling information. Substrings β€œβ­β€ and β€œXTR” in the buttons's text will be replaced with a Telegram Star icon. + */ + pay(text: string | InlineKeyboardButton.AbstractInlineKeyboardButton) { + return this.add(InlineKeyboard.pay(text)); + } + /** + * Create a new payment button, confer + * https://core.telegram.org/bots/api#payments + * + * This type of button must always be the first button in the first row and + * can only be used in invoice messages. + * + * @param text The text to display, and optional styling information. Substrings β€œβ­β€ and β€œXTR” in the buttons's text will be replaced with a Telegram Star icon. + */ + static pay( + text: string | InlineKeyboardButton.AbstractInlineKeyboardButton, + ): InlineKeyboardButton.PayButton { + const pay = true; + return typeof text === "string" ? { text, pay } : { ...text, pay }; + } + /** + * Adds a style to the last added button of the inline keyboard. + * + * ```ts + * const keyboard = new InlineKeyboard() + * .text('blue button') + * .style('primary') + * ``` + * + * @param style Style of the button + */ + style(style: InlineKeyboardButton.AbstractInlineKeyboardButton["style"]) { + const rows = this.inline_keyboard.length; + if (rows === 0) { + throw new Error("Need to add a button before applying a style!"); + } + const lastRow = this.inline_keyboard[rows - 1]; + const cols = lastRow.length; + if (cols === 0) { + throw new Error("Need to add a button before applying a style!"); + } + lastRow[cols - 1].style = style; + return this; + } + /** + * Adds a danger style to the last added button of the inline keyboard. + * Alias for `.style('danger')`. + * + * ```ts + * const keyboard = new InlineKeyboard() + * .text('red button') + * .danger() + * ``` + */ + danger() { + return this.style("danger"); + } + /** + * Adds a success style to the last added button of the inline keyboard. + * Alias for `.style('success')`. + * + * ```ts + * const keyboard = new InlineKeyboard() + * .text('green button') + * .success() + * ``` + */ + success() { + return this.style("success"); + } + /** + * Adds a primary style to the last added button of the inline keyboard. + * Alias for `.style('primary')`. + * + * ```ts + * const keyboard = new InlineKeyboard() + * .text('blue button') + * .primary() + * ``` + */ + primary() { + return this.style("primary"); + } + /** + * Adds a custom emoji icon to the last added button of the inline keyboard. + * + * ```ts + * const keyboard = new InlineKeyboard() + * .text('button with icon') + * .icon(myCustomEmojiIconIdentifier) + * ``` + * + * @param icon Unique identifier of the custom emoji shown before the text of the button + */ + icon( + icon: InlineKeyboardButton.AbstractInlineKeyboardButton[ + "icon_custom_emoji_id" + ], + ) { + const rows = this.inline_keyboard.length; + if (rows === 0) { + throw new Error("Need to add a button before adding an icon!"); + } + const lastRow = this.inline_keyboard[rows - 1]; + const cols = lastRow.length; + if (cols === 0) { + throw new Error("Need to add a button before adding an icon!"); + } + lastRow[cols - 1].icon_custom_emoji_id = icon; + return this; + } + /** + * Creates a new inline keyboard that contains the transposed grid of + * buttons of this inline keyboard. This means that the resulting inline + * keyboard has the rows and columns flipped. + * + * Note that inline buttons can only span multiple columns, but never + * multiple rows. This means that if the given arrays have different + * lengths, some buttons might flow up in the layout. In these cases, + * transposing an inline keyboard a second time will not undo the first + * transposition. + * + * Here are some examples. + * + * ``` + * original transposed + * [ a ] ~> [ a ] + * + * [ a ] + * [a b c] ~> [ b ] + * [ c ] + * + * [ a b ] [a c e] + * [ c d ] ~> [ b d ] + * [ e ] + * + * [ a b ] [a c d] + * [ c ] ~> [ b e ] + * [d e f] [ f ] + * ``` + */ + toTransposed() { + const original = this.inline_keyboard; + const transposed = transpose(original); + return new InlineKeyboard(transposed); + } + /** + * Creates a new inline keyboard with the same buttons but reflowed into a + * given number of columns as if the buttons were text elements. Optionally, + * you can specify if the flow should make sure to fill up the last row. + * + * This method is idempotent, so calling it a second time will effectively + * clone this inline keyboard without reordering the buttons. + * + * Here are some examples. + * + * ``` + * original flowed + * [ a ] ~> [ a ] (4 columns) + * + * [ a ] + * [a b c] ~> [ b ] (1 column) + * [ c ] + * + * [ a b ] [a b c] + * [ c d ] ~> [ d e ] (3 columns) + * [ e ] + * + * [ a b ] [abcde] + * [ c ] ~> [ f ] (5 columns) + * [d e f] + * + * [a b c] [ a ] + * [d e f] ~> [b c d] (3 columns, { fillLastRow: true }) + * [g h i] [e f g] + * [ j ] [h i j] + * ``` + * + * @param columns Maximum number of buttons per row + * @param options Optional flowing behavior + */ + toFlowed(columns: number, options: FlowOptions = {}) { + const original = this.inline_keyboard; + const flowed = reflow(original, columns, options); + return new InlineKeyboard(flowed); + } + /** + * Creates and returns a deep copy of this inline keyboard. + */ + clone() { + return new InlineKeyboard( + this.inline_keyboard.map((row) => row.slice()), + ); + } + /** + * Appends the buttons of the given inline keyboards to this keyboard. + * + * @param sources A number of inline keyboards to append + */ + append(...sources: InlineKeyboardSource[]) { + for (const source of sources) { + const keyboard = InlineKeyboard.from(source); + this.inline_keyboard.push( + ...keyboard.inline_keyboard.map((row) => row.slice()), + ); + } + return this; + } + /** + * Turns a two-dimensional inline button array into an inline keyboard + * instance. You can use the static button builder methods to create inline + * button objects. + * + * @param source A two-dimensional inline button array + */ + static from(source: InlineKeyboardSource): InlineKeyboard { + if (source instanceof InlineKeyboard) return source.clone(); + return new InlineKeyboard(source.map((row) => row.slice())); + } +} + +function transpose(grid: T[][]): T[][] { + const transposed: T[][] = []; + for (let i = 0; i < grid.length; i++) { + const row = grid[i]; + for (let j = 0; j < row.length; j++) { + const button = row[j]; + (transposed[j] ??= []).push(button); + } + } + return transposed; +} +interface FlowOptions { + /** Set to `true` to completely fill up the last row */ + fillLastRow?: boolean; +} +function reflow( + grid: T[][], + columns: number, + { fillLastRow = false }: FlowOptions, +): T[][] { + let first = columns; + if (fillLastRow) { + const buttonCount = grid + .map((row) => row.length) + .reduce((a, b) => a + b, 0); + first = buttonCount % columns; + } + const reflowed: T[][] = []; + for (const row of grid) { + for (const button of row) { + const at = Math.max(0, reflowed.length - 1); + const max = at === 0 ? first : columns; + let next = (reflowed[at] ??= []); + if (next.length === max) { + next = []; + reflowed.push(next); + } + next.push(button); + } + } + return reflowed; +} diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 0000000..c990fb2 --- /dev/null +++ b/src/mod.ts @@ -0,0 +1,60 @@ +// Commonly used stuff +export { + Bot, + type BotConfig, + BotError, + type ErrorHandler, + type PollingOptions, +} from "./bot.ts"; + +export { InputFile } from "./types.ts"; + +export { + type CallbackQueryContext, + type ChatTypeContext, + type ChosenInlineResultContext, + type CommandContext, + Context, + type GameQueryContext, + type HearsContext, + type InlineQueryContext, + type ReactionContext, +} from "./context.ts"; + +// Convenience stuff, built-in plugins, and helpers +export * from "./convenience/constants.ts"; +export * from "./convenience/inline_query.ts"; +export * from "./convenience/input_media.ts"; +export * from "./convenience/keyboard.ts"; +export * from "./convenience/session.ts"; +export * from "./convenience/webhook.ts"; + +// A little more advanced stuff +export { + type CallbackQueryMiddleware, + type ChatTypeMiddleware, + type CommandMiddleware, + Composer, + type GameQueryMiddleware, + type HearsMiddleware, + type InlineQueryMiddleware, + type Middleware, + type MiddlewareFn, + type MiddlewareObj, + type NextFunction, + type ReactionMiddleware, +} from "./composer.ts"; + +export { type Filter, type FilterQuery, matchFilter } from "./filter.ts"; + +// Internal stuff for expert users +export { Api } from "./core/api.ts"; +export { + type ApiCallFn, + type ApiClientOptions, + type RawApi, + type TransformableApi, + type Transformer, + type WebhookReplyEnvelope, +} from "./core/client.ts"; +export { GrammyError, HttpError } from "./core/error.ts"; diff --git a/src/payload.ts b/src/payload.ts new file mode 100644 index 0000000..569e7e0 --- /dev/null +++ b/src/payload.ts @@ -0,0 +1,227 @@ +import { itrToStream } from "../platform.deno.ts"; +import { InputFile } from "../types.ts"; + +// === Payload types (JSON vs. form data) +/** + * Determines for a given payload if it may be sent as JSON, or if it has to be + * uploaded via multipart/form-data. Returns `true` in the latter case and + * `false` in the former. + * + * @param payload The payload to analyze + */ +export function requiresFormDataUpload(payload: unknown): boolean { + return payload instanceof InputFile || ( + typeof payload === "object" && + payload !== null && + Object.values(payload).some((v) => + Array.isArray(v) + ? v.some(requiresFormDataUpload) + : v instanceof InputFile || requiresFormDataUpload(v) + ) + ); +} +/** + * Calls `JSON.stringify` but removes `null` values from objects before + * serialization + * + * @param value value + * @returns stringified value + */ +function str(value: unknown) { + return JSON.stringify(value, (_, v) => v ?? undefined); +} +/** + * Turns a payload into an options object that can be passed to a `fetch` call + * by setting the necessary headers and method. May only be called for payloads + * `P` that let `requiresFormDataUpload(P)` return `false`. + * + * @param payload The payload to wrap + */ +export function createJsonPayload(payload: Record) { + return { + method: "POST", + headers: { + "content-type": "application/json", + connection: "keep-alive", + }, + body: str(payload), + }; +} +async function* protectItr( + itr: AsyncIterableIterator, + onError: (err: unknown) => void, +) { + try { + yield* itr; + } catch (err) { + onError(err); + } +} +/** + * Turns a payload into an options object that can be passed to a `fetch` call + * by setting the necessary headers and method. Note that this method creates a + * multipart/form-data stream under the hood. If possible, a JSON payload should + * be created instead for performance reasons. + * + * @param payload The payload to wrap + */ +export function createFormDataPayload( + payload: Record, + onError: (err: unknown) => void, +) { + const boundary = createBoundary(); + const itr = payloadToMultipartItr(payload, boundary); + const safeItr = protectItr(itr, onError); + const stream = itrToStream(safeItr); + return { + method: "POST", + headers: { + "content-type": `multipart/form-data; boundary=${boundary}`, + connection: "keep-alive", + }, + body: stream, + }; +} + +// === Form data creation +function createBoundary() { + // Taken from Deno std lib + return "----------" + randomId(32); +} +function randomId(length = 16) { + return Array.from(Array(length)) + .map(() => Math.random().toString(36)[2] || 0) + .join(""); +} + +const enc = new TextEncoder(); +/** + * Takes a payload object and produces a valid multipart/form-data stream. The + * stream is an iterator of `Uint8Array` objects. You also need to specify the + * boundary string that was used in the Content-Type header of the HTTP request. + * + * @param payload a payload object + * @param boundary the boundary string to use between the parts + */ +async function* payloadToMultipartItr( + payload: Record, + boundary: string, +): AsyncIterableIterator { + const files = collectFiles(payload); + // Start multipart/form-data protocol + yield enc.encode(`--${boundary}\r\n`); + // Send all payload fields + const separator = enc.encode(`\r\n--${boundary}\r\n`); + let first = true; + for (const [key, value] of Object.entries(payload)) { + if (value == null) continue; + if (!first) yield separator; + yield valuePart( + key, + value instanceof InputFile + ? value.toJSON() + : typeof value === "object" + ? str(value) + : value, + ); + first = false; + } + // Send all files + for (const { id, origin, file } of files) { + if (!first) yield separator; + yield* filePart(id, origin, file); + first = false; + } + // End multipart/form-data protocol + yield enc.encode(`\r\n--${boundary}--\r\n`); +} + +/** Information about a file extracted from a payload */ +type CollectedFile = { + /** To be used in the attach:// string */ + id: string; + /** Hints about where the file came from, useful for filename guessing */ + origin: string; + /** The extracted file */ + file: InputFile; +}; +/** + * Installs a `toJSON` implementation on each instance of `InputFile` contained + * in the payload. They return attach:// strings under which the respective + * instances should be sent. The modified payload can now be serialized to JSON. + * + * Returns the list of discovered `InputFile` instances along with the random + * identifiers that were used in the corresponding attach:// strings, as well as + * the origin keys of the original payload object. + * + * @param value a payload object, or a part of it + * @returns the discovered `InputFile` instances with identifiers and origins + */ +function collectFiles(value: unknown): CollectedFile[] { + if (typeof value !== "object" || value === null) return []; + return Object.entries(value).flatMap(([k, v]) => { + if (Array.isArray(v)) return v.flatMap((p) => collectFiles(p)); + else if (v instanceof InputFile) { + const id = randomId(); + // Serialize `InputFile` instance with attach:// string + Object.assign(v, { toJSON: () => `attach://${id}` }); + const origin = k === "media" && + "type" in value && typeof value.type === "string" + ? value.type // use `type` for `InputMedia*` + : k; // use property key otherwise + return { id, origin, file: v }; + } else return collectFiles(v); + }); +} + +/** Turns a regular value into a `Uint8Array` */ +function valuePart(key: string, value: unknown): Uint8Array { + return enc.encode( + `content-disposition:form-data;name="${key}"\r\n\r\n${value}`, + ); +} +/** Turns an InputFile into a generator of `Uint8Array`s */ +async function* filePart( + id: string, + origin: string, + input: InputFile, +): AsyncIterableIterator { + const filename = input.filename || `${origin}.${getExt(origin)}`; + if (filename.includes("\r") || filename.includes("\n")) { + throw new Error( + `File paths cannot contain carriage-return (\\r) \ +or newline (\\n) characters! Filename for property '${origin}' was: +""" +${filename} +"""`, + ); + } + yield enc.encode( + `content-disposition:form-data;name="${id}";filename=${filename}\r\ncontent-type:application/octet-stream\r\n\r\n`, + ); + const data = await input.toRaw(); + if (data instanceof Uint8Array) yield data; + else yield* data; +} +/** Returns the default file extension for an API property name */ +function getExt(key: string) { + switch (key) { + case "certificate": + return "pem"; + case "photo": + case "thumbnail": + return "jpg"; + case "voice": + return "ogg"; + case "audio": + return "mp3"; + case "animation": + case "video": + case "video_note": + return "mp4"; + case "sticker": + return "webp"; + default: + return "dat"; + } +} diff --git a/src/platform.deno.ts b/src/platform.deno.ts new file mode 100644 index 0000000..6c41ad2 --- /dev/null +++ b/src/platform.deno.ts @@ -0,0 +1,29 @@ +// deno-lint-ignore-file no-import-prefix + +/** Are we running on Deno or in a web browser? */ +export const isDeno = typeof Deno !== "undefined"; + +// === Export debug +import debug from "https://cdn.skypack.dev/debug@4.4.3"; +export { debug }; +const DEBUG = "DEBUG"; +if (isDeno) { + debug.useColors = () => !Deno.noColor; + const env = { name: "env", variable: DEBUG } as const; + const res = await Deno.permissions.query(env); + let namespace: string | undefined = undefined; + if (res.state === "granted") namespace = Deno.env.get(DEBUG); + if (namespace) debug.enable(namespace); + else debug.disable(); +} + +// === Export system-specific operations +// Turn an AsyncIterable into a stream +export const itrToStream = (itr: AsyncIterable) => + ReadableStream.from(itr); + +// === Base configuration for `fetch` calls +export const baseFetchConfig = (_apiRoot: string) => ({ duplex: "half" }); + +// === Default webhook adapter +export const defaultAdapter = "oak"; diff --git a/src/platform.node.ts b/src/platform.node.ts new file mode 100644 index 0000000..6d632d3 --- /dev/null +++ b/src/platform.node.ts @@ -0,0 +1,49 @@ +// === Needed imports +import { Agent as HttpAgent } from "http"; +import { Agent as HttpsAgent } from "https"; +import { Readable } from "stream"; + +// === Export debug +export { debug } from "debug"; + +// === Export system-specific operations +// Turn an AsyncIterable into a stream +export const itrToStream = (itr: AsyncIterable) => + Readable.from(itr, { objectMode: false }); + +// === Base configuration for `fetch` calls +const httpAgents = new Map(); +const httpsAgents = new Map(); +function getCached(map: Map, key: K, otherwise: () => V) { + let value = map.get(key); + if (value === undefined) { + value = otherwise(); + map.set(key, value); + } + return value; +} +export function baseFetchConfig(apiRoot: string) { + if (apiRoot.startsWith("https:")) { + return { + compress: true, + agent: getCached( + httpsAgents, + apiRoot, + () => new HttpsAgent({ keepAlive: true }), + ), + duplex: "half", + }; + } else if (apiRoot.startsWith("http:")) { + return { + agent: getCached( + httpAgents, + apiRoot, + () => new HttpAgent({ keepAlive: true }), + ), + duplex: "half", + }; + } else return { duplex: "half" }; +} + +// === Default webhook adapter +export const defaultAdapter = "express"; diff --git a/src/platform.web.ts b/src/platform.web.ts new file mode 100644 index 0000000..5e9af92 --- /dev/null +++ b/src/platform.web.ts @@ -0,0 +1,23 @@ +// deno-lint-ignore-file no-import-prefix + +import d from "https://cdn.skypack.dev/debug@4.4.3"; +export { d as debug }; + +// === Export system-specific operations +// Turn an AsyncIterable into a stream +export const itrToStream = (itr: AsyncIterable) => { + // do not assume ReadableStream.from to exist yet + const it = itr[Symbol.asyncIterator](); + return new ReadableStream({ + async pull(controller) { + const chunk = await it.next(); + if (chunk.done) controller.close(); + else controller.enqueue(chunk.value); + }, + }); +}; + +// === Base configuration for `fetch` calls +export const baseFetchConfig = (_apiRoot: string) => ({ duplex: "half" }); + +export const defaultAdapter = "cloudflare"; diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000..5d76711 --- /dev/null +++ b/src/session.ts @@ -0,0 +1,732 @@ +import { type MiddlewareFn } from "../composer.ts"; +import { type Context } from "../context.ts"; +import { debug as d } from "../platform.deno.ts"; +const debug = d("grammy:session"); + +type MaybePromise = Promise | T; + +// === Main session plugin +/** + * A session flavor is a context flavor that holds session data under + * `ctx.session`. + * + * Session middleware will load the session data of a specific chat from your + * storage solution, and make it available to you on the context object. Check + * out the [documentation](https://grammy.dev/ref/core/session) on session + * middleware to know more, and read the section about sessions on the + * [website](https://grammy.dev/plugins/session). + */ +export interface SessionFlavor { + /** + * Session data on the context object. + * + * **WARNING:** You have to make sure that your session data is not + * undefined by _providing an initial value to the session middleware_, or + * by making sure that `ctx.session` is assigned if it is empty! The type + * system does not include `| undefined` because this is really annoying to + * work with. + * + * Accessing `ctx.session` by reading or writing will throw if + * `getSessionKey(ctx) === undefined` for the respective context object + * `ctx`. + */ + get session(): S; + set session(session: S | null | undefined); +} +/** + * A lazy session flavor is a context flavor that holds a promise of some + * session data under `ctx.session`. + * + * Lazy session middleware will provide this promise lazily on the context + * object. Once you access `ctx.session`, the storage will be queried and the + * session data becomes available. If you access `ctx.session` again for the + * same context object, the cached value will be used. Check out the + * [documentation](https://grammy.dev/ref/core/lazysession) on lazy session + * middleware to know more, and read the section about lazy sessions on the + * [website](https://grammy.dev/plugins/session#lazy-sessions). + */ +export interface LazySessionFlavor { + /** + * Session data on the context object, potentially a promise. + * + * **WARNING:** You have to make sure that your session data is not + * undefined by _providing a default value to the session middleware_, or by + * making sure that `ctx.session` is assigned if it is empty! The type + * system does not include `| undefined` because this is really annoying to + * work with. + * + * Accessing `ctx.session` by reading or writing will throw iff + * `getSessionKey(ctx) === undefined` holds for the respective context + * object `ctx`. + */ + get session(): MaybePromise; + set session(session: MaybePromise); +} + +/** + * A storage adapter is an abstraction that provides read, write, and delete + * access to a storage solution of any kind. Storage adapters are used to keep + * session middleware independent of your database provider, and they allow you + * to pass your own storage solution. + */ +export interface StorageAdapter { + /** + * Reads a value for the given key from the storage. May return the value or + * undefined, or a promise of either. + */ + read: (key: string) => MaybePromise; + /** + * Writes a value for the given key to the storage. + */ + write: (key: string, value: T) => MaybePromise; + /** + * Deletes a value for the given key from the storage. + */ + delete: (key: string) => MaybePromise; + /** + * Checks whether a key exists in the storage. + */ + has?: (key: string) => MaybePromise; + /** + * Lists all keys. + */ + readAllKeys?: () => Iterable | AsyncIterable; + /** + * Lists all values. + */ + readAllValues?: () => Iterable | AsyncIterable; + /** + * Lists all keys with their values. + */ + readAllEntries?: () => + | Iterable<[key: string, value: T]> + | AsyncIterable<[key: string, value: T]>; +} + +/** + * Options for session middleware. + */ +export interface SessionOptions { + type?: "single"; + /** + * **Recommended to use.** + * + * A function that produces an initial value for `ctx.session`. This + * function will be called every time the storage solution returns undefined + * for a given session key. Make sure to create a new value every time, such + * that different context objects do that accidentally share the same + * session data. + */ + initial?: () => S; + /** + * An optional prefix to prepend to the session key after it was generated. + * + * This makes it easier to store session data under a namespace. You can + * technically achieve the same functionality by returning an already + * prefixed key from `getSessionKey`. This option is merely more convenient, + * as it does not require you to think about session key generation. + */ + prefix?: string; + /** + * This option lets you generate your own session keys per context object. + * The session key determines how to map the different session objects to + * your chats and users. Check out the + * [documentation](https://grammy.dev/plugins/session#how-to-use-sessions) + * on the website about how to use session middleware to know how session + * keys are used. + * + * The default implementation will store sessions per chat, as determined by + * `ctx.chatId`. + */ + getSessionKey?: ( + ctx: Omit, + ) => MaybePromise; + /** + * A storage adapter to your storage solution. Provides read, write, and + * delete access to the session middleware. + * + * Consider using a [known storage + * adapter](https://grammy.dev/plugins/session#known-storage-adapters) + * instead of rolling your own implementation of this. + * + * The default implementation will store session in memory. The data will be + * lost whenever your bot restarts. + */ + storage?: StorageAdapter; +} + +/** + * Options for session middleware if multi sessions are used. Specify `"type": + * "multi"` in the options to use multi sessions. + */ +export type MultiSessionOptions = + // deno-lint-ignore no-explicit-any + S extends Record // unknown breaks extends + ? { type: "multi" } & MultiSessionOptionsRecord + : never; +type MultiSessionOptionsRecord< + S extends Record, + C extends Context, +> = { + [K in keyof S]: SessionOptions; +}; + +/** + * Session middleware provides a persistent data storage for your bot. You can + * use it to let your bot remember any data you want, for example the messages + * it sent or received in the past. This is done by attaching _session data_ to + * every chat. The stored data is then provided on the context object under + * `ctx.session`. + * + * > **What is a session?** Simply put, the session of a chat is a little + * > persistent storage that is attached to it. As an example, your bot can send + * > a message to a chat and store the identifier of that message in the + * > corresponding session. The next time your bot receives an update from that + * > chat, the session will still contain that ID. + * > + * > Session data can be stored in a database, in a file, or simply in memory. + * > grammY only supports memory sessions out of the box, but you can use + * > third-party session middleware to connect to other storage solutions. Note + * > that memory sessions will be lost when you stop your bot and the process + * > exits, so they are usually not useful in production. + * + * Whenever your bot receives an update, the first thing the session middleware + * will do is to load the correct session from your storage solution. This + * object is then provided on `ctx.session` while your other middleware is + * running. As soon as your bot is done handling the update, the middleware + * takes over again and writes back the session object to your storage. This + * allows you to modify the session object arbitrarily in your middleware, and + * to stop worrying about the database. + * + * ```ts + * bot.use(session()) + * + * bot.on('message', ctx => { + * // The session object is persisted across updates! + * const session = ctx.session + * }) + * ``` + * + * It is recommended to make use of the `initial` option in the configuration + * object, which correctly initializes session objects for new chats. + * + * You can delete the session data by setting `ctx.session` to `null` or + * `undefined`. + * + * Check out the [documentation](https://grammy.dev/plugins/session) on the + * website to know more about how sessions work in grammY. + * + * @param options Optional configuration to pass to the session middleware + */ +export function session( + options: SessionOptions | MultiSessionOptions = {}, +): MiddlewareFn> { + return options.type === "multi" + ? strictMultiSession(options) + : strictSingleSession(options); +} + +function strictSingleSession( + options: SessionOptions, +): MiddlewareFn> { + const { initial, storage, getSessionKey, custom } = fillDefaults(options); + return async (ctx, next) => { + const propSession = new PropertySession, "session">( + storage, + ctx, + "session", + initial, + ); + const key = await getSessionKey(ctx); + await propSession.init(key, { custom, lazy: false }); + await next(); // no catch: do not write back if middleware throws + await propSession.finish(); + }; +} +function strictMultiSession( + options: MultiSessionOptions, +): MiddlewareFn> { + const props = Object.keys(options).filter((k) => k !== "type"); + const defaults = Object.fromEntries( + props.map((prop) => [prop, fillDefaults(options[prop])]), + ); + return async (ctx, next) => { + ctx.session = {} as S; + const propSessions = await Promise.all(props.map(async (prop) => { + const { initial, storage, getSessionKey, custom } = defaults[prop]; + const s = new PropertySession( + // @ts-expect-error cannot express that the storage works for a concrete prop + storage, + ctx.session, + prop, + initial, + ); + const key = await getSessionKey(ctx); + await s.init(key, { custom, lazy: false }); + return s; + })); + await next(); // no catch: do not write back if middleware throws + if (ctx.session == null) propSessions.forEach((s) => s.delete()); + await Promise.all(propSessions.map((s) => s.finish())); + }; +} + +/** + * > This is an advanced function of grammY. + * + * Generally speaking, lazy sessions work just like normal sessionsβ€”just they + * are loaded on demand. Except for a few `async`s and `await`s here and there, + * their usage looks 100 % identical. + * + * Instead of directly querying the storage every time an update arrives, lazy + * sessions quickly do this _once you access_ `ctx.session`. This can + * significantly reduce the database traffic (especially when your bot is added + * to group chats), because it skips a read and a wrote operation for all + * updates that the bot does not react to. + * + * ```ts + * // The options are identical + * bot.use(lazySession({ storage: ... })) + * + * bot.on('message', async ctx => { + * // The session object is persisted across updates! + * const session = await ctx.session + * // ^ + * // | + * // This plain property access (no function call) will trigger the database query! + * }) + * ``` + * + * Check out the + * [documentation](https://grammy.dev/plugins/session#lazy-sessions) on the + * website to know more about how lazy sessions work in grammY. + * + * @param options Optional configuration to pass to the session middleware + */ +export function lazySession( + options: SessionOptions = {}, +): MiddlewareFn> { + if (options.type !== undefined && options.type !== "single") { + throw new Error("Cannot use lazy multi sessions!"); + } + const { initial, storage, getSessionKey, custom } = fillDefaults(options); + return async (ctx, next) => { + const propSession = new PropertySession( + // @ts-expect-error suppress promise nature of values + storage, + ctx, + "session", + initial, + ); + const key = await getSessionKey(ctx); + await propSession.init(key, { custom, lazy: true }); + await next(); // no catch: do not write back if middleware throws + await propSession.finish(); + }; +} + +/** + * Internal class that manages a single property on the session. Can be used + * both in a strict and a lazy way. Works by using `Object.defineProperty` to + * install `O[P]`. + */ +// deno-lint-ignore ban-types +class PropertySession { + private key?: string; + private value: O[P] | undefined; + private promise: Promise | undefined; + + private fetching = false; + private read = false; + private wrote = false; + + constructor( + private storage: StorageAdapter, + private obj: O, + private prop: P, + private initial: (() => O[P]) | undefined, + ) {} + + /** Performs a read op and stores the result in `this.value` */ + private load() { + if (this.key === undefined) { + // No session key provided, cannot load + return; + } + if (this.wrote) { + // Value was set, no need to load + return; + } + // Perform read op if not cached + if (this.promise === undefined) { + this.fetching = true; + this.promise = Promise.resolve(this.storage.read(this.key)) + .then((val?: O[P]) => { + this.fetching = false; + // Check for write op in the meantime + if (this.wrote) { + // Discard read op + return this.value; + } + // Store received value in `this.value` + if (val !== undefined) { + this.value = val; + return val; + } + // No value, need to initialize + val = this.initial?.(); + if (val !== undefined) { + // Wrote initial value + this.wrote = true; + this.value = val; + } + return val; + }); + } + return this.promise; + } + + async init( + key: string | undefined, + opts: { custom: boolean; lazy: boolean }, + ) { + this.key = key; + if (!opts.lazy) await this.load(); + Object.defineProperty(this.obj, this.prop, { + enumerable: true, + get: () => { + if (key === undefined) { + const msg = undef("access", opts); + throw new Error(msg); + } + this.read = true; + if (!opts.lazy || this.wrote) return this.value; + this.load(); + return this.fetching ? this.promise : this.value; + }, + set: (v) => { + if (key === undefined) { + const msg = undef("assign", opts); + throw new Error(msg); + } + this.wrote = true; + this.fetching = false; + this.value = v; + }, + }); + } + + delete() { + Object.assign(this.obj, { [this.prop]: undefined }); + } + + async finish() { + if (this.key !== undefined) { + if (this.read) await this.load(); + if (this.read || this.wrote) { + const value = await this.value; + if (value == null) await this.storage.delete(this.key); + else await this.storage.write(this.key, value); + } + } + } +} + +function fillDefaults(opts: SessionOptions = {}) { + let { + prefix = "", + getSessionKey = defaultGetSessionKey, + initial, + storage, + } = opts; + if (storage == null) { + debug( + "Storing session data in memory, all data will be lost when the bot restarts.", + ); + storage = new MemorySessionStorage(); + } + const custom = getSessionKey !== defaultGetSessionKey; + return { + initial, + storage, + getSessionKey: async (ctx: C) => { + const key = await getSessionKey(ctx); + return key === undefined ? undefined : prefix + key; + }, + custom, + }; +} + +/** Stores session data per chat by default */ +function defaultGetSessionKey(ctx: Context): string | undefined { + return ctx.chatId?.toString(); +} + +/** Returns a useful error message for when the session key is undefined */ +function undef( + op: "access" | "assign", + opts: { custom: boolean; lazy?: boolean }, +) { + const { lazy = false, custom } = opts; + const reason = custom + ? "the custom `getSessionKey` function returned undefined for this update" + : "this update does not belong to a chat, so the session key is undefined"; + return `Cannot ${op} ${lazy ? "lazy " : ""}session data because ${reason}!`; +} + +// === Session migrations +/** + * When enhancing a storage adapter, it needs to be able to store additional + * information. It does this by wrapping the actual data inside an object, and + * adding more properties to this wrapper. + * + * This interface defines the additional properties that need to be stored by a + * storage adapter that supports enhanced sessions. + */ +export interface Enhance { + /** Version */ + v?: number; + /** Data */ + __d: T; + /** Expiry date */ + e?: number; +} +function isEnhance(value?: T | Enhance): value is Enhance | undefined { + return value === undefined || + typeof value === "object" && value !== null && "__d" in value; +} +/** Options for enhanced sessions */ +export interface MigrationOptions { + /** The original storage adapter that will be enhanced */ + storage: StorageAdapter>; + /** + * A set of session migrations, defined as an object mapping from version + * numbers to migration functions that transform data to the respective + * version. + */ + migrations?: Migrations; + /** + * Number of milliseconds after the last write operation until the session + * data expires. + */ + millisecondsToLive?: number; +} +/** + * A mapping from version numbers to session migration functions. Each entry in + * this object has a version number as a key, and a function as a value. + * + * For a key `n`, the respective value should be a function that takes the + * previous session data and migrates it to conform with the data that is used + * by version `n`. The previous session data is defined by the next key less + * than `n`, such as `n-1`. Versions don't have to be integers, nor do all + * versions have to be adjacent. For example, you can use `[1, 1.5, 4]` as + * versions. If `n` is the lowest value in the set of keys, the function stored + * for `n` can be used to migrate session data that was stored before migrations + * were used. + */ +export interface Migrations { + // deno-lint-ignore no-explicit-any + [version: number]: (old: any) => any; +} + +/** + * You can use this function to transform an existing storage adapter, and add + * more features to it. Currently, you can add session migrations and expiry + * dates. + * + * You can use this function like so: + * ```ts + * const storage = ... // define your storage adapter + * const enhanced = enhanceStorage({ storage, millisecondsToLive: 500 }) + * bot.use(session({ storage: enhanced })) + * ``` + * + * @param options Session enhancing options + * @returns The enhanced storage adapter + */ +export function enhanceStorage( + options: MigrationOptions, +): StorageAdapter { + let { storage, millisecondsToLive, migrations } = options; + storage = compatStorage(storage); + if (millisecondsToLive !== undefined) { + storage = timeoutStorage(storage, millisecondsToLive); + } + if (migrations !== undefined) { + storage = migrationStorage(storage, migrations); + } + return wrapStorage(storage); +} + +function compatStorage( + storage: StorageAdapter>, +): StorageAdapter> { + return { + read: async (k) => { + const v = await storage.read(k); + return isEnhance(v) ? v : { __d: v }; + }, + write: (k, v) => storage.write(k, v), + delete: (k) => storage.delete(k), + }; +} + +function timeoutStorage( + storage: StorageAdapter>, + millisecondsToLive: number, +): StorageAdapter> { + const ttlStorage: StorageAdapter> = { + read: async (k) => { + const value = await storage.read(k); + if (value === undefined) return undefined; + if (value.e === undefined) { + await ttlStorage.write(k, value); + return value; + } + if (value.e < Date.now()) { + await ttlStorage.delete(k); + return undefined; + } + return value; + }, + write: async (k, v) => { + v.e = addExpiryDate(v, millisecondsToLive).expires; + await storage.write(k, v); + }, + delete: (k) => storage.delete(k), + }; + return ttlStorage; +} +function migrationStorage( + storage: StorageAdapter>, + migrations: Migrations, +): StorageAdapter> { + const versions = Object.keys(migrations) + .map((v) => parseInt(v)) + .sort((a, b) => a - b); + const count = versions.length; + if (count === 0) throw new Error("No migrations given!"); + const earliest = versions[0]; + const last = count - 1; + const latest = versions[last]; + const index = new Map(); + versions.forEach((v, i) => index.set(v, i)); // inverse array lookup + function nextAfter(current: number) { + // TODO: use `findLastIndex` with Node 18 + let i = last; + while (current <= versions[i]) i--; + return i; + // return versions.findLastIndex((v) => v < current) + } + return { + read: async (k) => { + const val = await storage.read(k); + if (val === undefined) return val; + let { __d: value, v: current = earliest - 1 } = val; + let i = 1 + (index.get(current) ?? nextAfter(current)); + for (; i < count; i++) value = migrations[versions[i]](value); + return { ...val, v: latest, __d: value }; + }, + write: (k, v) => storage.write(k, { v: latest, ...v }), + delete: (k) => storage.delete(k), + }; +} +function wrapStorage( + storage: StorageAdapter>, +): StorageAdapter { + return { + read: (k) => Promise.resolve(storage.read(k)).then((v) => v?.__d), + write: (k, v) => storage.write(k, { __d: v }), + delete: (k) => storage.delete(k), + }; +} + +// === Memory storage adapter +/** + * The memory session storage is a built-in storage adapter that saves your + * session data in RAM using a regular JavaScript `Map` object. If you use this + * storage adapter, all sessions will be lost when your process terminates or + * restarts. Hence, you should only use it for short-lived data that is not + * important to persist. + * + * This class is used as default if you do not provide a storage adapter, e.g. + * to your database. + * + * This storage adapter features expiring sessions. When instantiating this + * class yourself, you can pass a time to live in milliseconds that will be used + * for each session object. If a session for a user expired, the session data + * will be discarded on its first read, and a fresh session object as returned + * by the `initial` option (or undefined) will be put into place. + */ +export class MemorySessionStorage implements StorageAdapter { + /** + * Internally used `Map` instance that stores the session data + */ + protected readonly storage = new Map< + string, + { session: S; expires?: number } + >(); + + /** + * Constructs a new memory session storage with the given time to live. Note + * that this storage adapter will not store your data permanently. + * + * @param timeToLive TTL in milliseconds, default is `Infinity` + */ + constructor(private readonly timeToLive?: number) {} + + read(key: string) { + const value = this.storage.get(key); + if (value === undefined) return undefined; + if (value.expires !== undefined && value.expires < Date.now()) { + this.delete(key); + return undefined; + } + return value.session; + } + + /** + * @deprecated Use {@link readAllValues} instead + */ + readAll() { + return this.readAllValues(); + } + + readAllKeys() { + return Array.from(this.storage.keys()); + } + + readAllValues() { + return Array + .from(this.storage.keys()) + .map((key) => this.read(key)) + .filter((value): value is S => value !== undefined); + } + + readAllEntries() { + return Array.from(this.storage.keys()) + .map((key) => [key, this.read(key)]) + .filter((pair): pair is [string, S] => pair[1] !== undefined); + } + + has(key: string) { + return this.storage.has(key); + } + + write(key: string, value: S) { + this.storage.set(key, addExpiryDate(value, this.timeToLive)); + } + + delete(key: string) { + this.storage.delete(key); + } +} + +function addExpiryDate(value: T, ttl?: number) { + if (ttl !== undefined && ttl < Infinity) { + const now = Date.now(); + return { session: value, expires: now + ttl }; + } else { + return { session: value }; + } +} diff --git a/src/shim.node.ts b/src/shim.node.ts new file mode 100644 index 0000000..9823864 --- /dev/null +++ b/src/shim.node.ts @@ -0,0 +1,2 @@ +export { AbortController, type AbortSignal } from "abort-controller"; +export { default as fetch } from "node-fetch"; diff --git a/src/types.deno.ts b/src/types.deno.ts new file mode 100644 index 0000000..95e532f --- /dev/null +++ b/src/types.deno.ts @@ -0,0 +1,456 @@ +// deno-lint-ignore-file no-import-prefix + +// === Needed imports +import { basename } from "jsr:@std/path@1.1.2/basename"; + +import { + type ApiMethods as ApiMethodsF, + type InlineQueryResult as InlineQueryResultF, + type InlineQueryResultArticle as InlineQueryResultArticleF, + type InlineQueryResultAudio as InlineQueryResultAudioF, + type InlineQueryResultCachedAudio as InlineQueryResultCachedAudioF, + type InlineQueryResultCachedDocument as InlineQueryResultCachedDocumentF, + type InlineQueryResultCachedGif as InlineQueryResultCachedGifF, + type InlineQueryResultCachedMpeg4Gif as InlineQueryResultCachedMpeg4GifF, + type InlineQueryResultCachedPhoto as InlineQueryResultCachedPhotoF, + type InlineQueryResultCachedSticker as InlineQueryResultCachedStickerF, + type InlineQueryResultCachedVideo as InlineQueryResultCachedVideoF, + type InlineQueryResultCachedVoice as InlineQueryResultCachedVoiceF, + type InlineQueryResultContact as InlineQueryResultContactF, + type InlineQueryResultDocument as InlineQueryResultDocumentF, + type InlineQueryResultGame as InlineQueryResultGameF, + type InlineQueryResultGif as InlineQueryResultGifF, + type InlineQueryResultLocation as InlineQueryResultLocationF, + type InlineQueryResultMpeg4Gif as InlineQueryResultMpeg4GifF, + type InlineQueryResultPhoto as InlineQueryResultPhotoF, + type InlineQueryResultVenue as InlineQueryResultVenueF, + type InlineQueryResultVideo as InlineQueryResultVideoF, + type InlineQueryResultVoice as InlineQueryResultVoiceF, + type InputMedia as InputMediaF, + type InputMediaAnimation as InputMediaAnimationF, + type InputMediaAudio as InputMediaAudioF, + type InputMediaDocument as InputMediaDocumentF, + type InputMediaLivePhoto as InputMediaLivePhotoF, + type InputMediaPhoto as InputMediaPhotoF, + type InputMediaSticker as InputMediaStickerF, + type InputMediaVideo as InputMediaVideoF, + type InputMediaVoiceNote as InputMediaVoiceNoteF, + type InputMessageContent as InputMessageContentF, + type InputPaidMedia as InputPaidMediaF, + type InputPaidMediaLivePhoto as InputPaidMediaLivePhotoF, + type InputPaidMediaPhoto as InputPaidMediaPhotoF, + type InputPaidMediaVideo as InputPaidMediaVideoF, + type InputPollMedia as InputPollMediaF, + type InputPollOption as InputPollOptionF, + type InputPollOptionMedia as InputPollOptionMediaF, + type InputProfilePhoto as InputProfilePhotoAnimatedF, + type InputProfilePhoto as InputProfilePhotoF, + type InputProfilePhoto as InputProfilePhotoStaticF, + type InputRichBlock as InputRichBlockF, + type InputRichBlockAnimation as InputRichBlockAnimationF, + type InputRichBlockAudio as InputRichBlockAudioF, + type InputRichBlockBlockQuotation as InputRichBlockBlockQuotationF, + type InputRichBlockCollage as InputRichBlockCollageF, + type InputRichBlockDetails as InputRichBlockDetailsF, + type InputRichBlockList as InputRichBlockListF, + type InputRichBlockListItem as InputRichBlockListItemF, + type InputRichBlockPhoto as InputRichBlockPhotoF, + type InputRichBlockSlideshow as InputRichBlockSlideshowF, + type InputRichBlockVideo as InputRichBlockVideoF, + type InputRichBlockVoiceNote as InputRichBlockVoiceNoteF, + type InputRichMessage as InputRichMessageF, + type InputRichMessageContent as InputRichMessageContentF, + type InputRichMessageMedia as InputRichMessageMediaF, + type InputSticker as InputStickerF, + type InputStoryContent as InputStoryContentF, + type InputStoryContentPhoto as InputStoryContentPhotoF, + type InputStoryContentVideo as InputStoryContentVideoF, + type Opts as OptsF, +} from "https://deno.land/x/grammy_types@v4.0.0/mod.ts"; +import { debug as d, isDeno } from "./platform.deno.ts"; + +const debug = d("grammy:warn"); + +// === Export all API types +export * from "https://deno.land/x/grammy_types@v4.0.0/mod.ts"; + +/** A value, or a potentially async function supplying that value */ +type MaybeSupplier = T | (() => T | Promise); +/** Something that looks like a URL. */ +interface URLLike { + /** + * Identifier of the resource. Must be in a format that can be parsed by the + * URL constructor. + */ + url: string; +} + +// === InputFile handling and File augmenting +/** + * An `InputFile` wraps a number of different sources for [sending + * files](https://grammy.dev/guide/files#uploading-your-own-files). + * + * It corresponds to the `InputFile` type in the [Telegram Bot API + * Reference](https://core.telegram.org/bots/api#inputfile). + */ +export class InputFile { + private consumed = false; + private readonly fileData: ConstructorParameters[0]; + /** + * Optional name of the constructed `InputFile` instance. + * + * Check out the + * [documentation](https://grammy.dev/guide/files#uploading-your-own-files) + * on sending files with `InputFile`. + */ + public readonly filename?: string; + /** + * Constructs an `InputFile` that can be used in the API to send files. + * + * @param file A path to a local file or a `Buffer` or a `ReadableStream` that specifies the file data + * @param filename Optional name of the file + */ + constructor( + file: MaybeSupplier< + | string + | Blob + | Deno.FsFile + | Response + | URL + | URLLike + | Uint8Array + | ReadableStream + | Iterable + | AsyncIterable + >, + filename?: string, + ) { + this.fileData = file; + filename ??= this.guessFilename(file); + this.filename = filename; + if ( + typeof file === "string" && + (file.startsWith("http:") || file.startsWith("https:")) + ) { + debug( + `InputFile received the local file path '${file}' that looks like a URL. Is this a mistake?`, + ); + } + } + private guessFilename( + file: ConstructorParameters[0], + ): string | undefined { + if (typeof file === "string") return basename(file); + if ("url" in file) return basename(file.url); + if (!(file instanceof URL)) return undefined; + if (file.pathname !== "/") { + const filename = basename(file.pathname); + if (filename) return filename; + } + return basename(file.hostname); + } + /** + * Internal method. Do not use. + * + * Converts this instance into a binary representation that can be sent to + * the Bot API server in the request body. + */ + async toRaw(): Promise< + Uint8Array | Iterable | AsyncIterable + > { + if (this.consumed) { + throw new Error("Cannot reuse InputFile data source!"); + } + const data = this.fileData; + // Handle local files + if (typeof data === "string") { + if (!isDeno) { + throw new Error( + "Reading files by path requires a Deno environment", + ); + } + const file = await Deno.open(data); + return file.readable[Symbol.asyncIterator](); + } + if (data instanceof Blob) return data.stream(); + if (isDenoFile(data)) return data.readable[Symbol.asyncIterator](); + // Handle Response objects + if (data instanceof Response) { + if (data.body === null) throw new Error(`No response body!`); + return data.body; + } + // Handle URL and URLLike objects + if (data instanceof URL) return await fetchFile(data); + if ("url" in data) return await fetchFile(data.url); + // Return buffers as-is + if (data instanceof Uint8Array) return data; + // Unwrap supplier functions + if (typeof data === "function") { + return new InputFile(await data()).toRaw(); + } + // Mark streams and iterators as consumed and return them as-is + this.consumed = true; + return data; + } + toJSON() { + throw new Error("InputFile instances must be sent via grammY"); + } +} + +async function fetchFile( + url: string | URL, +): Promise> { + const { body } = await fetch(url); + if (body === null) { + throw new Error(`Download failed, no response body from '${url}'`); + } + return body[Symbol.asyncIterator](); +} +function isDenoFile(data: unknown): data is Deno.FsFile { + return isDeno && data instanceof Deno.FsFile; +} + +// === Export InputFile types +/** Wrapper type to bundle all methods of the Telegram API */ +export type ApiMethods = ApiMethodsF; + +/** Utility type providing the argument type for the given method name or `{}` if the method does not take any parameters */ +export type Opts = OptsF[M]; + +/** This object describes a sticker to be added to a sticker set. */ +export type InputSticker = InputStickerF; + +/** This object represents the content of a media message to be sent. It should be one of + + - InputMediaAnimation + - InputMediaAudio + - InputMediaDocument + - InputMediaLivePhoto + - InputMediaPhoto + - InputMediaVideo */ +export type InputMedia = InputMediaF; +/** This object represents the content of a media message to be sent. It should be one of + + - InputMediaAnimation + - InputMediaAudio + - InputMediaDocument + - InputMediaLivePhoto + - InputMediaPhoto + - InputMediaVideo */ +export type InputMediaWithoutUpload = InputMediaF; +/** Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. */ +export type InputMediaAnimation = InputMediaAnimationF; +/** Represents an audio file to be treated as music to be sent. */ +export type InputMediaAudio = InputMediaAudioF; +/** Represents a general file to be sent. */ +export type InputMediaDocument = InputMediaDocumentF; +/** Represents a live photo to be sent. */ +export type InputMediaLivePhoto = InputMediaLivePhotoF; +/** Represents a photo to be sent. */ +export type InputMediaPhoto = InputMediaPhotoF; +/** Represents a sticker file to be sent. */ +export type InputMediaSticker = InputMediaStickerF; +/** Represents a video to be sent. */ +export type InputMediaVideo = InputMediaVideoF; +/** Represents a voice message file to be sent. */ +export type InputMediaVoiceNote = InputMediaVoiceNoteF; +/** This object contains information about one answer option in a poll to send. */ +export type InputPollOption = InputPollOptionF; +/** This object represents the content of a poll description or a quiz explanation to be sent. It should be one of + +- InputMediaAnimation +- InputMediaAudio +- InputMediaDocument +- InputMediaLivePhoto +- InputMediaLocation +- InputMediaPhoto +- InputMediaVenue +- InputMediaVideo */ +export type InputPollMedia = InputPollMediaF; +/** This object represents the content of a poll option to be sent. It should be one of + + - InputMediaAnimation + - InputMediaLivePhoto + - InputMediaLocation + - InputMediaPhoto + - InputMediaSticker + - InputMediaVenue + - InputMediaVideo */ +export type InputPollOptionMedia = InputPollOptionMediaF; +/** This object describes the paid media to be sent. Currently, it can be one of + +- InputPaidMediaPhoto +- InputPaidMediaVideo */ +export type InputPaidMedia = InputPaidMediaF; +/** The paid media to send is a live photo. */ +export type InputPaidMediaLivePhoto = InputPaidMediaLivePhotoF; +/** The paid media to send is a photo. */ +export type InputPaidMediaPhoto = InputPaidMediaPhotoF; +/** The paid media to send is a video. */ +export type InputPaidMediaVideo = InputPaidMediaVideoF; +/** This object describes a profile photo to set. Currently, it can be one of + +- InputProfilePhotoStatic +- InputProfilePhotoAnimated */ +export type InputProfilePhoto = InputProfilePhotoF; +/** A static profile photo in the .JPG format. */ +export type InputProfilePhotoStatic = InputProfilePhotoStaticF; +/** An animated profile photo in the MPEG4 format. */ +export type InputProfilePhotoAnimated = InputProfilePhotoAnimatedF; +/** This object describes the content of a story to post. Currently, it can be one of + +- InputStoryContentPhoto +- InputStoryContentVideo */ +export type InputStoryContent = InputStoryContentF; +/** Describes a photo to post as a story. */ +export type InputStoryContentPhoto = InputStoryContentPhotoF; +/** Describes a video to post as a story. */ +export type InputStoryContentVideo = InputStoryContentVideoF; + +/** This object represents one result of an inline query. Telegram clients currently support results of the following 20 types: + +- InlineQueryResultCachedAudio +- InlineQueryResultCachedDocument +- InlineQueryResultCachedGif +- InlineQueryResultCachedMpeg4Gif +- InlineQueryResultCachedPhoto +- InlineQueryResultCachedSticker +- InlineQueryResultCachedVideo +- InlineQueryResultCachedVoice +- InlineQueryResultArticle +- InlineQueryResultAudio +- InlineQueryResultContact +- InlineQueryResultGame +- InlineQueryResultDocument +- InlineQueryResultGif +- InlineQueryResultLocation +- InlineQueryResultMpeg4Gif +- InlineQueryResultPhoto +- InlineQueryResultVenue +- InlineQueryResultVideo +- InlineQueryResultVoice + +Note: All URLs passed in inline query results will be available to end users and therefore must be assumed to be public. */ +export type InlineQueryResult = InlineQueryResultF; +/** Represents a link to an MP3 audio file stored on the Telegram servers. By default, this audio file will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the audio. */ +export type InlineQueryResultCachedAudio = InlineQueryResultCachedAudioF< + InputFile +>; +/** Represents a link to a file stored on the Telegram servers. By default, this file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the file. */ +export type InlineQueryResultCachedDocument = InlineQueryResultCachedDocumentF< + InputFile +>; +/** Represents a link to an animated GIF file stored on the Telegram servers. By default, this animated GIF file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with specified content instead of the animation. */ +export type InlineQueryResultCachedGif = InlineQueryResultCachedGifF; +/** Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. By default, this animated MPEG-4 file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. */ +export type InlineQueryResultCachedMpeg4Gif = InlineQueryResultCachedMpeg4GifF< + InputFile +>; +/** Represents a link to a photo stored on the Telegram servers. By default, this photo will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the photo. */ +export type InlineQueryResultCachedPhoto = InlineQueryResultCachedPhotoF< + InputFile +>; +/** Represents a link to a sticker stored on the Telegram servers. By default, this sticker will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the sticker. */ +export type InlineQueryResultCachedSticker = InlineQueryResultCachedStickerF< + InputFile +>; +/** Represents a link to a video file stored on the Telegram servers. By default, this video file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the video. */ +export type InlineQueryResultCachedVideo = InlineQueryResultCachedVideoF< + InputFile +>; +/** Represents a link to a voice message stored on the Telegram servers. By default, this voice message will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the voice message. */ +export type InlineQueryResultCachedVoice = InlineQueryResultCachedVoiceF< + InputFile +>; +/** Represents a link to an article or web page. */ +export type InlineQueryResultArticle = InlineQueryResultArticleF; +/** Represents a link to an MP3 audio file. By default, this audio file will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the audio. */ +export type InlineQueryResultAudio = InlineQueryResultAudioF; +/** Represents a contact with a phone number. By default, this contact will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the contact. */ +export type InlineQueryResultContact = InlineQueryResultContactF; +/** Represents a Game. */ +export type InlineQueryResultGame = InlineQueryResultGameF; +/** Represents a link to a file. By default, this file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the file. Currently, only .PDF and .ZIP files can be sent using this method. */ +export type InlineQueryResultDocument = InlineQueryResultDocumentF; +/** Represents a link to an animated GIF file. By default, this animated GIF file will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. */ +export type InlineQueryResultGif = InlineQueryResultGifF; +/** Represents a location on a map. By default, the location will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the location. */ +export type InlineQueryResultLocation = InlineQueryResultLocationF; +/** Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). By default, this animated MPEG-4 file will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. */ +export type InlineQueryResultMpeg4Gif = InlineQueryResultMpeg4GifF; +/** Represents a link to a photo. By default, this photo will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the photo. */ +export type InlineQueryResultPhoto = InlineQueryResultPhotoF; +/** Represents a venue. By default, the venue will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the venue. */ +export type InlineQueryResultVenue = InlineQueryResultVenueF; +/** Represents a link to a page containing an embedded video player or a video file. By default, this video file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the video. + +> If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must replace its content using input_message_content. */ +export type InlineQueryResultVideo = InlineQueryResultVideoF; +/** Represents a link to a voice recording in an .OGG container encoded with OPUS. By default, this voice recording will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message. */ +export type InlineQueryResultVoice = InlineQueryResultVoiceF; + +/** This object represents the content of a message to be sent as a result of an inline query. Telegram clients currently support the following types: + +- InputTextMessageContent +- InputRichMessageContent +- InputLocationMessageContent +- InputVenueMessageContent +- InputContactMessageContent +- InputInvoiceMessageContent */ +export type InputMessageContent = InputMessageContentF; +/** Describes a rich message to be sent. Exactly one of the fields html, markdown, or blocks must be used. */ +export type InputRichMessage = InputRichMessageF; +/** Describes a rich message to be sent. Exactly one of the fields html, markdown, or blocks must be used. */ +export type InputRichMessageWithoutUpload = InputRichMessageF; +/** Represents the content of a rich message to be sent as the result of an inline query. */ +export type InputRichMessageContent = InputRichMessageContentF; +/** Describes a media element embedded in an outgoing rich message. */ +export type InputRichMessageMedia = InputRichMessageMediaF; +/** An item of a list to be sent. */ +export type InputRichBlockListItem = InputRichBlockListItemF; +/** This object represents a block in a rich formatted message to be sent. Currently, it can be any of the following types: + +- InputRichBlockParagraph +- InputRichBlockSectionHeading +- InputRichBlockPreformatted +- InputRichBlockFooter +- InputRichBlockDivider +- InputRichBlockMathematicalExpression +- InputRichBlockAnchor +- InputRichBlockList +- InputRichBlockBlockQuotation +- InputRichBlockPullQuotation +- InputRichBlockCollage +- InputRichBlockSlideshow +- InputRichBlockTable +- InputRichBlockDetails +- InputRichBlockMap +- InputRichBlockAnimation +- InputRichBlockAudio +- InputRichBlockPhoto +- InputRichBlockVideo +- InputRichBlockVoiceNote +- InputRichBlockThinking */ +export type InputRichBlock = InputRichBlockF; +/** A list of blocks, corresponding to the HTML tag \
    or \
      with multiple nested tags \
    1. . */ +export type InputRichBlockList = InputRichBlockListF; +/** A block quotation, corresponding to the HTML tag \
      . */ +export type InputRichBlockBlockQuotation = InputRichBlockBlockQuotationF< + InputFile +>; +/** A collage, corresponding to the custom HTML tag \. */ +export type InputRichBlockCollage = InputRichBlockCollageF; +/** A slideshow, corresponding to the custom HTML tag \. */ +export type InputRichBlockSlideshow = InputRichBlockSlideshowF; +/** An expandable block for details disclosure, corresponding to the HTML tag \
      . */ +export type InputRichBlockDetails = InputRichBlockDetailsF; +/** A block with an animation, corresponding to the HTML tag \