--- url: 'https://mtcute.dev/guide/dispatcher/children.md' --- # Child Dispatchers Child dispatchers is an elegant way to divide logic in your application. Child dispatcher is an isolated dispatcher with its own dispatcher groups, propagation and handlers that do not interfere with other dispatchers (the only exception being `StopChildrenPropagation`) ## Creating a child ```ts const child = Dispatcher.child() ``` Then you can register your handlers to `child`. ## Adding a child Dispatcher on its own does nothing, it needs to be bound to a parent to become a child dispatcher. That is done simply by calling `addChild`: ```ts dp.addChild(child) ``` Only dispatchers that are not bound to a Client can be used as a child. This also means that a dispatcher can only be a child to one dispatcher, i.e. the following code **will not work**: ```ts dp.addChild(child) otherDp.addChild(child) // error! ``` However, you can use `.clone()` method to make this work: ```ts dp.addChild(child) otherDp.addChild(child.clone()) // ok ``` ## Removing a child When building some kind of modular architecture, it is useful to also remove a child dispatcher. Luckily, it is just as easy: ```ts dp.removeChild(child) ``` Do note, however, that if you are using a cloned dispatcher, calling `removeChild` on the original dispatcher will do nothing. Instead, you have to store the reference to the cloned dispatcher: ```ts const childClone = child.clone() otherDp.addChild(childClone) // later otherDp.removeChild(childClone) ``` ## Extending Instead of using child dispatchers, you can extend the existing dispatcher with another one: ```ts dp.extend(child) ``` Note that in this case, `child` **will not** be isolated, and its handler groups, children, scenes, etc. will be merged to the original dispatcher. If `child` contains scenes with already registered names, they will be overwritten. Extending will not work if the child is using a custom storage or a custom key delegate. Using a dispatcher after it was `.extend()`-ed into another dispatcher is undefined behaviour and should be avoided. --- --- url: 'https://mtcute.dev/guide/topics/conversation.md' --- # Conversation A conversation is an object that represents some chat and all messages inside it since the start of the conversation until it is stopped. ::: warning **DO NOT** use conversations to interact with users. Conversations are designed to be used in a one-shot fashion, and will not work properly when used with users, as they don't remember any state. This may change in the future, but for now you should use [scenes](/guide/dispatcher/scenes) instead. ::: ## Usage Create a `Conversation` object and pass `TelegramClient` and the peer you want there: ```ts const conv = new Conversation(tg, 'stickers') ``` Then, use `.with()`, and inside it you can interact with the other party: ```ts await conv.with(async () => { await conv.sendText('Hello!') await conv.waitForResponse() }) ``` ::: tip `.with()` is a simple wrapper that automatically calls `.start()` and `.stop()` for you, essentially this: ```ts await conv.start() try { // ... code ... } finally { conv.stop() } ``` Calling `.stop()` is vitally important, failing to do so *will* lead to memory leaks, so use `.with()` whenever possible. ::: ## Sending messages To send messages, use `conv.sendText`, `conv.sendMedia` and `conv.sendMediaGroup` methods: ```ts await conv.sendText('Hello!') await conv.sendMedia('BQACAgEAAx...Z2mGB8E') await conv.sendMediaGroup(['BQACAgEAAx...Z2mGB8E', 'BQACAgEAAx...Z2mGB8E']) ``` **DO NOT** use client methods like `tg.sendText`, because conversation state won't properly be updated. ## Waiting for events Currently, `Conversation` supports waiting for new messages, edits and read acknowledgments: ```ts await conv.sendText('Hello!') await conv.waitForNewMessage() await conv.sendText('Hello!') await conv.waitForRead() await conv.sendText('Hello!') await conv.waitForNewMessage() await conv.sendText('Now edit that') await conv.waitForEdit() ``` ### Smarter waiting Instead of `waitForNewMessage`, you can use `waitForResponse` or `waitForReply`. `waitForResponse` will wait for a message which was sent strictly after the given message (by default, the last one) `waitForReply` will wait for a message which is a reply to the given message (by default, the last one) ### Timeouts By default, for every `waitFor*` method a timeout of 15 seconds is applied. If the event does not occur within those, `MtTimeoutError` is thrown. This is a pretty generous default when interacting with bots. However, if you are interacting with other people (which you shouldn't, use [scenes](/guide/dispatcher/scenes) instead), you may want to raise the timeout or disable it altogether. To do that, you can pass `timeout` parameter to the functions: ```ts await conv.waitForNewMessage(filters.any, 60_000) // 60 sec timeout await conv.waitForResponse(filters.any, { timeout: null }) // disable timeout ``` ### Filters You can apply dispatcher filters to the `waitFor*` methods: ```ts await conv.waitForNewMessage(filters.regex(/welcome/i)) ``` Since dispatcher filters are simple functions, you can also use custom filters: ```ts await conv.waitForResponse((msg) => msg.id > 42) ``` If a newly received message (or other update) does not match the filter, it will be ignored. --- --- url: 'https://mtcute.dev/guide/advanced/session-convert.md' --- # Converting sessions If you're coming from another library, you might already have a session lying around. mtcute provides a way to convert sessions from some other libraries to mtcute's format in `@mtcute/convert` package. ::: warning Please, **only use this to convert your own sessions**. **DO NOT** use this to convert stolen sessions or sessions you don't own. Please be a decent person. ::: ## [Telethon v1.x](https://github.com/LonamiWebs/Telethon) > Telethon v2 seems to have removed the ability to export sessions, > so it's currently not supported ```ts import { convertFromTelethonSession } from '@mtcute/convert' const client = new TelegramClient({ ... }) await client.importSession(convertFromTelethonSession("...")) ``` ## [Pyrogram](https://github.com/pyrogram/pyrogram) ```ts import { convertFromPyrogramSession } from '@mtcute/convert' const client = new TelegramClient({ ... }) await client.importSession(convertFromPyrogramSession("...")) ``` ## [GramJS](https://github.com/gram-js/gramjs) ```ts import { convertFromGramjsSession } from '@mtcute/convert' const client = new TelegramClient({ ... }) await client.importSession(convertFromGramjsSession("...")) ``` ### Store session In some version GramJS added support for storing session as a directory of files, and can be imported like so: ```ts import { readGramjsStoreSession, convertFromGramjsSession } from '@mtcute/convert' const client = new TelegramClient({ ... }) const session = await readGramjsStoreSession('/path/to/session') await client.importSession(convertFromGramjsSession(session)) ``` ## [MTKruto](https://github.com/MTKruto/MTKruto) ```ts import { convertFromMtkrutoSession } from '@mtcute/convert' const client = new TelegramClient({ ... }) await client.importSession(convertFromMtkrutoSession("...")) ``` ## [Telegram Desktop](https://github.com/telegramdesktop/tdesktop) (tdata) ```ts import { convertFromTdata } from '@mtcute/convert' const client = new TelegramClient({ ... }) await client.importSession(await convertFromTdata({ path: '/path/to/tdata', ignoreVersion: true // note: this might break // passcode: '123456' // if you have a passcode })) ``` ## Backwards If you need to convert a session from mtcute to another library, you can use the `convertTo*` functions instead: ```ts console.log(convertToTelethonSession(await client.exportSession())) ``` ## String-to-string Once converted, you can use `writeStringSession` to convert the session to a string: ```ts console.log(writeStringSession(convertFromTelethonSession("..."))) ``` ## Manual If your library is not supported, you can still convert the session manually. In the most simple case, you'll only need `auth_key` and data center information: ```ts const dc = { id: 2, ipAddress: '149.154.167.41', port: 443, } await client.importSession({ version: 3, testMode: false, primaryDcs: { main: dc, media: dc }, authKey: new Uint8Array([ /* ... */ ]), }) ``` ### Data center information If you only know DC ID and not the IP address, you can use the mappings from `@mtcute/convert` to resolve it: ```ts import { DC_MAPPING_PROD } from '@mtcute/convert' const dc = DC_MAPPING_PROD[2] ``` ### User information If you happen to know some information about the user logged in, it might help to provide it as well: ```ts await client.importSession({ ..., self: { userId: 777000, isBot: false, isPremium: false, usernames: [], } }) ``` --- --- url: 'https://mtcute.dev/guide/advanced/custom-schema.md' --- # Custom schema ::: danger While this *is* a somewhat supported feature/use-case, very limited support is provided because of the nature of the feature. When having any problems with the **implementation**, feel free to open an issue. In all other cases linked to usage of this, but not caused by the implementation itself (e.g. internal state breaking, storage corruption, missing updates, wrong types, etc.), please deal with it as you see fit. ::: In some cases it might be viable for you to use a custom schema for your bot, including but not limited to: * Using a yet unreleased layer (e.g. taken from [Telegram Android](https://github.com/TGScheme/Schema)) * Using a newer layer before the library is updated to support it * Using an older layer * Using undocumented/non-existent constructors (e.g. fuzzing) ## Basics At the base level, mtcute provides a special `mtcute.customMethod` method that basically forwards the bytes you pass to the server as-is, without any additional serialization, and return the result as-is too: ```ts const res = tg.call({ _: 'mtcute.customMethod', // `bytes` is the raw TL serialization of the method you want to call bytes: new Uint8Array([0xde, 0xad, 0xbe, 0xef]) }) // res is the raw TL serialization of the result console.log(res) ``` Additionally, there's `overrideLayer` client option that allows you to override the layer number: ```ts const tg = new TelegramClient({ ..., overrideLayer: 1337 }) ``` ## Parsing TL schema However, manually de/serializing everything would be super tedious, so you can use `@mtcute/tl-utils` to code-gen everything on demand: ```ts import { patchRuntimeTlSchema } from '@mtcute/tl-utils' import { __tlReaderMap, __tlWriterMap } from '@mtcute/core/utils.js' // here you can pass just the difference between // the built-in schema and the custom one const nextSchema = patchRuntimeTlSchema(` updateWoof from:Peer = Update; ---functions--- woof.bark at:InputPeer = Bool; `.trim(), __tlReaderMap, __tlWriterMap) ``` ::: tip `patchRuntimeTlSchema` uses `eval` under the hood, so it might not work in all environments ::: Once parsed, you can pass `nextSchema` to `TlBinaryReader` and `TlBinaryWriter` to use it: ```ts import { TlBinaryReader, TlBinaryWriter } from '@mtcute/core/utils.js' const r = await tg.call({ _: 'mtcute.customMethod', bytes: TlBinaryWriter.serializeObject(nextSchema.writerMap, { _: 'woof.bark', at: await tg.resolvePeer('teidesu') } as any) }) console.log(TlBinaryReader.deserializeObject(nextSchema.readerMap, r)) ``` ## Updates Handling new updates is a bit more involved, since there is no request-response mechanism, so you will have to hack into the inners of the library. ```ts const tg = new TelegramClient({ ..., readerMap: nextSchema.readerMap }) tg.onRawUpdate.add(({ update, peers }) => { if (update._ === 'updateWoof') { console.log('got woof from %o', peers.get(update.at)) } }) ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/di.md' --- # Dependency Injection When scaling up your bot to multiple files, you may find it useful to inject dependencies into the children dispatchers instead of having to pass them around manually. `@mtcute/dispatcher` provides a simple service locator that you can use to inject dependencies: ```ts // for typescript, you need to declare the dependencies declare module '@mtcute/dispatcher' { interface DispatcherDependencies { db: Database } } // create a root dispatcher const dp = Dispatcher.for(tg) // inject the database dp.inject('db', new Database()) // or dp.inject({ db: new Database() }) // and then add a child dispatcher import { childDispatcher } from './file2' dp.addChild(childDispatcher) // file2.ts const dp = Dispatcher.child() dp.onNewMessage(async (ctx) => { // the dependencies are available in dp.deps const db = dp.deps.db await db.saveMessage(ctx.message) }) export const childDispatcher = dp ``` ::: info You can only inject dependencies into the root dispatcher (the one created with `Dispatcher.for`), and they will be available in *all* children dispatchers. ::: --- --- url: 'https://mtcute.dev/guide/intro/faq.md' --- # FAQ > except the only person who has ever asked them was my alter ego Miscellaneous questions about the library and Telegram API as a whole that don't really belong to any other topic. If you are new to the library or Telegram API, you can skip this page for now and return later. ## What is mtcute? mtcute is a TypeScript library and framework for MTProto clients and bots. It was written from scratch, however some logic was borrowed from TDLib and similar projects like [Pyrogram](https://github.com/pyrogram/pyrogram) and [Telethon](https://github.com/LonamiWebs/Telethon) ## Can I use it with JavaScript? While you surely can, this is **not recommended**, since you would lose any benefits TypeScript gives, including type checking and smart suggestions. ## How old is mtcute? mtcute is pretty late to the party. Work on the library started in early 2021, was first published on GitHub in spring, and the first alpha release was published in the end of October 2023. ## Why mtcute? First of all, apart from mtcute, there aren't many libraries for MTProto in TypeScript (and even JS, for that matter). There's [@mtproto/core](https://github.com/alik0211/mtproto-core), which is extremely low-level and not TS-friendly; there's [GramJS](https://github.com/gram-js/gramjs), which is a port of Telethon, and pretty much nothing else (at the moment of writing). mtcute tries to provide a simple, elegant and type-safe API even for advanced use cases, while also achieving good performance, staying up-to-date with the current schema and providing near-complete documentation. ## Why are API keys needed for bots? Because Telegram requires you to. Bot API internally is just a TDLib instance running with its own API ID and hash for the connection. ## Webhooks? Webhooks are only used by Bot API because there's no persistent connection to the client to send updates. mtcute, on the other hand, has a persistent server connection, and receives updates from Telegram. This is **not** polling, because the updates are sent by Telegram, and not requested by the library. ## What are the IPs of the DCs? The primary DC (and primary Test DC) is always available at [my.telegram.org](https://my.telegram.org). List of other DCs is fetched by the client on demand. You can use this code (it doesn't even require authorization): ```ts tg.call({ _: 'help.getConfig' }) .then((res) => console.log(res.dcOptions)) ``` ## How to migrate an account? You can't. Even though Telegram docs state that the server might decide to do that, this feature was confirmed to be unimplemented yet by Levin (source unavailable). ## Why does it work slower sometimes? Because of Telegram's infrastructure. Firstly, supergroups reside in the same DC as the (original) creator, and if it is not the same as yours, it must first be passed through that DC, which incurs some delay. In the worst case, you, creator and other user are all in different DCs, and it takes some time before the client receives an update about that. The same goes for text-mentioning a user from another DC. It takes time for the server to check access hash for that user since it is stored in another DC, and thus sending a message takes more time. Another reason is that updates in supergroups are sent in order of priority ([source](https://docs.pyrogram.org/faq#why-is-my-client-reacting-slowly-in-supergroups), unverified): 1. Creator 2. Administrators 3. Bots 4. Mentioned users 5. Recently online users 6. Everyone else This is **not** affected by the library, and we can't do anything about it. This can also be reproduced in TDLib and Bot API. Your best bet on improving update latency is to: * Use `openChat` method on the client to open chats you are interested in * Periodically call `setOffline(false)` to tell Telegram that you are online * Use an account in the same DC as the peer you are interacting with ## Why do I get PEER\_ID\_INVALID/MtPeerNotFoundError? First, make sure that the ID you pass is actually correct. If it is, then probably the problem is that you haven't met this peer in the current session. As described in [Peers section](../topics/peers.html), you need access hash to interact with the user, which is only sent by the server. Think of how you find peers in normal clients - you search for usernames, open them from dialogs, messages, members lists, etc. The same goes for mtcute - you need to encounter the user before you can interact with them. Some ideas on how you can fix this: * Use [`findDialogs`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient#findDialogs) method to iterate over all dialogs and find the one you need * Use a username/phone number instead of ID * Use [`getMessages`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient#getMessages) method and fetch some message by the user (so mtcute caches the access hash) * ...and a lot more ways to "meet" a user without interacting with them ### Why do I get "Peer ... is not found in local cache"? For a similar reason. In some cases, Telegram will send an [incomplete](../topics/peers.html#incomplete-peers) peer object, which is not enough to interact with the user. mtcute tries its best to fill in the missing fields on demand (whenever you use `.resolvePeer` or any other method that uses it under the hood), but sometimes it unfortunately fails. There isn't much we can do about it :c By the way, there's an [`isPeerAvailable`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient#isPeerAvailable) method that you can use to check if a peer is available, that *never* does any network requests. Do note, however, that it is prone to false negatives, meaning that `resolvePeer` *might* still work if that method returns `false`, but should always work if it returns `true`. ## Why is my verification code expired? That's probably because you have sent it to someone. To protect users from potential scammers, Telegram checks if the outgoing message contains the verification code, and if it does, immediately revokes it. If you actually want to share it, consider somehow scrambling it, for example: `12345` → `one two three four f1ve` ## How to avoid flood errors? Write code with care, make less requests and do not abuse Telegram. Nobody knows the exact reason why flood waits occur, and that is intentional. Publicly available information often contradicts with other, so there is no definitive answer. You might find helpful information in [this article](https://telegra.ph/So-your-bot-is-rate-limited-01-26). This might also be an issue with the library. Currently mtcute doesn't do a lot of caching, though it is on the roadmap. Particularly, issues may arise if your bot is high-load. If you receive transport error -429, however, please [let us know](https://t.me/mt_cute), so we can investigate further. ## How to not get banned? Do not abuse Telegram. If you use the API for spamming, flooding, faking counters or similar, you *will* be banned. Accounts created using unofficial API clients are automatically put under observation to prevent violation of the ToS ([source](https://core.telegram.org/api/obtaining_api_id#using-the-api-id)). In some cases, even logging in with an unofficial client to an account created using an official one may trigger the system If you are planning to implement an active userbot, be extra careful, avoid using VOIP numbers and try to minimize server load generated (for example, implement local caching and rate limiting). ## I was banned, help! The library only does the things you told it to do. If you abuse Telegram, then the ban is justified. If you still have access to the phone number used when registering the account, you can try contacting the Telegram support and ask them to recover your account: . If you don't, then welp. Create a new account and be *even more* careful. ## How to know when I'm banned When an account is banned, any network request will fail with `USER_DEACTIVATED_BAN`, even simple ones like `getMe`. You can also use a [network middleware](../advanced/net-middlewares.md#errors-in-middlewares) to set up a client-wide listener for this error: ```ts const tg = new TelegramClient({ ..., network: { middlewares: [ networkMiddlewares.onRpcError((ctx, error) => { if (error.errorMessage === 'USER_DEACTIVATED_BAN') { console.log('account was banned :c') tg.close() } }), ...networkMiddlewares.basic() ] } }) ``` ## How to fix `better-sqlite3` issues? A very common issue with mtcute users under Node.js is the native bindings that come with the `better-sqlite3` library mtcute uses internally. Unfortunately, until `node:sqlite` is stable, we're stuck with this library. ### Make sure `postinstall` scripts are allowed Starting with pnpm 10, `postinstall` scripts are no longer allowed by default. If you are using `pnpm`, check that your `pnpm-workspace.yaml` file contains: ```yaml onlyBuiltDependencies: - better-sqlite3 ``` If it doesn't, add it and run `pnpm rebuild better-sqlite3`. ### Try running `pnpm rebuild better-sqlite3` If you are getting an error similar to this: ```sh Error: The module '.../better_sqlite3.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 127. This version of Node.js requires NODE_MODULE_VERSION 115. Please try re-compiling or re-installing the module (for instance, using `npm rebuild` or `npm install`). at Module._extensions..node (node:internal/modules/cjs/loader:1454:18) at ... { code: 'ERR_DLOPEN_FAILED' } ``` Chances are, you are using a newer Node.js version than the one `better-sqlite3` bindings are compiled for. Either use the same version as before, or run `pnpm rebuild better-sqlite3` to rebuild them. You can view a mapping of `NODE_MODULE_VERSION` values to actual Node.js versions [here](https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json) ### Try using LTS Node.js If you are having issues building the library, you can try using LTS Node.js version (20, 22, 24 as of writing). better-sqlite3 comes with pre-built binaries for LTS Node.js versions. This will download the necessary binaries automatically in its postinstall script, and you won't need to do anything. ### Make sure all build-time dependencies are installed If you aren't using an LTS Node.js, you'll have to build it yourself. For better-sqlite3 to build correctly, you need `python3`, `make` and `gcc` to be installed. > yes, python, because `better-sqlite3` uses `node-gyp`, and `node-gyp` uses python 😭 Depending on your OS/distro, you may need to run one of the following: ```sh # Windows winget install Microsoft.VisualStudio.2022.BuildTools Python.Python.3.11 # macOS xcode-select --install # install xcode command-line tools brew install python make # nix nix-shell -p python3 gnumake gcc # Ubuntu/Debian sudo apt install python3 make gcc # Arch Linux sudo pacman -S python make gcc # Fedora sudo dnf install python3 make gcc # CentOS/RHEL sudo yum install python3 make gcc # openSUSE sudo zypper install python3 make gcc # Alpine Linux sudo apk add python3 make gcc ``` ## How to view logs? mtcute does quite a bit of debug logging, which is however not visible by default. To view these logs, you need to pass `logLevel: level` to the client constructor: ```ts new TelegramClient({ ..., logLevel: 5 }) ``` Additionally, you can use the `MTCUTE_LOG_LEVEL=5` environment variable (node/deno/bun): ``` MTCUTE_LOG_LEVEL=5 node my-script.js ``` Or `localStorage.MTCUTE_LOG_LEVEL = '5'` in browser ### Log levels * `0`: Disable logging * `1`: Error: something went very wrong * `2`: Warning: something went wrong, but the library was able to recover * `3`: Info: basic information about what's happening * `4`: Debug: detailed information about how the library handles MTProto packets * `5`: Verbose: additionally prints all RPC requests and responses The default log level is 2 (Warning). When troubleshooting, it's recommended to set the log level to 4 or 5. ::: warning Note that since every single request is logged on level 5, it will probably contain private information. Take care when sharing these logs! ::: ### Custom log handler You can also provide a custom log handler: ```ts const tg = new TelegramClient({ ... }) tg.log.mgr.handler = (color, level, tag, fmt, args) => { // color: color for the tag, based on the tag // level: log level // tag: tag for the message (i.e. the scope of the library that produced the message) // fmt, args: format string and its arguments, compatible with `console.log` (and `util.format`) } ``` --- --- url: 'https://mtcute.dev/guide/topics/files.md' --- # Files Working with files and media is important in almost any bot, and mtcute makes it very simple. ## Downloading files To download a file, just use `downloadIterable`, `downloadStream`, `downloadBuffer`or `downloadToFile` method on the object that represents a file, for example: ```ts tg.onNewMessage.add(async (msg) => { if (msg.media?.type === 'photo') { await tg.downloadToFile('download.jpg', msg.media) } }) ``` Or in case you don't have the object, you can download a file by its [File ID](#file-ids): ```ts await tg.downloadToFile('download.jpg', 'AgACAgEAAxkBA...33gAgABHwQ') ``` ::: tip `downloadToFile` is only available for Node.js. Other methods are environment agnostic. ::: ## Uploading files In Telegram, files are primarily used to back a media (a photo, a chat avatar, a document, etc.) Client methods (like `setProfilePhoto`) accept `InputFileLike`, which is a type containing information on how to upload a certain file, or to re-use an existing file (see [File IDs](#file-ids)). `InputFileLike` can be one of: * `Buffer`, in this case contents of the buffer will be uploaded as file * Readable stream (both.js and Web are supported) * `File` from the Web API * `Response` from `window.fetch` or `node-fetch` * `UploadedFile`, see [Uploading files manually](#uploading-files-manually) * `string` with URL: `https://example.com/file.jpg` (only supported sometimes) * `string` with file path (only Node.js): `file:path/to/file.jpg` (note the `file:` prefix) * `string` with [File ID](#file-ids) ```ts await tg.setProfilePhoto('photo', Buffer.from(...)) await tg.setProfilePhoto('photo', fs.createReadableStream('assets/renge.jpg')) await tg.setProfilePhoto('photo', await fetch('https://nyanpa.su/renge.jpg')) await tg.setProfilePhoto('photo', await tg.uploadFile(...)) await tg.setProfilePhoto('photo', 'file:assets/renge.jpg') await tg.setProfilePhoto('photo', 'BQACAgEAAx...Z2mGB8E') // setProfilePhoto and some other methods don't support URLs (TG limitation) // await tg.setProfilePhoto('photo', 'https://nyanpa.su/renge.jpg') ``` ## Sending media As mentioned earlier, most of the time file is used as a media in a Message. Sending media is incredibly easy with mtcute - you simply call `sendMedia` and provide `InputMediaLike`. `InputMediaLike` can be constructed manually, or using one of the builder functions exported in [`InputMedia` namespace](https://ref.mtcute.dev/modules/_mtcute_core.index.InputMedia): ```ts await tg.sendMedia('me', InputMedia.photo('file:assets/welcome.jpg')) await tg.sendMedia('me', InputMedia.auto('BQACAgEAAx...Z2mGB8E', { caption: 'Backup of the project' })) // when using InputMedia.auto with file IDs and not using // additonal parameters like `caption`, you can simply pass it as a string await tg.sendMedia('me', 'CAADAgADLgZZCKNgg2JpAg') ``` First argument is `InputFileLike`, so you can use [any supported type](#files) when using it. Using `InputMedia` instead of separate methods (like in Bot API) allows easily switching to `sendMediaGroup`: ```ts await tg.sendMediaGroup('me', [ InputMedia.photo('file:assets/welcome.jpg'), InputMedia.auto('AgACAgEAAxkBA...6D2AgABHwQ'), 'AgACAgEAAxkBA...33gAgABHwQ', ]) ``` ::: tip Even though you *technically can* pass media groups of different types, do not mix them up (you can mix photos and videos), since that would result in a server-sent error. ::: ## File IDs ::: tip File IDs are implemented in `@mtcute/file-id` package, which can be easily used outside of mtcute. ::: If you ever worked with the Bot API, you probably already know what File ID is. If you don't, it is a unique string identifier that is used to represent files already stored on Telegram servers. It comes in very handy when dealing with files programmatically, since storing a simple string is much easier than storing a lot of different TL objects, parsing them, normalizing, converting, etc. mtcute File IDs are a port of TDLib's File IDs (which are also used in Bot API), which means they are **100% interoperable** with TDLib and Bot API. They do have some limitations though: * You can't get a File ID until you upload a file and use it somewhere, e.g. as a message media (or you can use [uploadMedia](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient#uploadMedia)). * When sending by File ID, you can't change type of the file (i.e. if this is a video, you can't send it as a document) or any meta information like duration. * File ID cannot be used for thumbnails. * When sending a photo by File ID of one of its sizes, all sizes will be re-used. * File ID is unique per-user and can't be used on another account (however, some developers seem to bypass this with userbots). * For user accounts, File ID expire (after ~24-48 hours). * The same file may have different *valid* File IDs. ::: details Why is this? All of the above points can be explained fairly easily, but to understand them you'll need to have some understanding of how files in MTProto work. * File ID contains document/photo ID and file reference, which are not available until that file is uploaded and used somehow, or `messages.uploadMedia` method is used. * Photos and documents are completely different types in MTProto. As for documents, when using `inputDocument`, Telegram [does not allow](https://corefork.telegram.org/constructor/inputDocument) setting new document attributes, and re-uses the attributes from the original document. They contain file type (video/audio/voice/etc), file name, duration (for video/audio), and so on. * For thumbnails, Telegram [requires](https://corefork.telegram.org/constructor/inputMediaUploadedDocument) clients to use `InputFile`, which can only contain newly uploaded files. * When sending photos, File ID is transformed to [inputPhoto](https://corefork.telegram.org/constructor/inputPhoto), which does not contain information about sizes. When downloading, however, it is transformed to [inputPhotoFileLocation](https://corefork.telegram.org/constructor/inputPhotoFileLocation), which does contain `thumbSize` parameter * File IDs contain what is known as a File reference, which is unique per-user. It seems that bots can sometimes use any file reference for files that they have recently encountered, but this is pretty unreliable and should not be used. * File reference [may expire](https://core.telegram.org/api/file_reference), making the File ID unusable. Telegram claims that this does not happen to bots though ([source](https://core.telegram.org/bots/faq#can-i-count-on-file-ids-to-be-persistent)) * File reference or File ID format might change over time. ::: File ID is available in `.fileId` field: ```ts tg.onNewMessage.add(async (msg) => { if (msg.media?.type === 'photo') { console.log(msg.media.fileId) } }) ``` ### Unique File ID This is also a concept ported from TDLib. It is similar to File ID, but built in such a way that it uniquely defines some file, i.e. the same file always has the same Unique File ID, and different files have different Unique File IDs. It can't be used to download a file, but it is the same for different users/bots. Unique File ID is available in `.uniqueFileId` field: ```ts tg.onNewMessage.add(async (msg) => { if (msg.media?.type === 'photo') { console.log(msg.media.uniqueFileId) } }) ``` ## Uploading files manually ::: tip This method is rarely needed outside mtcute, simply because most of the methods already handle uploading automatically, and for Raw API you can use `normalizeInputFile` and `normalizeInputMedia` ::: To upload files, Client provides a simple method `uploadFile`. It has a bunch of options, but the only required one is `file`. ### Uploading a local file To upload a local file from Node.js, you can either provide file path, or a readable stream (note that here you don't need `file:` prefix): ```ts await tg.uploadFile({ file: 'assets/renge.jpg' }) // or await tg.uploadFile({ file: fs.createReadStream('assets/renge.jpg') }) ``` ### Uploading from `Buffer` To upload a `Buffer` as a file, simply pass it as `file`: ```ts const data = Buffer.from(...) await tg.uploadFile({ file: data }) ``` ### Uploading from stream To upload from a stream, pass it as `file`, and provide file size whenever possible: ```ts await tg.uploadFile({ file: stream, fileSize: streamExpectedLength }) ``` ### Uploading from the Internet To upload a file from the Internet, you can use `window.fetch` and simply pass the response object: ```ts await tg.uploadFile({ file: await fetch('https://nyanpa.su/renge.jpg') }) ``` If you are using some other library for HTTP(S), it probably also supports returning streams, but you'll have to extract meta from the response object manually. Rough example for [axios](https://npmjs.com/package/axios): ```ts async function uploadFileAxios(tg: TelegramClient, config: AxiosRequestConfig) { const response = await axios({ ...config, responseType: 'stream' }) return tg.uploadFile({ file: response.data, fileSize: parseInt(response.headers['content-length'] || 0), fileMime: response.headers['content-type'], // fileName: ... }) } const file = await uploadFileAxios(tg, { url: 'https://nyanpa.su/renge.jpg' }) await tg.sendMedia('me', InputMedia.photo(file)) ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/filters.md' --- # Filters Filters is a powerful concept that allows handlers to only process some of the updates and not every single one. mtcute comes with a lot of filters for different cases, and you can write your own as well. ## Common filters For the full reference, see `filters` namespace in the [API reference](https://ref.mtcute.dev/modules/_mtcute_dispatcher.filters) For many updates, `filters.userId` is supported which filters by user ID(s) that issued the update (i.e. message sender, poll voter, etc.): ```ts dp.onNewMessage( filters.userId(12345678), async (msg) => { // ... } ) ``` There's also `filters.chatId` that checks for the chat ID instead: ```ts dp.onNewMessage( filters.chatId(-100123456789), async (msg) => { // ... } ) ``` `filters.chat` allows you to filter by chat type(s): ```ts dp.onNewMessage( filters.chat('private'), async (msg) => { // ... } ) ``` For every media type, there's a filter that will only match that media type: `filters.photo`, `filters.video`, etc: ```ts dp.onNewMessage( filters.photo, async (msg) => { await msg.replyText('Great photo though') } ) ``` For service messages, there's `filters.action` that allows filtering by service message action type(s): ```ts dp.onNewMessage( filters.action('chat_created'), async (msg) => { await msg.answerText(`${msg.user.mention()} created ${msg.action.title}`) } ) ``` There are also `filters.command` and `filters.regex` that add information about the match into the update object: ```ts dp.onNewMessage( filters.command('start'), async (msg) => { if (msg.command[1] === 'from_inline') { await msg.answerText('Thanks for using inline mode!') } } ) dp.onNewMessage( filters.regex(/^I'?m (\S+)/i), async (msg) => { await msg.replyText(`Hi ${msg.match[1]}, I'm Dad!`) } ) ``` For ChatMember updates, you can use `filters.chatMember` to filter by change type: ```ts dp.onChatMemberUpdate( filters.chatMember('joined'), async (upd: ChatMemberUpdate) => { await upd.chat.sendText(`${upd.user.mention()}, welcome to the chat!`) } ) ``` You can also use `filters.chatMemberSelf` to filter for actions that were issued by the current user: ```ts dp.onChatMemberUpdate( filters.and( filters.chatMemberSelf, filters.chatMember('joined'), ), async (upd: ChatMemberUpdate) => { await addChatToDatabase(upd.chat) } ) ``` ## Type modification Some filters like `filters.photo` only match in case `msg.media` is a `Photo`, and it makes sense to make the handler aware of that and avoid redundant checks in your code. This is true for most of the built-in filters: ```ts dp.onNewMessage( async (msg) => { // msg.media is Photo | Video | ... | null } ) dp.onNewMessage( filters.media, async (msg) => { // msg.media is Photo | Video | ... } ) dp.onNewMessage( filters.photo, async (msg) => { // msg.media is Photo } ) ``` ## Combining filters Filters on their own are already pretty powerful, but you can also combine and negate them. This also modifies the [type modification](#type-modification) accordingly. ### Negating To negate a filter, use `filters.not`: ```ts dp.onNewMessage( filters.photo, async (msg) => { // msg.media is Photo } ) dp.onNewMessage( filters.not(filters.photo), async (msg) => { // msg.media is Exclude } ) ``` ### Logical addition Logical addition (i.e. OR operator) is supported with `filters.or` ```ts dp.onNewMessage( filters.or(filters.video, filters.photo), async (msg) => { // msg.media is Photo | Video } ) ``` ### Logical multiplication Logical multiplication (i.e. AND operator) is supported with `filters.and`. ```ts dp.onNewMessage( filters.and(filters.chat('private'), filters.photo), async (msg) => { // msg.media is Photo } ) ``` ## Custom filters Sometimes pre-existing filters are just not enough. Then, you can write a custom filter. ### Simple custom filter Under the hood, filters are simply functions, so you can do the following: ```ts dp.onNewMessage( (msg) => msg.sender.isVerified, async (msg) => { // ... } ) ``` It can even be asynchronous: ```ts dp.onNewMessage( async (msg) => await shouldProcessMessage(msg.id), async (msg) => { // ... } ) ``` ### Parameters To accept parameters in your custom filter, simply create a function that returns a filter: ```ts const usernameRegex = (regex: RegExp): UpdateFilter => (msg) => { const m = msg.sender.username?.match(regex) return !!m } dp.onNewMessage( usernameRegex(/some_regex/), async (msg) => { // ... } ) ``` ### Adding type modification You can also add type modification to your custom filter: ```ts const fromChat: UpdateFilter = (msg) => msg.sender.type === 'chat' dp.onNewMessage( fromChat, async (msg) => { // msg.sender is Chat } ) ``` ::: warning This is used as-is, it is not checked if the filter actually checks for the given modification due to TypeScript limitations. ::: ### Additional fields You can add additional fields to the update using type modifications. This may be useful in multiple cases, for example, to add the result of the parametrized filter: ```ts const usernameRegex = (regex: RegExp): UpdateFilter< Message, { usernameMatch: RegExpMatchArray } > => (msg) => { const m = msg.sender.username?.match(regex) if (m) { ;(obj as any).usernameMatch = m return true } return false } dp.onNewMessage( usernameRegex(/some_regex/), async (msg) => { // msg.usernameMatch is RegExpMatchArray } ) ``` Or, you can add some additional fields similar to a middleware in other frameworks: ```ts const loadSenderFromDb: UpdateFilter = async (msg) => { ;(msg as any).senderDb = await db.loadUser(msg.sender.id) return true } dp.onNewMessage( filters.and(filters.chat('private'), loadSenderFromDb), async (msg) => { // msg.senderDb is UserModel } ) ``` ::: warning While this is possible, this is **strongly not recommended**. Instead, do this right inside the handler code: ```ts dp.onNewMessage( filters.chat('private'), async (msg) => { const senderDb = await db.loadUser(msg.sender.id) } ) ``` ::: --- --- url: 'https://mtcute.dev/guide/intro/updates.md' --- # Getting updates ## What is an Update? Updates are events that happen in Telegram and sent to the clients. This includes events about a new message, joined chat member, inline keyboard callbacks, etc. In a bot environment, a [Dispatcher](/guide/dispatcher/intro.html) is normally used to handle the updates. ### Don't need them? Sometimes, you might not really need any updates. Then, just disable them in `TelegramClient` parameters: ```ts const tg = new TelegramClient({ // ... disableUpdates: true }) ``` ## Setting up The parameters themselves will be explained a bit below, for now let's just focus on how they are passed. `TelegramClient` has updates handling turned on by default, and you can configure it using `updates` parameter ```ts const tg = new TelegramClient({ // ... updates: { messageGroupingInterval: 250, catchUp: true } }) ``` The updates themselves are dispatched on the client as events (see [reference](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient)): ```ts tg.onNewMessage.add((msg) => { console.log(msg.text) }) // You can also handle any supported update at once: tg.onUpdate.add((upd) => { if (upd.name === 'new_message') { console.log(upd.data.text) } }) // As well as raw MTProto updates: tg.onRawUpdate.add(({ update, peers }) => { console.log(update._) }) ``` ::: tip The handlers should be synchronous, so if you want to do something async, make sure to also handle the errors: ```ts tg.onNewMessage.add(async (msg) => { try { await msg.answerText('test') } catch (e) { console.error(e) } }) ``` ::: ### Missed updates When your client is offline, updates are still stored by Telegram, and can be fetched later (client "catches up" with the updates). When back online, mtcute may "catch up", fetch any missed updates and process them. To do that, pass `catchUp: true` parameter as shown above: ### Message grouping As you may already know, albums handling in Telegram is not very trivial, as they are sent by the server as separate messages. To make it easier to handle them, you may opt into grouping them automatically. To do that, pass `messageGroupingInterval` as shown above. It is a number of milliseconds to wait for the next message in the album. If the next message is not received in that time, the album is considered complete and dispatched as a single `message_group` object. The recommended value is `250` ms. ::: warning This **will** introduce delays of up to `messageGroupingInterval` ms for every message with media groups, and may sometimes break ordering. Use with caution. ::: ## Opening chats For Telegram to properly send updates for channels (e.g. for channels that you are not a member of, and for more consistent updates for channels that you are a member of), you need to open them first. This is done by calling `openChat` method: ```ts await tg.openChat('durov') ``` Once you're done, you can close the chat by calling `closeChat`: ```ts await tg.closeChat('durov') ``` ::: danger Opening a chat with `openChat` method will make the library make additional requests every so often. Which means that you should **avoid opening more than 5-10 chats at once**, as it will probably trigger server-side limits and you might start getting transport errors or even get banned. If missing *some* updates from *some* channels (or having them arrive a bit late) is acceptable for you, you might want to consider not opening them at all. You will still receive updates for channels you are a member of, but they might be delayed. ::: ## Dispatcher Dispatcher is a class that dispatches client events to registered handlers, while also filtering and propagating them as needed. You can think of it as some sort of framework that allows you to do everything more declaratively and handles most of the boilerplate related to state. Dispatcher is provided by `@mtcute/dispatcher` package. ### Registering Dispatcher is quite a powerful thing, and we will explore it in-depth in a separate section. For now, let's just register a dispatcher and add a simple handler: ```ts const tg = new TelegramClient(...) const dp = Dispatcher.for(tg) dp.onNewMessage(async (msg) => { await msg.forwardTo({ toChatId: 'me' }) }) await tg.start() ``` Pretty simple, right? We have registered a "new message" handler and made it forward any new messages to "Saved Messages". ### Filters Example above is pretty useless in real world, though. Most of the time you will want to *filter* the events, and only react to some of them. Let's make our code only handle messages containing media: ```ts dp.onNewMessage( filters.media, async (msg) => { await msg.forwardTo({ toChatId: 'me' }) } ) ``` Filters can do a lot more than that, and we will cover them further [later](../dispatcher/filters.html). --- --- url: 'https://mtcute.dev/guide/dispatcher/groups-propagation.md' --- # Groups and propagation When you register multiple handlers with conflicting filters, only the one registered the first will be executed, to avoid handling the same update twice. This is sometimes undesirable, and to handle these you can use either [handler groups](#groups), or [propagation symbols](#propagation) ::: tip You can also use a [child Dispatcher](children.html). In this page, however, it is assumed that all handlers are registered to one dispatcher. ::: ## Groups Your first option to handle the same update multiple times is by using a separate handler group. Handler groups are identified with a single number (group `0` is the default group) and are processed within the same dispatcher one-by-one in order (`..., -2, -1, 0, 1, 2, ...`). For example, consider the following code ```ts dp.onNewMessage( filters.or(filters.text, filters.sticker), async (msg) => { console.log('Text or sticker') } ) dp.onNewMessage( filters.text, async (msg) => { console.log('Text only') } ) ``` In this code, the second handler will never be executed, because the first one handles `text` messages as well. To make the Dispatcher execute the second handler, you can register it to a different group: ```ts dp.onNewMessage( filters.text, async (msg) => { console.log('Text only') }, 1 ) ``` In this case, this handler will be executed *after* the first one. Alternatively, you can pass a negative number to make the second handler execute *before* the first one: ```ts dp.onNewMessage( filters.text, async (msg) => { console.log('Text only') }, -1 ) ``` Group can also be set using `addUpdateHandler`: ```ts dp.addUpdateHandler({ ... }, 1) ``` ## Propagation To customize the behaviour even further, you can use propagation symbols that `@mtcute/dispatcher` exports in `PropagationAction` enum. ### Stop propagation To prevent the update from being handled by any other handlers within the same dispatcher, you can use `PropagationAction.Stop`: ```ts dp.onNewMessage( filters.or(filters.text, filters.sticker), async (msg) => { console.log('Text or sticker') return PropagationAction.Stop } ) dp.onNewMessage( filters.text, async (msg) => { console.log('Text only') }, 1 ) ``` In the above code, second handler *will not* be executed even though it is in a separate group. This will not, however, prevent the handlers from a [child Dispatcher](children.html) to be executed. ### Stop children propagation A bit ahead of ourselves, since we haven't covered child Dispatchers yet, but the idea is pretty simple. `PropagationAction.StopChildren` is very similar to the previous one, but also prevents the handlers from child dispatchers to be executed: ```ts dp.onNewMessage( filters.or(filters.text, filters.sticker), async (msg) => { console.log('Text or sticker') return PropagationAction.StopChildren } ) const dp1 = Dispatcher.child() dp.addChild(dp1) dp1.onNewMessage( filters.text, async (msg) => { console.log('Text only') } ) ``` In the above code, second executor will not be called, even though it is in a child dispatcher. ### Continue propagation As an alternative to [groups](#groups), you can use `PropagationAction.Continue` symbol. It makes the dispatcher continue propagating this update within the same group even though some handler from that group was already executed: ```ts dp.onNewMessage( filters.or(filters.text, filters.sticker), async (msg) => { console.log('Text or sticker') return PropagationAction.Continue } ) dp.onNewMessage( filters.text, async (msg) => { console.log('Text only') } ) ``` In the above code, the second dispatcher will be called for text messages even though the first one also matches them. Note that `Continue` only works within the same handler group. ## Raw updates As mentioned earlier, raw updates are handled independently of parsed updates, and they have their own groups and propagation chain. This means that in the following example both handlers will be called, despite returning `Stop` action: ```ts dp.onNewMessage(() => { return PropagationAction.Stop }) dp.onRawUpdate( (cl, upd) => upd._ === 'updateNewMessage', () => { ... } ) ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/handlers.md' --- # Handlers Dispatcher can process a lot of different update types, and for each of them you can register as many handlers as you want. For each of the handler types, there are 2 ways you can add a handler - either using a specialized method, or with `addUpdateHandler` method, as [described below](#addupdatehandler) See also: [Reference](https://ref.mtcute.dev/classes/_mtcute_dispatcher.Dispatcher) ::: warning Do not add or remove handlers inside of another handler, this may lead to undefined behaviour. ::: ## New message Whenever a new message is received by the bot, or a message is sent by another client (mostly used for users), `new_message` handlers are dispatched: ```ts dp.onNewMessage(async (upd) => { await upd.answerText('Hey!') }) ``` ## Edit message Whenever a message is edited (and client receives an update about that\*), `edit_message` handlers are dispatched: ```ts dp.onEditMessage(async (upd) => { await upd.replyText('Yes.') }) ``` \* Telegram might decide not to send these updates in case this message is old enough. ## Message group When message grouping is enabled (see [here](/guide/intro/updates.md#message-grouping)), `message_group` handlers are dispatched when a media group (aka album) is received: ```ts dp.onMessageGroup(async (upd) => { await upd.replyText('Thanks for the media!') }) ``` ## Delete message Whenever a message is deleted (and client receives an update about that\*), `delete_message` handlers are dispatched. Note that these updates only contain message ID(s), and not its contents, and it is up to you to check what that ID corresponds to. ```ts dp.onDeleteMessage(async (upd) => { if (upd.messageIds.includes(42)) { console.log('Magic message deleted :c') } }) ``` Note that for private chats, this does not include user's ID, so you may want to implement some caching if you need that info. \* Telegram might decide not to send these updates in case this message is old enough. ## Chat member Whenever chat member status is changed in a channel/supergroup where the bot is an administrator, `chat_member` handlers are dispatched. ```ts dp.onChatMemberUpdate(async (upd) => { console.log(`${upd.user.displayName} ${upd.type} by ${upd.actor.displayName}`) }) ``` ::: tip You can filter by update type using `filters.chatMember`: ```ts dp.onChatMemberUpdate( filters.chatMember('joined'), async (upd) => { await upd.client.sendText(upd.chat, `${upd.user.mention()}, welcome to the chat!`) } ) ``` ::: ## Inline query Whenever an inline query is sent by user to your bot, `inline_query` handlers are dispatched: ```ts dp.onInlineQuery(async (upd) => { await upd.answer([], { switchPm: { text: 'Hello!', parameter: 'inline_hello' } }) }) ``` You can learn more about inline queries in [Inline Mode](./inline-mode.html) section. ## Chosen inline result When a user selects an inline result, and assuming that you have **inline feedback** feature enabled, `chosen_inline_result` handlers are dispatched: ```ts dp.onChosenInlineResult(async (upd) => { await upd.editMessage({ text: `${result.user.displayName}, thanks for using inline!` }) }) ``` As mentioned, these updates are only sent by Telegram when you have enabled **inline feedback** feature. You can enable it in [@BotFather](https://t.me/botfather). It is however noted by Telegram that this should only be used for statistical purposes, and even if probability setting is 100%, not all chosen inline results may be reported ([source](https://core.telegram.org/api/bots/inline#inline-feedback)). ## Callback query Whenever user clicks on a [callback button](../topics/keyboards.html#inline-keyboards), `callback_query` or `inline_callback_query` handlers are dispatched, based on the origin of the message: ```ts dp.onCallbackQuery(async (upd) => { await upd.answer({ text: '🌸' }) }) dp.onInlineCallbackQuery(async (upd) => { await upd.answer({ text: '🌸' }) }) ``` For messages sent normally by the bot (e.g. using `sendText`), `callback_query` handlers are dispatched. For messages sent from an inline query (e.g. inside `onInlineQuery`), `inline_callback_query` handlers are dispatched. ## Poll update Whenever a poll state is updated (stopped, anonymous user has voted, etc.), `poll` handlers are dispatched: ```ts dp.onPollUpdate(async (upd) => { // do something }) ``` Bots only receive updates about polls that they have sent. Note that due to Telegram limitation, sometimes the update does not include the poll itself, and mtcute creates a "stub" poll, that is missing most of the information (including question text, answers text, missing flags like `quiz`, etc.). Number of votes per-answer is always there, though. If you need that missing information, you will need to implement caching by yourself. Bot API (strictly speaking, TDLib) does it internally and thus is able to provide all the needed information autonomously. This is not implemented in mtcute yet. ## Poll vote When a user votes in a public poll, `poll_vote` handlers are dispatched: ```ts dp.onPollVote(async (upd) => { upd.user.sendText('Thanks for voting!') }) ``` Bots only receive updates about polls that they have sent. This update currently doesn't contain information about the poll, only the poll ID, so if you need that info, you'll have to implement caching yourself. ## User status When a user's online status changes (e.g. user goes offline), and client receives an update about that\*, `user_status` handlers are dispatched: ```ts dp.onUserStatusUpdate(async (upd) => { console.log(`User ${upd.userId} is now ${upd.status}`) }) ``` \* Telegram might decide not to send these updates in many cases, for example: you don't have an active PM with this user, this user is from a large chat that you aren't currently chatting in, etc. ## User typing When a user's typing status changes, and client receives an update about that\*, `user_status` handlers are dispatched: ```ts dp.onUserTyping(async (upd) => { console.log(`${upd.userId} is ${upd.status} in ${upd.chatId}`) }) ``` \* Telegram might decide not to send these updates in many cases, for example: you haven't talked to this user for some time, that user/chat is archived, etc. ## History read When history is read in a chat (either by the other party or by you from another client), and client receives an update about that, `history_read` handlers are dispatched: ```ts dp.onHistoryRead(async (upd) => { console.log(`History read in ${upd.chatId} up to ${upd.maxReadId}`) }) ``` ## Bot stopped When a user clicks "Stop bot" button in the bot's profile, `bot_stopped` handlers are dispatched: ```ts dp.onBotStopped(async (upd) => { console.log(`Bot stopped by ${upd.user.id}`) }) ``` ## Join requests These updates differ depending on whether the currently logged in user is a bot or not. ### Bot When a user requests to join a group/channel where the current bot is an admin, `bot_chat_join_request` handlers are dispatched: ```ts dp.onBotChatJoinRequest(async (upd) => { console.log(`User ${upd.user.id} wants to join ${upd.chat.id}`) if (upd.user.id === DUROV) { await upd.decline() } }) ``` ### User When a user requests to join a group/channel where the current user is an admin, `chat_join_request` handlers are dispatched: ```ts dp.onChatJoinRequest(async (upd) => { console.log(`User ${upd.recentRequesters[0].id} wants to join ${upd.chatId}`) }) ``` These updates contain less information than bot join requests, and additional info should be fetched manually if needed ## Pre-checkout query When a user clicks "Pay" button, `pre_checkout_query` handlers are dispatched: ```ts dp.onPreCheckoutQuery(async (upd) => { await upd.approve() }) ``` ## Story updates When a story is posted or modified, `story` handlers are dispatched: ```ts dp.onStoryUpdate(async (upd) => { console.log(`${upd.peer.id} posted or modified a story!`) }) ``` ## Delete story updates When a story is deleted, `delete_story` handlers are dispatched: ```ts dp.onDeleteStory(async (upd) => { console.log(`${upd.peer.id} deleted story ${upd.storyId}!`) }) ``` ## Raw updates Dispatcher only implements the most commonly used updates, but you can still add a handler for a custom MTProto update: ```ts dp.onRawUpdate( (cl, upd) => upd._ === 'updateUserName', async ( client: TelegramClient, update_: tl.TypeUpdate, peers: PeersIndex, ) => { const update = update_ as tl.RawUpdateUserName // ... } ) ``` Note that the signature is slightly different: since the update is not parsed by the library, client, raw update and entities are provided as-is. Raw update handlers are dispatched independently of parsed update handlers ([learn more](groups-propagation.html#raw-updates)). ## `addUpdateHandler` Another way to register a handler is to use `addUpdateHandler` method. It supports all the updates that have a specialized methods, and this method is actually used by `on*` under the hood. It accepts an object, containing update type, its handler and optionally a `check` function, which checks if the update should be handled by this handler (basically a [Filter](filters.html)): ```ts dp.addUpdateHandler({ name: 'new_message', callback: async (msg) => { ... }, check: filters.media }) ``` This may be useful in case you are loading your handlers dynamically. ## Removing handlers It might be useful to remove a previously added handler. ### Removing a single handler To remove a single handler, it must have been added using `addUpdateHandler`, and then that object should be passed to `removeUpdateHandler`: ```ts const handler = { ... } dp.addUpdateHandler(handler) // later dp.removeUpdateHandler(handler) ``` Handlers are matched by the object that wraps them, because the same function may be used for multiple different handlers. ### Removing handlers by type To remove all handlers that have the given type, pass this type to `removeUpdateHandler`: ```ts dp.removeUpdateHandler('new_message') ``` ### Removing all handlers Pass `all` to `removeUpdateHandler`: ```ts dp.removeUpdateHandler('all') ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/errors.md' --- # Handling errors We've already briefly touched handling errors in dispatchers in the Getting Started section, but let's dive a bit deeper. ## Registering a handler An error handler for a Dispatcher is simply a function that is called whenever an error is thrown inside one of the handlers. For convenience, that function has access to the error itself, the parsed update and the [state](./state) (if applicable): ```ts dp.onNewMessage( filters.command('do_stuff'), async (msg) => { throw new Error('Some error') } ) dp.onError(async (error, update, state) => { if (update.name === 'new_message') { await update.data.replyText(`Error: ${error.message}`) return true } return false }) ``` Error handler function is expected to return `boolean` indicating whether the update was handled. If it was not, it will be propagated to Client. `update` is an object that contains 2 fields: `name` and `data`. `name` is simply update name (`new_message`, `edit_message`, etc.; see [Handlers](handlers.html)), and `data` is the respective object. ## What errors are not handled Errors inside [Raw update handlers](handlers.html#raw-updates) are not handled by the error handler. Any other errors within the same dispatcher (both handlers and filters) are handled by it. ## Propagation ### To Client If the error handler is not registered, throws an error or returns `false`, the error is propagated to Client's [error handler](../intro/errors.html#client-errors). Obviously, in Client's error handler you won't have access to the update that caused this error: ```ts dp.onNewMessage( filters.command('do_stuff'), async (msg) => { throw new Error('Some error') } ) tg.onError.add((err) => { // will be called since there's no `dp.onError` console.log(err) }) ``` ### Within the Dispatcher When an error is thrown by one of the handlers, propagation within this dispatcher stops (the same way as if it returned `StopPropagation`): ```ts dp.onNewMessage( filters.command('do_stuff'), async (msg) => { throw new Error('Some error') } ) dp.onNewMessage( async (msg) => { // will not reach } ) ``` ### To parent/children Errors are **not** propagated to parent dispatcher or to any of the children dispatchers: ```ts const dp = Dispatcher.for(tg) const dp1 = Dispatcher.child() const dp2 = Dispatcher.child() dp.addChild(dp1) dp1.addChild(dp2) // dp --child--> dp1 --child--> dp2 dp.onError(() => console.log('DP caught error')) dp1.onError(() => console.log('DP1 caught error')) dp2.onError(() => console.log('DP2 caught error')) dp1.onNewMessage(() => { throw new Error() }) // Only "DP1 caught error" will ever be printed ``` However, if you need that behaviour, you can use `propagateErrorToParent`: ```ts const dp = Dispatcher.for(tg) const dp1 = Dispatcher.child() const dp2 = Dispatcher.child() dp.addChild(dp1) dp1.addChild(dp2) // dp --child--> dp1 --child--> dp2 dp.onError(() => console.log('DP caught error')) dp1.onError(() => { console.log('DP1 caught error') return dp1.propagateErrorToParent(...arguments) }) dp2.onError(() => console.log('DP2 caught error')) dp1.onNewMessage(() => { throw new Error() }) // "DP1 caught error" and "DP caught error" will be printed for each new message ``` --- --- url: 'https://mtcute.dev/guide/intro/errors.md' --- # Handling errors > There are two ways to write error-free programs; only the third one works > > © Alan J. Perlis Errors are an inevitable part of any software development, especially when working with external APIs, and it is important to know how to handle them. ## RPC Errors Almost any RPC call can result in an RPC error (like `FLOOD_WAIT_%d`, `CHAT_ID_INVALID`, etc.). All these RPC errors are instances of `tl.RpcError`. Sadly, JavaScript does not provide a nice syntax to handle different kinds of errors, so you will need to write a bit of boilerplate: ```ts try { // your code // } catch (e) { if (tl.RpcError.is(e, 'FLOOD_WAIT_%d')) { // handle... } else throw e } ``` ::: tip mtcute automatically handles flood waits smaller than `floodSleepThreshold` by sleeping for that amount of seconds. ::: ### Errors with parameters Some errors (like `FLOOD_WAIT_%d`) also contain a parameter. This parameter is available as error's field (in this case in `.seconds` field) after checking for error type using `.is()`: ```ts try { // your code // } catch (e) { if (tl.RpcError.is(e, 'FLOOD_WAIT_%d')) { await new Promise((res) => setTimeout(res, e.seconds)) } else throw e } ``` ## mtcute errors mtcute has a group of its own errors that are used to indicate that the provided input is invalid, or that the server returned something weird. All these errors are subclassed from `MtcuteError`: | Name | Description | Package | |---|---|---| | `MtArgumentError` | Some argument passed to the method appears to be incorrect in some way | core | `MtSecurityError` | Something isn't right with security of the connection | core | `MtUnsupportedError` | Server returned something that mtcute does not support (yet). Should not normally happen, and if it does, feel free to [open an issue](https://github.com/mtcute/mtcute/issues/new). | core | `MtTypeAssertionError`| Server returned some type, but mtcute expected it to be another type. Usually means a bug on mtcute side, so feel free to [open an issue](https://github.com/mtcute/mtcute/issues/new). | `MtTimeoutError` | Timeout for the request has been reached | core | `MtPeerNotFoundError` | Only thrown by `resolvePeer`. Means that mtcute wasn't able to find a peer for a given `InputPeerLike`. | client | `MtMessageNotFoundError` | mtcute hasn't been able to find a message by the given parameters | client | `MtInvalidPeerTypeError` | mtcute expected another type of peer (e.g. you provided a user, but a channel was expected). | client | `MtEmptyError` | You tried to access some property that is not available on the object | client ## Client errors Even though these days internet is much more stable than before, stuff like "Error: Connection reset" still happens. Also, there might be some client-level error that happened internally (e.g. error while processing updates). You can handle these errors using `TelegramClient#onError`: ```ts const tg = new TelegramClient(...) tg.onError.add((err) => { console.log(err) }) ``` ::: tip mtcute handles reconnection and stuff automatically, so you don't need to call `.connect()` again! This should primarily be used for logging and debugging, as well as some edge cases where you might need access to low-level connection state ::: ## Dispatcher errors [Learn more in Dispatcher section](../dispatcher/errors.html). Unhandled errors that had happened inside dispatcher's handlers can be handled as well: ```ts const dp = Dispatcher.child() dp.onError((error, update, state) => { console.log(error) // to indicate that the error was handled return true }) ``` Dispatcher errors are **local**, meaning that they only trigger error handler within the current dispatcher, and do not propagate to parent/children. They also stop propagation within this dispatcher. If there is no dispatcher error handler, but an error still occurs, the error is propagated to `TelegramClient`'s `onError` emitter. --- --- url: 'https://mtcute.dev/guide/dispatcher/inline-mode.md' --- # Inline mode Users can interact with bots using inline queries, by starting a message with bot's username and then typing their query. You can learn more about them in [Bot API docs](https://core.telegram.org/bots/inline) ## Handling inline queries To handle inline queries to your bot, simply use `onInlineQuery`: ```ts dp.onInlineQuery(async (query) => { // ... }) ``` You can also use `filters.regex` to filter based on the query text: ```ts dp.onInlineQuery( filters.regex(/^cats /), async (query) => { // ... } ) ``` ## Answering to inline queries Answering an inline query means sending results of the query to Telegram. The results may contain any media, including articles, photos, videos, stickers, etc, and can also be heterogeneous, which means that you can use different media types in results for the same query. To answer a query, simply use `.answer()` and pass an array that contains [results](#results): ```ts dp.onInlineQuery(async (query) => { query.answer([...]) }) ``` ::: tip Clients wait about 10 seconds for results, after which they assume the bot timed out. Make sure not to do heavy computations there! ::: ## Results Every inline result must contain a **unique** result ID, which can be later used in [chosen inline result updates](#chosen-inline-results) updates to determine which one was chosen. Media inside inline results *must* be provided via either HTTP URL, or re-used from Telegram (e.g. using [File IDs](/guide/topics/files.md#file-ids)). Telegram does not allow uploading new files directly to inline results. When choosing a result, by default, a message containing the respective result is sent, but the message contents can be [customized](#custom-message) using `message` field. Telegram supports a bunch of result types, and mtcute supports sending all of them as an inline result. In the below examples we'll be using direct URLs to content, but you can also use File IDs instead. ### Article An article is a result that contains a title, description and *optionally* a thumbnail and a URL: ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.article( 'RESULT_ID', { title: 'Article title', description: 'Article description', thumb: 'https://example.com/image.jpg', url: 'https://example.com/some-article.html' } ) ]) }) ``` When choosing this result, by default, a message with the following text is sent (Handlebars syntax is used here): ```handlebars {{#if url}} {{title}} {{else}} {{title}} {{/if}} {{#if description}} {{description}} {{/if}} ``` For the above example, this would result in the following message: ### GIF You can send an animated GIF (either real GIF, or an MP4 without sound) as a result. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.gif( 'RESULT_ID', 'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4' ) ]) }) ``` You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android) ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.gif( 'RESULT_ID', 'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4', { title: 'GIF title', description: 'GIF description', } ) ]) }) ``` ### Video You can send an MP4 video as an inline result. When sending by direct URL, there's a file size limit of 20 MB, and you *must* provide a thumbnail, otherwise the result will be ignored. Thumbnail is only used until the video file is cached by Telegram, and is then overridden by Telegram-generated video thumbnail. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.video( 'RESULT_ID', 'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4', { thumb: 'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg', title: 'Video title', description: 'Video description', } ) ]) }) ``` Alternatively, you can send a video by its URL (e.g. from YouTube) using `isEmbed: true`: ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.video( 'RESULT_ID', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', { isEmbed: true, thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg', title: 'Video title', description: 'Video description', } ) ]) }) ``` Choosing such result would send a message containing URL to that video: ### Audio You can send an MPEG audio file as an inline result. When sending by direct URL, there's a file size limit of 20 MB. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.audio( 'RESULT_ID', 'https://vk.com/mp3/cc_ice_melts.mp3', { performer: 'Griby', title: 'Tayet Lyod', } ) ]) }) ``` ::: tip NOTE Performer, title and other meta can't be changed once the file is cached by Telegram (they will still be displayed in the results, but not in the message). To avoid caching when sending by URL, add a random query parameter (e.g. `?notgcache`), which will make Telegram think this is a new file. ::: ### Voice You can send an OGG file as a voice note inline result. Waveform seems to only be generated for OGG files encoded with OPUS, so if it is not generated, try re-encoding your file with OPUS. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.voice( 'RESULT_ID', 'https://tei.su/test_voice.ogg', { title: 'Voice title', } ) ]) }) ``` ### Photo You can send an image as an inline result. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg' ) ]) }) ``` You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android) ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { title: 'Photo title', description: 'Photo description', } ) ]) }) ``` ### Sticker You can send a sticker as an inline result. You can't send a sticker by URL ([Telegram limitation](https://t.me/tdlibchat/17923)), only by File ID. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.sticker( 'RESULT_ID', 'CAACAgIAAxk...JtzysqiUK3IAQ' ) ]) }) ``` ### File You can send a file as an inline result. Due to Telegram limitations, when using URLs, you can only send PDF and ZIP files, and must set `mime` accordingly (`application/pdf` and `application/zip` MIMEs respectively). With File IDs, you can send any file. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.file( 'RESULT_ID', 'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf', { mime: 'application/pdf', title: 'File title', description: 'File description' } ) ]) }) ``` ### Geolocation You can send a geolocation as an inline result. By default, Telegram generates `thumb` for result based on the location provided. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.geo( 'RESULT_ID', { latitude: 55.751999, longitude: 37.617734, title: 'Kremlin' } ), ]) }) ``` ### Venue You can send a venue as an inline result. By default, Telegram generates `thumb` for result based on the location provided. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.venue( 'RESULT_ID', { latitude: 55.751999, longitude: 37.617734, title: 'Kremlin', address: 'Red Square' } ), ]) }) ``` ### Contact You can send a contact as an inline result. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.contact( 'RESULT_ID', { firstName: 'Alice', phone: '+79001234567', thumb: 'https://avatars.githubusercontent.com/u/86301490' } ), ]) }) ``` ### Games Finally, you can send a game as an inline result. ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.game('RESULT_ID', 'game_short_name'), ]) }) ``` ## Custom message By default, mtcute generates a message based on result contents. However, you can override the message that will be sent using `message` ### Text Instead of media or default article message, you may want to send a custom text message: ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { message: BotInlineMessage.text( 'Ha-ha, just kidding. No Renge for you :p' ) } ) ]) }) ``` ### Media You can customize media message (for photos, videos, voices, documents, etc.) with custom caption, keyboard, etc: ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { message: BotInlineMessage.media({ text: 'Nyanpasu!' }), } ) ]) }) ``` Sadly, Telegram does not allow sending another media instead of the one you provided in the result (however, you *could* use web previews to accomplish similar results) ### Geolocation Instead of sending the default message, you can send geolocation, or even live geolocation: ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { // or BotInlineMessage.geoLive message: BotInlineMessage.geo({ latitude: 55.751999, longitude: 37.617734, }), } ) ]) }) ``` ### Venue Instead of sending the default message, you can send a venue ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { message: BotInlineMessage.venue({ latitude: 55.751999, longitude: 37.617734, title: 'Kremlin', address: 'Red Square' }), } ) ]) }) ``` ### Contact Instead of sending the default message, you can send a contact ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.photo( 'RESULT_ID', 'https://nyanpa.su/renge.jpg', { message: BotInlineMessage.contact({ firstName: 'Alice', phone: '+79001234567', }), } ) ]) }) ``` ### Game For inline results containing a game, you can customize keyboard under the message ([learn more](../topics/keyboards.html)): ```ts dp.onInlineQuery(async (query) => { query.answer([ BotInline.game('RESULT_ID', 'test', { message: BotInlineMessage.game({ replyMarkup: ... }) }), ]) }) ``` ## Switch to PM Some bots may benefit from switching to PM with the bot for some action (e.g. logging in with your account). For that, you can use `switchPm` button along with your results: ```ts dp.onInlineQuery(async (query) => { query.answer([], { switchPm: { text: 'Log in', parameter: 'login_inline' } }) }) dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => { await msg.answerText('Thanks for logging in!') }) ``` ## Chosen inline results When a user selects an inline result, and assuming that you have **inline feedback** feature enabled, an update is received, which can be handled: ```ts dp.onChosenInlineResult(async (result) => { await result.editMessage({ text: `${result.user.displayName}, thanks for using inline!` }) }) ``` You can use `filters.regex` to filter by chosen result ID: ```ts dp.onChosenInlineResult( filters.regex(/^CATS_/), async (result) => { // ... } ) ``` These updates are only sent by Telegram when you have enabled **inline feedback** feature. You can enable it in [@BotFather](https://t.me/botfather). It is however noted by Telegram that this should only be used for statistical purposes, and even if probability setting is 100%, not all chosen inline results may be reported ([source](https://core.telegram.org/api/bots/inline#inline-feedback)). ## Editing inline message You can edit an inline message in Chosen inline query update handlers, and in Callback query updates: ::: tip NOTE In the below examples, it is assumed that callback query originates from an inline message. ::: ```ts dp.onChosenInlineResult(async (result) => { await result.editMessage({...}) }) dp.onCallbackQuery(async (query) => { await query.editMessage({...}) }) ``` You can also save inline message ID and edit the message later: ```ts function updateMessageLater(msgId: string) { setTimeout(() => { tg.editInlineMessage(msgId, {...}) .catch(console.error) }, 5000) } dp.onChosenInlineResult(async (result) => { updateMessageLater(result.messageIdStr) }) dp.onCallbackQuery(async (query) => { updateMessageLater(query.inlineMessageIdStr) }) ``` ::: tip `messageIdStr` and `inlineMessageIdStr` contain string representation of the inline message ID, to simplify its storage. You can also use the `messageId` and `inlineMessageId` respectively, which contain a TL object, in case you need to use Raw API. In mtcute, they are interchangeable. ::: You can also edit media inside inline messages, and even upload new media directly to them (unlike Bot API): ```ts dp.onChosenInlineResult(async (result) => { const link = await getDownloadLink(result.id) await result.editMessage({ media: InputMedia.audio(await fetch(link)) }) }) ``` --- --- url: 'https://mtcute.dev/guide/topics/inline-mode.md' --- # Inline mode Users can interact with bots using inline queries, by starting a message with bot's username and then typing their query. ## Implementing inline mode First, you'll need to enable inline mode in [@BotFather](https://t.me/botfather), either in `/mybots` or with `/setinline` Then, you can use Dispatcher to [implement inline mode](../dispatcher/inline-mode.html) for your bot. Instead of Dispatcher, you can also use client events (however you will miss features that Dispatcher provides): ```ts tg.onInlineQuery.add(async (query) => { await query.answer([]) }) ``` ## Using inline mode As a user, you can use inline mode just like with a normal client. It is currently not implemented as a Client method, but you can use [Raw API](raw-api.html): ```ts const chat = await tg.resolvePeer('me') const results = await tg.call({ _: 'messages.getInlineBotResults', bot: toInputUser(await tg.resolvePeer('music'))!, peer: chat, query: 'vivaldi', offset: '' }, { throw503: true }) ``` Then, for example, to send the first result: ```ts const first = results.results[0] const res = await tg.call({ _: 'messages.sendInlineBotResult', peer: chat, randomId: randomLong(), queryId: results.queryId, id: first.id }) tg.handleClientUpdate(res, true) ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/intro.md' --- # Introduction We've already briefly [touched](../intro/updates.html) on what Dispatcher is, but as a quick reminder: Dispatcher is a class that processes updates from the client and *dispatches* them to the registered handlers. It is implemented in `@mtcute/dispatcher` package ## Setting up To use a dispatcher, you need to first create a bound dispatcher using `Dispatcher.for` method: ```ts import { Dispatcher } from '@mtcute/dispatcher' const tg = new TelegramClient({...}) const dp = Dispatcher.for(tg) ``` --- --- url: 'https://mtcute.dev/guide/topics/keyboards.md' --- # Keyboards You probably already know what a keyboard is, and if you don't, check the [Bots documentation](https://core.telegram.org/bots#keyboards) by Telegram. ## Sending a keyboard When developing bots, a common feature that many developers use is sending custom keyboards to their users - be it inline or reply. In both cases, this is done by providing `replyMarkup` parameter when using `sendText` or similar methods. It accepts plain JavaScript object, but you can also use builder functions from `BotKeyboard` namespace. In mtcute, buttons are represented as a two-dimensional array. ## Reply keyboards Reply keyboard is a keyboard that is shown under user's writebar. When user taps on some button, a message is sent containing this button's text. ```ts await tg.sendText('username', 'Awesome keyboard!', { replyMarkup: BotKeyboard.reply([ [BotKeyboard.text('First button')], [BotKeyboard.text('Second button')], ]) }) ``` You can only use the following button types with reply keyboards: | Name | Type | Notes | | ------------------- | ---------------------------- | ----------------------- | | Text-only | `BotKeyboard.text` | | | Request contact | `BotKeyboard.requestContact` | only for private chats. | | Request geolocation | `BotKeyboard.requestGeo` | only for private chats. | | Request poll | `BotKeyboard.requestPoll` | only for private chats. | Using any other will result in an error by Telegram. You can also instruct the client to hide a previously sent reply keyboard: ```ts await tg.sendText('username', 'No more keyboard :p', { replyMarkup: BotKeyboard.hideReply() }) ``` Or, ask the user to reply to this message with custom text: ```ts await tg.sendText('username', 'What is your name?', { replyMarkup: BotKeyboard.forceReply() }) ``` ## Inline keyboards Inline keyboard is a keyboard that is shown under the message. When user taps on some button, a client does some action that particular button instructs it to do. ```ts await tg.sendText('username', 'Awesome keyboard!', { replyMarkup: BotKeyboard.inline([ [BotKeyboard.callback('First button', 'btn:1')], [BotKeyboard.callback('Second button', 'btn:2')], ]) }) ``` You can only use the following button types with inline keyboards: | Name | Type | Notes | | -------------- | -------------------------- | ----------------------------------------------------------------------------------- | | Callback | `BotKeyboard.callback` | When clicked a callback query will be sent to the bot. | | URL | `BotKeyboard.url` | When clicked the client will open the given URL. | | Switch inline | `BotKeyboard.switchInline` | When clicked the client will open an inline query to this bot with the given query. | | "Play game" | `BotKeyboard.game` | Must be the first one, must be used with `InputMedia.game` as the media. | | "Pay" | `BotKeyboard.pay` | Must be the first one, must be used with `InputMedia.invoice` as the media. | | Seamless login | `BotKeyboard.urlAuth` | [Learn more](https://corefork.telegram.org/constructor/inputKeyboardButtonUrlAuth) | | WebView | `BotKeyboard.webView` | [Learn more](https://corefork.telegram.org/api/bots/webapps) | | Open user | `BotKeyboard.userProfile` | When clicked the client will open the given user's profile | | Request peer | `BotKeyboard.requestPeer` | When clicked the client will ask the user to choose a peer and will send a message with [`ActionPeerChosen`](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ActionPeerChosen) | Using any other will result in an error by Telegram. ## Keyboard builder Sometimes 2D array is a bit too low-level, and thus mtcute provides an easy-to-use builder for the keyboards. Once created using `BotKeyboard.builder()`, you can `push` buttons there, and then get it either `asInline` or `asReply`: ```ts const markup = BotKeyboard.builder() .push(BotKeyboard.text('Button 1')) .push(BotKeyboard.text('Button 2')) .asReply() // Result: // [ Button 1 ] // [ Button 2 ] ``` You can also push a button conditionally, or even use a function: ```ts const markup = BotKeyboard.builder() .push(BotKeyboard.text('Button 1')) .push(isAdmin && BotKeyboard.text('Button 2')) .push(() => BotKeyboard.text('Button 3')) .asReply() // Result: // [ Button 1 ] // [ Button 2 ] (only if admin) // [ Button 3 ] ``` When `push`-ing multiple buttons at once, they will be wrapped after a certain number of buttons added (default: 3): ```ts const markup = BotKeyboard.builder() .push( BotKeyboard.text('Button 1'), BotKeyboard.text('Button 2'), BotKeyboard.text('Button 3'), BotKeyboard.text('Button 4'), ) .asReply() // Result: // [ Button 1 ] [ Button 2 ] [ Button 3 ] // [ Button 4 ] ``` Or, you can add entire rows at once without them getting wrapped (and even populate them from a function!): ```ts const markup = BotKeyboard.builder() .row( BotKeyboard.text('1'), BotKeyboard.text('2'), BotKeyboard.text('3'), BotKeyboard.text('4'), ) .row((row) => { for (let i = 5; i <= 8; i++ ) { row.push(BotKeyboard.text(`${i}`)) } }) .asReply() // Result: // [ 1 ] [ 2 ] [ 3 ] [ 4 ] // [ 5 ] [ 6 ] [ 7 ] [ 8 ] ``` ## Callback data builders Writing, parsing and checking callback data manually gets tiring quite fast. Luckily, mtcute provides a tool that does the heavy stuff for you, called Callback data builder. ### Creating a builder Consider a simple bot that has some posts to display to user, and the user can switch between them using inline buttons. First, let's declare a builder for the button: ```ts const PostButton = new CallbackDataBuilder('post', 'id', 'action') ``` Here, `post` is the *prefix*, which will be prepended to all callback data strings generated by this builder to disambiguate. Make sure to use something unique! `id` and `action` are *fields* which will be parsed/serialized to the callback data string in that particular order. Only include important stuff there, since callback data is limited to 64 characters! ::: tip Callback data builders are meant to be static, so it is best to declare them in a separate file and import from other files. ::: ### Creating buttons Now that we have the builder, we can use `.build` method to add buttons to the messages: ```ts await msg.answerText('...', { replyMarkup: BotKeyboard.inline([ [ BotKeyboard.callback( 'Post title', PostButton.build({ id: 1, action: 'view' }) ) ] ]) }) ``` The above code will produce the following callback data in that button: ``` post:1:view ``` ### Handling clicks Our button is currently rather useless, since we haven't registered a handler for it just yet. We can use `.filter` method of our builder to create a filter to suit our needs: ```ts dp.onCallbackQuery(PostButton.filter({ action: 'view' }), async (upd) => { const post = await getPostById(upd.match.id) if (!post) { await upd.answer({ text: 'Not found!' }) return } await upd.editMessage({ text: post.text }) }) ``` `.filter` not only handles parsing and checking, but also provides `.match` extension field that contains the parsed data, and you can use it inside your code. ## Using a keyboard When using mtcute as a client, you may want to use some keyboard that was attached to some message. ```ts dp.onNewMessage(async (msg: Message) => { const markup = msg.markup switch (markup.type) { // see below } }) ``` If type is `hide_reply`, there is (obviously) nothing to do except to hide the current reply keyboard from the UI (if applicable). If type is `force_reply`, just send a message in reply to this message: ```ts await msg.replyText('Some text') ``` If type is `reply` or `inline`, then there are some buttons available. You can find the one you need, and then act accordingly: ```ts const buttons = markup.buttons const buttonINeed = BotKeyboard.findButton(buttons, 'Button text') switch (buttonINeed._) { // see below } ``` `buttonINeed` will be a plain TL object of type [KeyboardButton](https://corefork.telegram.org/type/KeyboardButton). ### Emulating a click See [Telegram docs](https://core.telegram.org/api/bots/buttons#pressing-buttons) on this topic. --- --- url: 'https://mtcute.dev/guide/intro/llm-assistance.md' --- # LLM Assistance There are two (not mutually exclusive) generally recommended ways to teach your agent about mtcute: ## Context7 MCP Complete mtcute docs, generated from this website as well as the API reference, are available over at [Context7](https://context7.com), which you can easily integrate with your own agents via MCP: ```bash pnpx ctx7 setup ``` Or manually by following the [instructions](https://context7.com/docs/resources/all-clients). ## `using-mtcute` skill mtcute comes with an **experimental** `using-mtcute` skill that explains the basics and also bundles a few handy scripts for the agent to easily search across the actual API surface (both high-level and raw API methods). It is available in the `.claude/skills/using-mtcute` directory, and can be installed using [skills.sh](https://skills.sh) CLI: ```bash pnpx skills add https://github.com/mtcute/mtcute --skill using-mtcute ``` ## llms.txt Additionally, this entire website is available as plain markdown, by simply adding `.md` to the end of the URL, as well as the `llms.txt` index available at [mtcute.dev/llms.txt](https://mtcute.dev/llms.txt). Although in my personal experience, agents aren't very eager to fetch the docs manually that way, and often prefer to just hallucinate instead. You should probably prefer the above two methods instead. --- --- url: 'https://mtcute.dev/guide/dispatcher/middlewares.md' --- # Middlewares Dispatcher provides basic middleware functionality. It is not as extensible as middlewares in frameworks likes [Telegraf](https://github.com/telegraf/telegraf), and that is by design, since we already have filters and propagation using which you can achieve pretty much the same. Middlewares are only called for parsed updates. ## Middleware types Dispatcher supports 2 middlewares: pre-update and post-update. ### Pre-update **Pre-update** middleware is called right before an update is going to be dispatched, and can be used to skip that update altogether. ```ts dp.onPreUpdate((upd) => { // randomly skip 10% of updates if (Math.random() < 0.1) return PropagationAction.Stop }) ``` ### Post-update **Post-update** middleware is called after an update was processed by the dispatcher. Whether the update was handled is also provided here: ```ts dp.onPostUpdate((handled, upd) => { if (handled) { console.log(`handled ${upd.name}`) } }) ``` ## Additional context You can add some additional context in the pre-update middleware, and that context will also be available in post-update middleware and error handler: ```ts interface TimerContext { start: number } dp.onPreUpdate((upd) => { upd.start = performance.now() }) dp.onPostUpdate((handled, upd) => { if (handled) { console.log(`handled ${upd.name} in ${performance.now() - upd.start} ms`) } }) dp.onError((err, upd) => { console.log(`error for ${upd.name} after ${performance.now() - upd.start} ms`) }) ``` Note that *currently* you can't access that context from handlers or filters (see [mtcute#4](https://github.com/mtcute/mtcute/issues/4)). --- --- url: 'https://mtcute.dev/README.md' --- # mtcute docs Documentation website for [mtcute](https://github.com/mtcute/mtcute), built with [VitePress](https://vitepress.dev/). ## Development ```bash pnpm install --frozen-lockfile pnpm run dev ``` --- --- url: 'https://mtcute.dev/guide/intro/mtproto-vs-bot-api.md' --- # MTProto vs. Bot API Unlike many existing libraries and frameworks for Telegram in TypeScript/JavaScript, mtcute uses MTProto, and not Bot API. This allows mtcute to be much more flexible and support many features that Bot API does not. ## What is Bot API? [**Telegram Bot API**](https://core.telegram.org/bots/api), or simply **Bot API**, is an HTTP(s) interface provided and hosted by Telegram that allows developers to build bots. Under the hood, Bot API uses TDLib (which in turn uses MTProto API), and simply provides HTTP methods that closely correlate with TDLib methods. ## What is MTProto API? [MTProto](https://core.telegram.org/mtproto) is the custom protocol invented by Nikolai Durov and his team, consisting of six ACM champions, half of them Ph.Ds in math. It took them about two years to roll out the current version of MTProto ([source](https://news.ycombinator.com/item?id=6916860)). It is used to communicate between clients and Telegram servers. On the surface, MTProto API is basically an RPC, where MTProto and TL are used behind the scenes to serialize, encrypt and process the requests. \~~*Sounds like VK API with extra steps, right?*~~ ## Why MTProto? MTProto clients (like mtcute) connect directly to Telegram, removing the need to use additional transport layers like HTTP, polling or webhooks. This means **less overhead**, since the data is sent directly to you, and not passed through the Bot API server and then sent to you via HTTP: Apart from smaller overhead, using MTProto has many other advantages, including: | | Bot API | MTProto | |---|---|---| | Userbots | Only bots | Both bots and users. | Files | 20 MB download, 50 MB upload. | No limits (except global limit of 2000 MB) | Objects | Often brief and non-exhaustive | Exposes anything you can think of | Updates | Only a limited subset | Updates about virtually anything that had happened | Errors | Often non-informative (e.g. slow mode is the same as flood wait) and not machine-readable | Informative and simple to use | Version | Receives updates slower | Updates with the TL layer | Compatibility | Updates randomly, you have to prepare for the upcoming breaking changes.Attempts to stay backwards-compatible, leading to weird hacks | You can stay on older version for as long as you need > **Note**: the above table assumes official Bot API instance, hosted at `api.telegram.org`. > Self-hosted and/or custom Bot API instances bypass some of these limitations. ## Why not MTProto? Everything has its drawbacks though. Using mtcute instead of Bot API (or TDLib) for high-load bots might currently not be the best idea, since TDLib caches basically everything, while mtcute doesn't. This is our primary focus for the upcoming releases, though. If your bot is high-load, and you receive errors like 500 and 429, this definitely means a problem on mtcute side. Please [let us know](https://t.me/mt_cute), so we can investigate further. Another drawback is that due to mtcute being an enthusiast project, it does not offer the same level of robustness and support as Bot API, and is missing some features that Bot API has. --- --- url: 'https://mtcute.dev/guide/advanced/net-middlewares.md' --- # Network middlewares In some cases it may make sense to intercept *all* outgoing requests and control the request flow. ## Default middlewares By default, mtcute uses two middlewares: flood-waiter and internal errors handler. The combined default middleware is exported in `networkMiddlewares.basic`, and can be configured as follows: ```ts const tg = new TelegramClient({ ..., network: { middlewares: networkMiddlewares.basic({ floodWaiter: { maxWait: 5000, maxRetries: 5 }, internalErrors: { maxRetries: 5 } }) } }) ``` Flood-waiter and internal errors handler middlewares themselves are exported under `networkMiddlewares.floodWaiter` and `networkMiddlewares.internalErrorsHandler` respectively. ## Writing middlewares Middleware is simply an async function that takes `ctx` and `next` as arguments. The `ctx` object contains information about the RPC call, including the request itself and any additional parameters that were passed along, and `next` function can be used to call the next middleware in the chain, returning the call result (or an [error](#errors-in-middlewares)): ```ts const myMiddleware: RpcCallMiddleware = async (ctx, next) => { if (ctx.request._ === 'help.getConfig') { return myConfig } return next(ctx) } ``` ::: info If you are familiar with grammY/telegraf or koa middlewares, you might find the `ctx, next` syntax familiar. Indeed, these middlewares were heavily inspired by them. However, they work slightly different here, as the task is slightly different too. Unlike grammY-style middlewares, `next` *can* be called multiple times, and the last pseudo-"middleware" in the chain will actually execute the request contained in the `ctx` (instead of being a no-op). And because of that, `ctx` is always passed explicitly, allowing to execute multiple different requests from a single middleware. ::: ### Errors in middlewares To improve performance, RPC errors in middlewares are monadic, meaning that an RPC error is considered a valid result. To check if the call resulted in an error, you can use `isTlRpcError` handler: ```ts const myMiddleware: RpcCallMiddleware = async (ctx, next) => { const res = await next(ctx) if (isTlRpcError(res) && res.errorMessage === 'PEER_ID_INVALID') { logPeerIdInvalid(ctx.request) } return res } ``` You can also use `networkMiddlewares.onRpcError` helper to create a middleware that only handles RPC errors: ```ts const client = new TelegramClient({ ..., network: { middlewares: [ networkMiddlewares.onRpcError(async (ctx, error) => { if (error.errorMessage === 'PEER_ID_INVALID') { logPeerIdInvalid(ctx.request) } }), ...networkMiddlewares.basic() ] } }) ``` ### Modifying request In some cases, it might make sense to modify the request before sending. One way to do so is to overwrite the `ctx` fields: ```ts const myMiddleware: RpcCallMiddleware = async (ctx, next) => { if (ctx.request._ === 'users.getFullUser') { ctx.request.id = { _: 'inputUserSelf' } } return next(ctx) } ``` Alternatively, you can construct your own context: ```ts const myMiddleware: RpcCallMiddleware = async (ctx, next) => { if (ctx.request._ === 'users.getFullUser') { return next({ manager: ctx.manager, params: ctx.params, request: { _: 'users.getFullUser', id: { _: 'inputUserSelf' } } }) } return next(ctx) } ``` ### Applying middlewares Once you're done writing your middleware, you need to connect it to the client. That's done by passing an array to the `middlewares` option, like this: ```ts const tg = new TelegramClient({ ..., network: { middlewares: [ myMiddleware, myOtherMiddleware, // You'll probably also want to include all the default // middlewares, as passing this option overrides them. ...networkMiddlewares.basic() ] } }) ``` ::: info **Middleware order matters**, which is why we include the basic middlewares last — we want `myMiddleware` and `myOtherMiddleware` to also benefit from them (i.e. have flood waits and internal errors handled) ::: --- --- url: 'https://mtcute.dev/guide/advanced/serialization.md' --- # Object serialization In some cases it might be necessary to manually serialize the objects you receive from the server to store them somewhere and be able to recreate the original object later. There are a few possible solutions for this, and it depends on your use case. ## Temporary storage If you need the serialization temporarily (e.g. to pass around between processes), and you are sure that the receiving side will use the same mtcute version, you can simply do: ```ts import { serializeObject, serializePeersIndex, deserializeObject, deserializePeersIndex, } from '@mtcute/core/utils.js' // we'll use a Message for this example, // the process is similar for other objects declare const obj: Message // e.g. from dp.onNewMessage // Message object consists of tl.TypeMessage and a PeersIndex. // not all objects use a PeersIndex, // but those that do will need one to be serialized separately. const serializedObj = serializeObject(obj.raw) const serializedPeers = serializePeersIndex(obj._peers) // on the receiving side const message = deserializeObject(serializedObj) assert(tl.isAnyMessage(message)) const peers = deserializePeersIndex(serializedPeers) const message = new Message(message, peers) ``` ::: tip The same `PeersIndex` might be reused across multiple objects, so you might want to manually store the peers separately instead of storing everything for each object ::: ## Persistent storage If you need to store the serialization for a longer period of time, e.g. in a database for future access, you can use `deserializeObjectWithCompat` instead. It works pretty much the same on the surface, but will also deserialize objects that were serialized with older versions of the library: ```ts import { serializeObject, serializePeersIndex, deserializeObjectWithCompat, deserializePeersIndexWithCompat, } from '@mtcute/core/utils.js' declare const obj: Message const serializedObj = serializeObject(obj.raw) const serializedPeers = serializePeersIndex(obj._peers) // on the receiving side const message = deserializeObjectWithCompat(serializedObj) assert(tl.isAnyMessage(message)) const peers = deserializePeersIndexWithCompat(serializedPeers) const message = new Message(message, peers) ``` ::: warning Limitations `deserializeObjectWithCompat` doesn't support *every single object* in the schema, nor *every single layer*, because that way the bundle size would be huge. Objects that most commonly require persistent storage are supported, however, and if you find something missing for your use-case feel free to [open an issue](https://github.com/mtcute/mtcute/issues/new) For the complete list of supported objects, please see [TYPES\_FOR\_COMPAT](https://github.com/mtcute/mtcute/blob/master/packages/core/scripts/tl/constants.ts) or [compat.tl](https://github.com/mtcute/mtcute/blob/master/packages/core/scripts/tl/data/compat.tl) ::: --- --- url: 'https://mtcute.dev/guide/topics/parse-modes.md' --- # Parse modes You may be familiar with parse modes from the Bot API. Indeed, the idea is pretty much the same - parse mode defines the syntax to use for formatting entities in messages. However, there's a major difference – in mtcute, the client doesn't know anything about how the parse modes are implemented. Instead, it just accepts an object containing the `text` and `entities` fields, and sends it to the server: ```ts await tg.sendText('self', { text: 'Hi, User!', entities: [ { _: 'messageEntityBold', offset: 4, length: 4 } ] }) ``` Of course, passing this object manually is not very convenient, so mtcute provides a set of *parsers* that can be used to convert a string with entities to this structure. For convenience, mtcute itself provides two parsers – for Markdown and HTML. They are both implemented as separate packages, and they themselves are tagged template literals, which makes it very easy to interpolate variables into the message. ## Markdown Markdown parser is implemented in `@mtcute/markdown-parser` package: ```ts import { md } from '@mtcute/markdown-parser' dp.onNewMessage(async (msg) => { await msg.answerText(md`Hello, **${msg.sender.username}**`) }) ``` **Note**: the syntax used by this parser is **not** compatible with Bot API's Markdown or MarkdownV2 syntax. See [documentation](https://ref.mtcute.dev/modules/_mtcute_markdown-parser) to learn about the syntax. ## HTML HTML parser is implemented in `@mtcute/html-parser` package. It provides two variants: ### `html` - HTML-like whitespace Whitespace is collapsed just like in real HTML (newlines and consecutive spaces become a single space). Use `
` for line breaks and ` ` for multiple spaces. ```ts import { html } from '@mtcute/html-parser' dp.onNewMessage(async (msg) => { await msg.answerText(html`Hello, ${msg.sender.username}`) }) ``` ### `thtml` - preserved whitespace Whitespace (spaces and newlines) is kept as-is (Bot API style). Common leading indentation is automatically stripped (dedented), so it's safe to use in indented code. ```ts import { thtml } from '@mtcute/html-parser' dp.onNewMessage(async (msg) => { await msg.answerText(thtml` Hello, ${msg.sender.displayName}! Welcome back. `) // text: "Hello, Name!\nWelcome back." }) ``` **Note**: Both variants support all Bot API HTML tags. The `html` variant differs from Bot API only in whitespace handling (it collapses whitespace like real HTML). Use `thtml` for full Bot API compatibility. See [documentation](https://ref.mtcute.dev/modules/_mtcute_html-parser) to learn more about the syntax. ## Interpolation Both parsers support interpolation of variables into the message, as can be seen in the examples above. Both parsers support the following types of interpolation: * `string` - **will not** be parsed, and appended to plain text as-is * `number` - will be converted to string and appended to plain text as-is * `TextWithEntities` or `MessageEntity` - will add the text and its entities to the output. This is the type returned by `md` and `html` themselves, so you can even mix and match them: ```ts const greeting = (user) => html`${user.displayName}` const text = md`**Hello**, ${user}!` ``` * falsy value (i.e. `null`, `undefined`, `false`) - will be ignored ### Unsafe interpolation In some cases, you may already have a string with entities, and want to parse it to entities. In this case, you can use the method as a function: ```ts const text = 'Hello, **User**!' await tg.sendText('self', md(text)) // or even await tg.sendText('self', md`${md(text)} What's new?`) ``` ## Un-parsing Both HTML and Markdown parsers also provide an `unparse` method, which can be used to convert the message back to the original text: ```ts import { html } from '@mtcute/html-parser' const msg = await tg.sendText('me', html`Hi, User!`) console.log(msg.text) // Hi, User! console.log(html.unparse(msg)) // Hi, User! ``` --- --- url: 'https://mtcute.dev/guide/topics/peers.md' --- # Peers One of the most important concepts in MTProto is the "peer". Peer is an object that defines a user, a chat or a channel, and is widely used within the APIs. ## What is a Peer? In MTProto, there are 2 types representing a peer: [Peer](https://core.telegram.org/type/Peer) and [InputPeer](https://core.telegram.org/type/InputPeer) [Peer](https://core.telegram.org/type/Peer) defines the peer type (user/chat/channel) and its ID, and is usually returned by the server inside some other object (like Message). [InputPeer](https://core.telegram.org/type/InputPeer) defines the peer by providing its type, ID and access hash. Access hashes are a mechanism designed to prevent users from accessing peers that they never met. These objects are mostly used when sending RPC queries to Telegram. > There are also `InputUser` and `InputChannel` that are used > to prevent clients from passing incorrect peers > (e.g. restricting a user in a legacy group chat). > > They are basically the same, and you should only care about > them when using Raw APIs, so we'll skip them for now. ::: tip In MTProto, you'll often see `InputSomething` and `Something` types. This simply means that the former should be used when making requests, and the latter is sent by the server back. ::: ## Chats and Channels As you may have noticed, in MTProto there are only three types of peers: users, chats and channels. However, things are not as simple as you may imagine, so let's dive a bit deeper. **[Chat](https://core.telegram.org/constructor/chat)** is a legacy (aka basic) group. The one which is created by default when you use "Create group" button in official clients. Official clients refer to them as "Groups" **[Channel](https://core.telegram.org/constructor/channel)** is anything that is not a user, nor a legacy group. Supergroups, actual broadcast channels and broadcast groups are all represented in MTProto as a **Channel** with a different set of flags: * A **broadcast channel** is a [Channel](https://core.telegram.org/constructor/channel) where `.broadcast === true` * A **supergroup** (also referred to as megagroup) is a [Channel](https://core.telegram.org/constructor/channel) where `.megagroup === true` * A **forum** is a supergroup where `.forum === true` * A **broadcast group** (also referred to as gigagroup) is a [Channel](https://core.telegram.org/constructor/channel) where `.gigagroup === true`. They are basically a **supergroup** where default permissions disallow sending messages and cannot be changed ([src](https://t.me/tdlibchat/15164)). Official clients only use "Channel" when referring to *broadcast channels*. Basic groups are still used *(probably?)* because they are enough for most people's needs, and are also lighter on server resources. However, they are missing many important features for public communities, like: usernames, custom admin rights, per-user restrictions, event log and more. Official clients silently "migrate" legacy groups to supergroups (actually channels) whenever the user wants to use a feature not supported by the Chat, like setting a username. Channel cannot be migrated back to Chat. ### Chat in mtcute In addition to the mess described above, mtcute also has a [Chat](https://ref.mtcute.dev/classes/_mtcute_core.index.Chat) type. It is used to represent any chat-like entities (i.e. basic groups and channels), but **not** users. In many mtcute APIs, you'll also encounter `Peer` type, which is essentially `Chat | User`. ## Communities Telegram also has **communities** — collections of linked group chats (represented in TL as [community](https://core.telegram.org/constructor/community)). They are somewhat similar to channels: on the wire they are addressed with `InputChannel` (though only in `communities.*` RPC methods), and their IDs share the channel [marked ID](#marked-ids) namespace (i.e. `-100...`). In mtcute, communities are represented as a [Chat](https://ref.mtcute.dev/classes/_mtcute_core.index.Chat) with `.chatType === 'community'`, and can be used as an `InputPeerLike` in community-related methods: ```ts const community = await tg.createCommunity({ title: 'test', chatId: someGroup }) await tg.linkCommunityPeer({ communityId: community, peerId: anotherGroup }) const full = await tg.getFullChat(community) console.log(full.linkedPeers) ``` Note, however, that communities are **not** fully-fledged peers: you can't send messages to them, and they can't appear as `msg.chat` or `msg.sender`. Most peer-related methods don't accept them, either. Chats that are linked to a community expose `.linkedCommunityId` (a marked community ID), and the `change_community` service message carries the community's `Chat` object itself. ::: warning Communities are a fairly recent and rather half-baked feature, and the API around them is likely to change in the future. ::: ## Fetching peers Often, you'll need to interact with a peer later, when the respective `Peer` object is no longer available. Of course, manually storing peers along with their access hash is very tedious. That is why mtcute handles it for you! Peers and their access hashes are automatically stored inside the storage you provided, and can be accessed at any time later. This way, to get an InputPeer, you simply need to call `resolvePeer` method and provide a Peer ```ts const peer = await tg.resolvePeer({ _: 'peerUser', userId: 777000 }) ``` But still, this is very tedious, so you can pass many more than just a `Peer` object to `resolvePeer`: * Peer's [marked ID](#marked-ids) * Peer's username * Peer's phone number (will only work with contacts) * `"me"` or `"self"` to refer to yourself (current user) * `Peer`, `InputPeer`, `InputUser` and `InputChannel` objects * `Chat`, `User` or generally anything where `.inputPeer: InputPeer` is available ```ts const peer = await tg.resolvePeer(-1001234567890) const peer = await tg.resolvePeer("durovschat") const peer = await tg.resolvePeer("+79001234567") const peer = await tg.resolvePeer("me") ``` ## `InputPeerLike` However, you will only really need to use `resolvePeer` method when you are working with the Raw APIs. High-level methods use `InputPeerLike`, a special type used to represent an input peer. In fact, we have already covered it - it is the very type that `resolvePeer` takes as its argument: ```ts async function resolvePeer(peer: InputPeerLike): Promise ``` Client methods implicitly call `resolvePeer` to convert `InputPeerLike` to input peer and use it for the MTProto API call. ::: tip Whenever possible, pass `Chat`, `InputPeer` or `"me"/"self"` as `InputPeerLike`, since this avoids redundant storage and/or API calls. When not possible, use their marked IDs, since most of the time it is the cheapest way to fetch an `InputPeer`. For smaller-scale scripts, you *can* use usernames and phone numbers. They are also cached in the storage, but might require additional API call if they are not. Also, not every user has a username, and only your contacts can be fetched by the phone number. ```ts // ❌ BAD await tg.sendText(msg.sender.username!, ...) await tg.sendText(msg.sender.phone!, ...) // 🧐 BETTER await tg.sendText(msg.sender.id, ...) // ✅ GOOD await tg.sendText(msg.sender.inputPeer, ...) await tg.sendText(msg.sender, ...) ``` ::: ## Marked IDs As you may have noticed, both `Peer` and `InputPeer` contain peer type, but when using client methods and Bot API, you don't specify it manually. `Peer` and `InputPeer` contain what is called as a "bare" ID, the ID inside that particular peer type. Bare IDs between different peer types may (and do!) collide, and that is why Marked IDs are used. Marked ID is a slightly transformed variant of the bare ID to unambiguously define both peer type and peer ID with a single integer (currently, it fits into JS number, but later we may be forced to move to 64-bit integers). This was first introduced in TDLib, and was since adopted by many third-party libraries, including mtcute. ::: tip The concept described below is implemented and exported in utils, see [getBasicPeerType](https://ref.mtcute.dev/functions/_mtcute_core.index.getBasicPeerType), [getMarkedPeerId](https://ref.mtcute.dev/functions/_mtcute_core.index.getMarkedPeerId), ::: It works as follows: * `User` IDs are kept as-is (`123 -> 123`) * `Chat` IDs are negated (`123 -> -123`) * `Channel` IDs are subtracted from `-1000000000000` (`1234567890 -> -1001234567890`) Some sources may say that it's simply prepending `-100` to the ID, but that's not entirely true, since channel ID may be not 10 digits long. This way, you can easily determine peer type: ```ts const MIN_CHANNEL_ID = -1002147483647 const MAX_CHANNEL_ID = -1000000000000 const MIN_CHAT_ID = -2147483647 const MAX_USER_ID = 2147483647 if (peer < 0) { if (MIN_CHAT_ID <= peer) return 'chat' if (MIN_CHANNEL_ID <= peer && peer < MAX_CHANNEL_ID) return 'channel' } else if (0 < peer && peer <= MAX_USER_ID) { return 'user' } ``` And then, to convert it back to bare ID, use the exact same operation (it works in two directions). ## Incomplete peers In some cases, mtcute may not be able to immediately provide you with complete information about a user/chat (see [min constructors](https://core.telegram.org/api/min) in MTProto docs). This currently only seems to happen for `msg.sender` and `msg.chat` fields for non-bot accounts in large chats, so if you're only ever going to work with bots, you can safely ignore this section (for now?). ::: tip Complete peers ≠ [full peers](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient#getFullChat)! * Incomplete are seen in updates in rare cases, and are missing some fields (e.g. username) * Complete peers are pretty much all the other peer objects you get in updates * Full peers are objects with additional information (e.g. bio) which you should request explicitly ::: For such chats, the server may send "min" constructors, which contain incomplete information about the user/chat. To avoid blocking the updates loop, and since the missing information is not critical, mtcute will return an incomplete peer object, which is *good enough* for most cases. For example, such `User` objects may have the following data missing or have incorrect values: * `.username` may be missing * `.photo` may be missing with some privacy settings * online status may be incorrect * and probably more (for more info please consult the official docs for [user](https://core.telegram.org/constructor/user) and [channel](https://core.telegram.org/constructor/channel)) The user itself is still usable, though. If you need to get the missing information, you can call `getUsers`/`getChat` method, which will return a complete `User`/`Chat` object. ```ts const user = ... // incomplete user from somewhere const [completeUser] = await tg.getUsers(user) ``` If you are using [Dispatcher](/guide/dispatcher/intro.md), you can use `.getCompleteSender()` or `.getCompleteChat()` methods instead: ```ts dp.onNewMessage(async (msg) => { const sender = await msg.getCompleteSender() const chat = await msg.getCompleteChat() }) ``` Or you can use `withCompleteSender` and `withCompleteChat` middleware-like filters: ```ts dp.onNewMessage( filters.withCompleteSender(filters.sender('user')), async (msg) => { const user = msg.sender // user is guaranteed to be complete } ) ``` --- --- url: 'https://mtcute.dev/guide.md' --- # Quick start This is a quick guide on how to get mtcute up and running as fast as possible. ## Node.js For bots in Node.js, there's a special package that scaffolds a project for you: ```bash pnpm create @mtcute/bot my-awesome-bot ``` Just follow the instructions and you'll get a working bot in no time! ### Manually For existing projects, you'll probably want to add it manually, though. > **Note**: mtcute is currently targeting TypeScript 5.0. > If you are using an older version of TypeScript, please consider upgrading. 1. Get your API ID and Hash at . 2. Install `@mtcute/node` package: ```bash pnpm add @mtcute/node ``` 3. Import the package and create a client: ```ts import { TelegramClient, html } from '@mtcute/node' const tg = new TelegramClient({ apiId: API_ID, apiHash: 'API_HASH' }) const self = await tg.start({ ... }) console.log(`Logged in as ${self.displayName}`) await tg.sendText('self', html`Hello from mtcute!`) ``` 4. That's literally it! Happy hacking 🚀 ## Bun Support for Bun is provided in `@mtcute/bun` package, and Bun is also supported in `@mtcute/create-bot`. ```bash bun create @mtcute/bot my-awesome-bot # or add to an existing project bun add @mtcute/bun ``` ## Deno Support for Deno is provided in `@mtcute/deno` package, which is published to the [jsr.io](https://jsr.io) registry: ```ts import { TelegramClient } from 'jsr:@mtcute/deno' const tg = new TelegramClient({ apiId: 123456, apiHash: '0123456789abcdef0123456789abcdef', storage: 'my-account' // will use sqlite-based storage }) await tg.start() ``` ```bash deno run -A --unstable-ffi your-script.ts ``` Deno is also supported in `@mtcute/create-bot`, which is only available in npm: ```bash deno run -A npm:@mtcute/create-bot my-awesome-bot ``` ## Browser For browsers, it is recommended to use [vite](https://vitejs.dev). Webpack is probably also fine, but you may need to do some extra configuration. For usage in browsers, mtcute provides an `@mtcute/web` package: ```bash pnpm add @mtcute/web ``` ::: info For vite, you'll need to deoptimize `@mtcute/wasm` (see [vite#8427](https://github.com/vitejs/vite/issues/8427)): ```ts // in vite.config.ts export default defineConfig({ optimizeDeps: { exclude: ['@mtcute/wasm'] } }) ``` ::: Then, you can use it as you wish: ```ts import { TelegramClient } from '@mtcute/web' const tg = new TelegramClient({ apiId: 123456, apiHash: '0123456789abcdef0123456789abcdef', storage: 'my-account' // will use IndexedDB-based storage }) tg.call({ _: 'help.getConfig' }).then((res) => console.log(res)) ``` See also: [Tree-shaking](/guide/advanced/treeshaking.md) ## Other runtimes mtcute strives to be as runtime-agnostic as possible, so it should work in any environment that supports some basic ES2020 features (notably, bigints. There's an [unofficial fork](https://github.com/cyan-2048/mtcute) that uses polyfills for bigints, if you're into that). In case your runtime of choice is not listed above, you can try using `@mtcute/core` directly You will need to provide your own implementations of storage, networking and crypto - feel free to take a look at web/node implementations for reference (or even extend them to better fit your needs, e.g. if some runtime only partially supports some Node.js APIs). ```ts import { TelegramClient } from '@mtcute/core/client.js' const tg = new TelegramClient({ ..., storage: new MyStorage(), crypto: new MyCrypto() transport: new MyTransport(), platform: new MyPlatform(), }) ``` --- --- url: 'https://mtcute.dev/guide/dispatcher/rate-limit.md' --- # Rate limit You may want to limit access to certain commands or handlers in your bot, for example to avoid flood limits. For that, you can use rate-limiting feature of the Dispatcher. Rate limiting is built into update state object, and all you have to do is to call `.rateLimit` function: ```ts dp.onNewMessage( filters.command('some_expensive_command'), async (msg, state) => { try { // 1 request every 15 seconds await state.rateLimit('some_expensive_command', 1, 15) } catch (e) { if (e instanceof RateLimitError) { await msg.replyText('Try again later') } throw e } const result = doSomeExpensiveComputations() await msg.replyText(result) } ) ``` In the above example, we use `some_expensive_command` as a key for the rate limit. This allows you to have multiple independent rate limits. When the rate limit is exceeded, `rateLimit` throws `RateLimitError`, which also contains `.reset` field with the Unix time when the rate limit will be reset. ## Throttle In some cases you might want to throttle a used instead of rate-limiting them, and that is exactly what `.throttle` does. When a rate limit is reached, it waits until the rate limit is replenished and then returns: ```ts dp.onNewMessage( filters.start, async (msg, state) => { // no more than 2 rps per user await state.throttle('start', 2, 1) await msg.replyText('Hi!') } ) ``` `throttle` returns tuple containing number of requests left until the user hits the limit, and when the limit will be reset: ```ts const [left, reset] = await state.throttle('some_expensive_command', 1, 15) await msg.replyText(`${left} requests left. Reset at ${new Date(reset).toString()}`) ``` --- --- url: 'https://mtcute.dev/guide/topics/raw-api.md' --- # Raw API mtcute implements a lot of methods to simplify using the Telegram APIs. However, it does not cover the entirety of the API, and in that case, you can resort to using the MTProto APIs directly. ::: warning When using MTProto API directly, you will have to manually implement any checks, arguments normalization, parsing, etc. Whenever possible, use client methods instead! ::: ## Calling MTProto API Before you can call some method, you need to know *what* to call and *how* to call it. To do that, please refer to [TL Reference](https://core.telegram.org/methods). Specifically, read the `node_modules/@mtcute/core/tl/index.d.ts` file. Then, simply pass method name and arguments to `.call()` method: ```ts const result = await tg.call({ _: 'account.checkUsername', username: 'finally_water' }) ``` Thanks to TypeScript, the request object is strictly typed, and the return value also has the correct type: ```ts const result = await tg.call({ _: 'account.checkUsername', username: 42 // error: must be a string }) result.ok // error: boolean does not have `ok` property ``` ## Common parameters In MTProto APIs, there are some parameters that are often encountered in different methods, and are briefly described below: | Name | Description | Safe default value | |---|---|---| | `hash` | Hash of the previously stored content, used to avoid re-fetching the content that is not modified. Methods that use this parameter have `*NotModified` class as one of the possible return types. It is not returned if `hash=0`. | `0` | `offset` | Offset for pagination | `0` | `limit` | Maximum number of items for pagination, max. limit is different for every method. | `0`, this will usually default to ~20 | `randomId` | Random message ID to avoid sending the same message. | `randomLong()` (exported by `@mtcute/core/utils.js`) Learn more about pagination in [Telegram docs](https://core.telegram.org/api/offsets) ## Resolving peers To fetch a value for fields that require `InputPeer`, use `resolvePeer` method. If you need `InputUser` or `InputChannel`, you can use the shorthand methods `resolveUser` and `resolveChannel`: ```ts const result = await tg.call({ _: 'channels.reportSpam', channel: await tg.resolveChannel(channelId), userId: await tg.resolveUser(userId), id: [1, 2, 3] }) ``` Alternatively, you can use `toInputChannel`/`toInputUser` on a resolved `InputPeer`: ```ts const channel = toInputChannel(await tg.resolvePeer(channelId)) ``` These functions will throw in case the peer is of wrong type. When you need to dispatch different API calls based on peer type (chat vs channel), use the type guards: ```ts import { isInputPeerChannel, toInputChannel } from '@mtcute/core/utils.js' const peer = await tg.resolvePeer(chatId) if (isInputPeerChannel(peer)) { await tg.call({ _: 'channels.deleteMessages', channel: toInputChannel(peer), id: ids }) } else { await tg.call({ _: 'messages.deleteMessages', id: ids, revoke: true }) } ``` ## Handling Updates Some RPC methods return `Updates` type. For these methods, it is important that they are properly handled by Updates manager. This is done by calling `tg.handleClientUpdate` method: ```ts const res = await tg.call({ _: 'contacts.addContact', ... }) tg.handleClientUpdate(res) ``` ::: tip Calling `tg.handleClientUpdate` will not dispatch all the updates contained in that object. This is sometimes undesirable, and can be avoided by passing `false` as the second argument: ```ts tg.handleClientUpdate(res, false) ``` ::: ::: details Why is this important? When RPC method returns `Updates`, it might have newer PTS and SEQ values. If it does, passing it to `handleClientUpdate` makes the library aware of those new updates. Otherwise, library would have to re-fetch them the next time an update is encountered. Also, in case PTS/SEQ values are bigger than the next expected value, an *update gap* is detected and missing updates will be fetched. ::: ### Dummy updates Some methods return not updates, but a class like [messages.affectedHistory](https://corefork.telegram.org/constructor/messages.affectedHistory). They also contain PTS values, and should also be handled. But since this is not an update, it can't be passed directly to `handleClientUpdate`, and instead a "dummy" update is created: ```ts const res = await this.call({ _: 'messages.deleteMessages', id: [1, 2, 3], }) const upd = createDummyUpdate(res.pts, res.ptsCount) tg.handleClientUpdate(upd) ``` Or, in case this PTS is related to a channel: ```ts const channel = toInputChannel(peer) const res = await this.call({ _: 'channels.deleteMessages', channel, id: [1, 2, 3], }) const upd = createDummyUpdate(res.pts, res.ptsCount, channel.channelId) tg.handleClientUpdate(upd) ``` ## Files and media To get an `InputFile` from `InputFileLike`, use `_normalizeInputFile`: ```ts const file = 'file:theme.txt' const res = await tg.call({ _: 'account.uploadTheme', file: await tg._normalizeInputFile(file), ... }) ``` To get an `InputMedia` from `InputMediaLike`, use `_normalizeInputMedia`: ```ts const file = InputMedia.auto('BQACAgEAAx...Z2mGB8E') const res = await tg.call({ _: 'messages.uploadMedia', media: await tg._normalizeInputMedia(file), ... }) ``` ## Message entities To normalize text with entities for use in raw API calls, use `_normalizeInputText`: ```ts const [text, entities] = await tg._normalizeInputText(inputText) ``` `InputText` is either a plain string or an object `TextWithEntities { text: string, entities: tl.TypeMessageEntity[] }`. The function handles resolving `messageEntityMentionName` entities (converting them to `inputMessageEntityMentionName` with proper `InputUser`) and trimming whitespace while adjusting entity offsets. To create formatted text using a parse mode, use the tagged template literals (e.g. html\`\Hello!\\` or md\`\*\*Hello!\*\*\`) which produce `TextWithEntities` objects that can be passed as `InputText`. ## Fully custom requests mtcute also allows you to send fully custom requests to the server. This is useful if you want to use some undocumented or yet-unreleased APIs and don't want to patch the library or use the TL schema override mechanism. You can use the `mtcute.customMethod` pseudo-method for that: ```ts const res = await tg.call({ _: 'mtcute.customMethod', bytes: Buffer.from('11223344', 'hex'), }) ``` `bytes` will be sent as-is to the server, and the response will be returned as a `Uint8Array` for you to handle on your own (it might be useful to look into [`@mtcute/tl-runtime` package](https://ref.mtcute.dev/modules/_mtcute_tl-runtime)) --- --- url: 'https://mtcute.dev/guide/dispatcher/scenes.md' --- # Scenes Scene is basically a child dispatcher with a name. It is not used by default, unless you explicitly **enter** into it, after which **all** (supported\*) updates will be redirected to the scene, and not processed as usual. This is particularly useful with FSM, since it allows users to enter independent "dialogues" with the bot. ## Creating a scene A scene is created by using `Dispatcher.scene`: ```ts interface SceneState { ... } const dp = Dispatcher.scene('scene-name') // add handlers to `dp` export const SomeScene = dp // then in the main file: dp.addScene(SomeScene) ``` If you don't use state within your scene, just don't pass anything: ```ts const scene = Dispatcher.scene() ``` ::: tip Scenes should only be added to the root dispatcher. ::: Scene names can't start with `$` (dollar sign), since it is reserved for internal FSM needs. Other than that, you can use any name. ## Entering a scene To enter a scene or change current scene, use `state.enter` and pass the scene instance: ```ts dp.onNewMessage(async (msg, state) => { await state.enter(SomeScene) }) ``` You can also pass some initial state to the scene: ```ts dp.onNewMessage(async (msg, state) => { await state.enter(SomeScene, { with: { foo: 'bar' } }) }) ``` By default, new scene will be used starting from the next update, but in some cases you may want it to be used immediately. To make the dispatcher immediately dispatch the update to the newly entered scene, use `PropagationAction.ToScene`: ```ts dp.onNewMessage(async (msg, state) => { await state.enter(SomeScene) return PropagationAction.ToScene }) ``` ## Exiting a scene To exit from the current scene, use `state.exit`: ```ts dp.onNewMessage(async (msg, state) => { await state.exit() }) ``` To make the dispatcher immediately dispatch the update to the root dispatcher, use `PropagationAction.ToScene`: ```ts dp.onNewMessage(async (msg, state) => { await state.exit() return PropagationAction.ToScene }) ``` Entering another scene will also exit the current one. ## Isolated state By default, scenes have their own, fully isolated FSM state, which is (by default) destroyed as soon as the user leaves the scene. This is more clean than using the global state, and also allows scenes to have their own state type. However, in some cases, you may want to access global FSM state. This is possible with `getGlobalState`: ```ts dp.onNewMessage(async (msg, state) => { const local = await state.get() const globalState = await dp.getGlobalState(msg) const global = await globalState.get() }) ``` Alternatively, you can disable isolated storage for FSM altogether and use global state directly: ```ts const dp = Dispatcher.scene() // add handlers to `dp` export const SomeScene = dp // in the main file: dp.addScene(SomeScene, /* scoped: */ false) ``` In this case, `scene` can't have state type other than `BotState` (i.e. the one used by the parent), and it will not be reset when the user leaves the scene. ## Wizard scenes A commonly used pattern for scenes is a step-by-step wizard. To simplify their creation, mtcute implements `WizardScene`, which is simply a Dispatcher with an additional method: `addStep`. Every step is an `onNewMessage` handler that is filtered by the current step, which is stored in wizard's FSM state. In each step, you can choose either to `WizardSceneAction.Stay` in the same step, proceed to the `WizardSceneAction.Next` step, or `WizardSceneAction.Exit` the wizard altogether. You can also return a `number` to jump to some step (ordering starts from 0). Additionally, wizard provides `onCurrentStep` filter that filters for updates that happened *after* the last triggered step. A simple example: ```ts interface RegForm { name?: string } const wizard = new WizardScene('REGISTRATION') wizard.addStep(async (msg) => { await msg.answerText('What is your name?', { replyMarkup: BotKeyboard.inline([[BotKeyboard.callback('Skip', 'SKIP')]]), }) return WizardSceneAction.Next }) wizard.onCallbackQuery(filters.and(wizard.onCurrentStep(), filters.equals('SKIP')), async (upd, state) => { await state.merge({ name: 'Anonymous' }) await wizard.skip(state) await upd.client.sendText(upd.chatId, 'Alright, "Anonymous" then\n\nNow enter your email') }) wizard.addStep(async (msg, state) => { // simple validation if (msg.text.length < 3) { await msg.replyText('Invalid name!') return WizardSceneAction.Stay } await state.set({ name: msg.text.trim() }) await msg.answerText('Enter your email') return WizardSceneAction.Next }) wizard.addStep(async (msg, state) => { const { name } = (await state.get())! console.log({ name, email: msg.text }) await msg.answerText('Thanks!') return WizardSceneAction.Exit }) ``` If you are using some custom state, you may want to set the default state for the wizard: ```ts wizard.setDefaultState({ name: 'Ivan' }) ``` By default, `{}` is used as the default state. ## Transition updates Whenever you `.enter()` or `.exit()` a scene, the dispatcher will also emit a transition update, which can be caught by using `onSceneTransition`: ```ts scene.onSceneTransition(async (upd, state) => { console.log(`Transition from ${upd.previousScene} to SomeScene`) }) ``` These handlers are called **before** any of the scene's handlers are called, even if `PropagationAction.ToScene` is used, and can be used to cancel the transition: ```ts dp.onNewMessage(async (msg, state) => { await state.enter(SomeScene) return PropagationAction.ToScene }) SomeScene.onSceneTransition(async (upd, state) => { await state.exit() return PropagationAction.Stop }) SomeScene.onNewMessage(async (msg, state) => { await msg.replyText('This will never be called') }) ``` The update which triggered the transition is passed to the handler, and you can use it to decide whether to cancel the transition or not: ```ts SomeScene.onSceneTransition(async (upd, state) => { if (upd.message.text === 'cancel') { return PropagationAction.Stop } }) ``` --- --- url: 'https://mtcute.dev/guide/intro/sign-in.md' --- # Signing in ::: warning Before we start, **please do not** use mtcute to abuse Telegram services or harm other users by any means (including spamming, scraping, scamming, etc.) Bots are meant to help people, not hurt them, so **please** make sure you don't :) ::: ## API Keys Before you can use this library (or any other MTProto library, for that matter), you need to obtain API ID and API Hash from Telegram: 1. Go to and log in with your Telegram account 2. Fill out the form to create a new application ::: tip You can leave URL field empty. App name and short name can (currently) be changed later. Note that you **will not** be able to create another application using the same account. ::: 3. Press *Create Application*, and you'll see your `api_id` and `api_hash`. :::warning Be careful with your API hash. It can not be revoked. ::: ## Signing in Now that we have got our API keys, we can sign in into our account: ```ts // Replace with your own values const tg = new TelegramClient({ apiId: API_ID, apiHash: 'API_HASH' }) const self = await tg.start({ phone: () => tg.input('Phone > '), code: () => tg.input('Code > '), password: () => tg.input('Password > ') }) console.log(`Logged in as ${self.displayName}`) ``` ::: tip `tg.input` is a tiny wrapper over the `node:readline` module, that will ask you for input in the console. It's not available in `@mtcute/core`, since it is platform-agnostic ::: ::: warning If you are using some kind of watcher (notably, [tsx](https://github.com/privatenumber/tsx/issues/163) suffers from this), you may run into issues when entering the values. In that case, try disabling the watcher functionality when logging in. ::: ## Signing in as a bot You can also use mtcute for bots (created via [@BotFather](https://t.me/BotFather)). You will still need API ID and Hash, though: ```ts{10} // Replace with your own values const tg = new TelegramClient({ apiId: API_ID, apiHash: 'API_HASH' }) const self = await tg.start({ botToken: '12345678:0123456789abcdef0123456789abcdef' }) console.log(`Logged in as ${self.displayName}`) ``` ## QR sign in mtcute also supports QR code sign in: ```ts import { encodeQR } from 'qr' // Replace with your own values const tg = new TelegramClient({ apiId: API_ID, apiHash: 'API_HASH' }) const self = await tg.start({ qrCodeHandler: (url, expires) => { // display the `url` as the qr code to the user, for example print it to console: console.log(encodeQR(url, 'ascii')) }, // logging in via qr still requires entering a 2fa password if you have one set up password: () => tg.input('Password > ') }) console.log(`Logged in as ${self.displayName}`) ``` ## Storing your API ID and Hash In the examples above, we hard-coded the API keys. It works fine, but it is better to not keep that kind of stuff in the code, let alone publish them to public repositories. Instead, it is a good practice to use environment variables and a `.env` that will contain them.\ You can load it then using [dotenv-cli](https://npmjs.org/package/dotenv-cli): ```bash # .env API_ID=123456 API_HASH=0123456789abcdef0123456789abcdef ``` ```ts const tg = new TelegramClient({ apiId: process.env.API_ID, apiHash: process.env.API_HASH }) ``` ```bash dotenv tsx your-file.ts ``` ::: note When making a public-facing client application, you'll inevitably expose them to the public, so it's not like it's game over if they get leaked. Nevertheless, it is still usually a good practice to not hardcode them. ::: ## Using a proxy When using Node.js, you can also connect to Telegram through proxy. This is particularly useful in countries like Iran or Russia, where Telegram might be limited. To learn how to set up a connection through proxy, refer to [Transport](../topics/transport.html#http-s-proxy-transport) documentation ## Manual sign in So far we've only discussed the `.start` helper method. While it does provide some flexibility and convenience, it is intended for handling *interactive* authorization flows, e.g. in cli or web apps. If you are building some kind of headless service, you will most likely want to use the underlying authorization methods directly, as described below. First, check if you are already signed in: ```ts async function checkSignedIn() { try { // Try calling any method that requires authorization // (getMe is the simplest one and likely the most useful, // but you can use any other) return await tg.getMe() } catch (e) { if (tl.RpcError.is(e, 'AUTH_KEY_UNREGISTERED')) { // Not signed in, continue return null } else { // Some other error, rethrow throw e } } } ``` If you are not signed in, you should first use `.sendCode` method: ```ts // phone can be in pretty much any format, // mtcute will automatically normalize it const phone = '+7 999 123 4567' const code = await tg.sendCode({ phone }) ``` The `code` object will contain [information about the sent code](https://ref.mtcute.dev/classes/_mtcute_core.index.SentCode), including the `phoneCodeHash` that you will need to use later. Now, you need to ask the user for the code and call `.signIn` method: ```ts const code = '12345' // code from user input const user = await tg.signIn({ phone, phoneCodeHash: code.phoneCodeHash, phoneCode: code }) ``` This method may either return right away, or throw one of: * `SESSION_PASSWORD_NEEDED` if the account has 2FA enabled, and you should [enter the password](#handling-2fa) * `PHONE_CODE_INVALID` if the code entered was invalid * `PHONE_CODE_EXPIRED` if the code has expired, and you should [resend it](#resending-code) ### Handling 2FA If the account has 2FA enabled, you will need to ask the user for the password, and then call `.checkPassword`: ```ts const password = 'hunter2' // password from user input const user = await tg.checkPassword(password) ``` In case of invalid password, this method will throw `PASSWORD_HASH_INVALID`. ### Resending code In some cases, the code may expire before the user enters it, or it may never arrive, so you may want to resend it. To do that, you can use `.resendCode` method: ```ts code = await tg.resendCode({ phone, phoneCodeHash: code.phoneCodeHash }) ``` ::: tip You can know beforehand the type of the next code that will be sent using the [`code.nextType`](https://ref.mtcute.dev/classes/_mtcute_core.index.SentCode#nextType) field ::: ### Updates If you want the updates to be processed, you should manually use `startUpdatesLoop` once you are signed in: ```ts if (await checkSignedIn()) { tg.startUpdatesLoop() } else { // some sign in logic tg.startUpdatesLoop() } ``` `tg.start` method does this automatically for you. --- --- url: 'https://mtcute.dev/guide/dispatcher/state.md' --- # State Finite State Machine (**FSM** for short, or simply **State**) is a commonly used concept when developing bots that allows the bot to "remember" its state, which in turn makes the bot more interactive and user-friendly. ::: tip FSM is even more useful when used with [Scenes](scenes.html) ::: ## Setup Dispatcher natively supports FSM. To set it up, simply pass a [storage](#storage) to the constructor: ```ts interface BotState { ... } const dp = Dispatcher.for(tg, { storage: new MemoryStateStorage() }) // or, for children const dp = Dispatcher.child() ``` You **must** provide some state type in order to use FSM (in the example above, `BotState`). You *can* use `any`, but this is not recommended. Then, the update state argument will be available in every handler that supports FSM (that is: `new_message`, `edit_message`, `message_group`, `callback_query`) as well as to their filters: ```ts dp.onNewMessage(async (msg, state) => { // ... }) ``` ::: warning Type parameter for `Dispatcher` (in this case, `BotState`) is only used as a *hint* for the compiler. It is not checked at runtime. ::: ## Getting current state To retrieve the current state, use `state.get`: ```ts dp.onNewMessage(async (msg, state) => { const current = await state.get() }) ``` By default, if there's no state stored, `null` is returned. However, you can provide the default fallback state, which will be used instead: ```ts dp.onNewMessage(async (msg, state) => { const current = await state.get({ ... }) // or a function const current = await state.get(() => ({ ... })) }) ``` ## Updating state To update the state, use `state.set`: ```ts dp.onNewMessage(async (msg, state) => { await state.set({ ... }) }) ``` You can also set a TTL, after which the newly set state will be considered "stale" and removed: ```ts dp.onNewMessage(async (msg, state) => { // ttl = 1 hour await state.set({ ... }, 3600) }) ``` You can also modify the existing state by only providing the modification (under the hood, the library will fetch the current state automatically): ```ts dp.onNewMessage(async (msg, state) => { await state.merge({ ... }) }) ``` If the state can be empty, make sure to pass the default state, otherwise an error will be thrown: ```ts dp.onNewMessage(async (msg, state) => { await state.merge({ ... }, defaultState) }) ``` ## Removing state To remove currently stored state, use `state.delete`: ```ts dp.onNewMessage(async (msg, state) => { await state.delete() }) ``` ## Related filters As mentioned above, state (and its type!) is also available to the filters, so you can make [custom filters](filters.html#custom-filters) that use it: ```ts dp.onNewMessage( (msg, state) => state.get().then((res) => res?.action === 'ENTER_PASSWORD'), async (msg, state) => { // ... } ) ``` However, the above isn't very clean, so the library provides `filters.state`: ```ts dp.onNewMessage( filters.state((state) => state.action === 'ENTER_PASSWORD'), async (msg, state: UpdateState) => { const current = await state.get() // or, if you have strict null checks const current = (await state.get())! } ) ``` Note that here we explicitly pass inner type, because due to TypeScript limitations, we can't automatically derive state type from the predicate. `filters.state` *does not* match empty state, instead, use `filters.stateEmpty`: ```ts dp.onNewMessage( filters.stateEmpty, async (msg, state) => { // ... } ) ``` ## Keying FSM may look like magic, but in fact it is not. Under the hood, user's state is stored in the [storage](#storage), and the key is derived from the update object. By default, `defaultStateKeyDelegate` is used, which derives the key as follows: * If private chat, `msg.chat.id` * If group chat, `msg.chat.id + '_' + msg.sender.id` * If channel, `msg.chat.id` * If callback query from a non-inline message: * If in private chat (i.e. `upd.chatType === 'user'`), `upd.user.id` * If in group/channel/supergroup (i.e. `upd.chatType !== 'user'`), `upd.chatId + '_' + upd.user.id` This is meant to be a pretty opinionated default, but you can use custom keying mechanism too, if you want: ```ts const customKey = (upd) => ... const dp = Dispatcher.for(tg, { storage, key: customKey }) // or, locally for a child dispatcher: const dp = Dispatcher.child({ key: customKey }) ``` ## Getting state from outside In some cases, you may need to access the state (and maybe even alter it) outside of handlers, in the handler that does not support state, or using a different key. You can do so by using `.getState`: ```ts const state = await dp.getState(await msg.getReply()) // you can also pass User/Chat instances: const state = await dp.getState(msg.sender) // and then, for example await state.delete() ``` When providing an object, dispatcher will use its own keying mechanism(s). You can provide a key manually to avoid that: ```ts const target = msg.getReply() const state = dp.getState(defaultStateKeyDelegate(target)) // or even manually const state = await dp.getState(`${target.chat.id}`) ``` You can also provide a totally custom key to store arbitrary data: ```ts // tip: prefix the key with $ and then something unique // to avoid clashing with FSM and Scenes const state = await dp.getState(`$internal-user-pref:${userId}`) ``` ::: warning `getState` **DOES NOT** guarantee type of the state, because it can not determine the origin of state key. By default, it uses dispatcher's state type, but you can also override this with type parameter: ```ts const state = await dp.getState(...) ``` ::: ## Storage Storage is the backend used by Dispatcher to store state related information. A storage is a class that implements [`IStateStorageProvider`](https://ref.mtcute.dev/types/_mtcute_dispatcher.IStateStorageProvider). ```ts const dp = Dispatcher.for(tg, { storage: new MemoryStateStorage() }) // or, locally for a child dispatcher: const dp = Dispatcher.child({ storage: new MemoryStateStorage() }) ``` ### SQLite storage You can re-use your existing SQLite storage for FSM: ```ts import { SqliteStorage } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' import { SqliteStateStorage } from '@mtcute/dispatcher' const storage = new SqliteStorage('my-account') const tg = new TelegramClient({ ..., storage }) const dp = Dispatcher.for(tg, { storage: SqliteStateStorage.from(storage) }) ``` Alternatively, you can create a new SQLite storage specifically for FSM: ```ts import { SqliteStorageDriver } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' import { SqliteStateStorage } from '@mtcute/dispatcher' const dp = Dispatcher.for(tg, { storage: new SqliteStateStorage(new SqliteStorageDriver('my-state')) }) ``` --- --- url: 'https://mtcute.dev/guide/topics/storage.md' --- # Storage Storage is a very important aspect of the library, which should not be overlooked. It is primarily used to handle caching and authorization (you wouldn't want to log in every time, right?). ## In-memory storage The simplest way to store data is to store it in-memory and never persist it anywhere, and this is exactly what `MemoryStorage` does. ```ts{4} import { MemoryStorage } from '@mtcute/core' const tg = new TelegramClient({ storage: new MemoryStorage() }) ``` ::: warning It is highly advised that you use some kind of persisted storage! With in-memory storage, you will need to re-authorize every time (assuming you don't use [session strings](#session-strings)), and also caching won't work past a single run. ::: ## SQLite storage The preferred storage for a server application is the one using SQLite, because it does not require loading the entire thing into memory, and is also faster than simply reading/writing a file. mtcute implements sqlite storages in runtime-specific packages, using the best libraries available for each runtime: * Node.js: [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) * Bun: `bun:sqlite` * Deno: `node:sqlite` ```ts{4} import { SqliteStorage } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' const tg = new TelegramClient({ storage: new SqliteStorage('my-account.session') }) ``` ::: tip In runtime-specific packages, SQLite storage is the default, and you can simply pass a string with file name instead of instantiating `SqliteStorage` manually: ```ts const tg = new TelegramClient({ storage: 'my-account.session' }) ``` ::: To improve performance, we use WAL mode by default ([Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md)). When using WAL, along with your SQLite file there may also be `-shm` and `-wal` files. If you don't like seeing those files, instead of disabling WAL altogether, consider putting your storage in a folder (i.e. `new SqliteStorage('storage/my-account')`). If you are in fact having problems with WAL mode, you can disable it with `disableWal` parameter. ## PostgreSQL storage For applications that already use PostgreSQL, or for deployments where a shared database is preferred over per-instance SQLite files, you can use `@mtcute/postgres`: ```bash pnpm add @mtcute/postgres pg ``` ```ts{5} import { TelegramClient } from '@mtcute/node' import { PostgresStorage } from '@mtcute/postgres' import pg from 'pg' const pool = new pg.Pool({ connectionString: 'postgres://localhost/mydb' }) const tg = new TelegramClient({ storage: new PostgresStorage(pool), }) ``` All tables are created in a dedicated schema (`mtcute` by default), which can be changed via the `schema` option: ```ts new PostgresStorage(pool, { schema: 'my_schema' }) ``` By default, mtcute does not close the client connection when the storage is destroyed. Set `autoClose: true` if you want the storage to handle that for you. Multiple clients can share the same database by using the `account` option: ````ts const pool = new pg.Pool({ connectionString: 'postgres://localhost/mydb' }) const bot1 = new TelegramClient({ storage: new PostgresStorage(pool, { account: 'bot-1' }), // ... }) const bot2 = new TelegramClient({ storage: new PostgresStorage(pool, { account: 'bot-2' }), // ... }) [PGlite](https://github.com/electric-sql/pglite) is also supported as a PostgreSQL client, useful for testing or embedded scenarios: ```ts import { PGlite } from '@electric-sql/pglite' const tg = new TelegramClient({ storage: new PostgresStorage(await PGlite.create()), }) ```` ## IndexedDB storage The preferred storage for a Web application is the one using IndexedDB, which is basically a browser's version of SQLite. ```ts{4} import { IdbStorage } from '@mtcute/web' const tg = new TelegramClient({ storage: new IdbStorage('my-account') }) ``` > Note that the string passed will be used as-is as the database name, > so you might want to prefix it to avoid conflicts. ::: tip In `@mtcute/web`, IndexedDB storage is the default, and you can simply pass a string with file name instead of instantiating `IdbStorage` manually: ```ts const tg = new TelegramClient({ storage: 'my-account' }) ``` ::: ## Session strings Sometimes it might be useful to export storage data to a string, and import it later to another storage. For example, when deploying userbot applications to a server, where you'll be using another storage. To generate a session string, simply call `exportSession`: ```ts await tg.start() console.log(await tg.exportSession()) ``` This will output a fairly long string (about 400 chars) to your console, which can then be imported: ```ts const tg = new TelegramClient({...}) await tg.importSession(SESSION_STRING) // or await tg.start({ session: SESSION_STRING }) ``` You can import session into any storage, including in-memory storage. This may be useful when deploying to services like [Heroku](https://www.heroku.com), where their ephemeral file system makes it impossible to use file-based storage. ::: warning Anyone with this string will be able to authorize as you and do anything. Treat this as your password, and **never give it away**! In case you have accidentally leaked this string, make sure to revoke this session in account settings: "Privacy & Security" > "Active sessions" > find the one containing "mtcute" > Revoke, or, in case this is a bot, revoke bot token with [@BotFather](https://t.me/botfather) Also note that you can't log in with the same session string from multiple IPs at once, and that would immediately revoke that session. ::: ::: details What is included? You might be curious about the information that the session string includes, and why is it so long. Most of the string is occupied by 256 bytes long MTProto authorization key, which, when Base64 encoded, results in **344** characters. Additionally, information about user (their ID and whether the user is a bot) and their DC is included, which results in an average of ~**400** characters ::: ## Implementing custom storage The easiest way to implement a custom storage would be to make a subclass of `MemoryStorage`. Additionaly, mtcute abstracts away the sqlite storage implementation, so you can use the the `BaseSqliteStorage` API to implement sqlite storage using your library of choice (see [Node.js implementation](https://github.com/mtcute/mtcute/tree/master/packages/node/src/sqlite) for reference). ### Architecture A storage provider in mtcute is composed of: * **Driver**: the core of the storage, which handles reading and writing data to the storage and implements lifecycle methods like `load` and `save`. Driver also manages migrations for the storage, however the migrations themselves are not part of the driver, but are registered separately by repositories * **Repository**: a set of methods to read and write data of a specific entity to the storage, allowing for more efficient and organized access to the data. Repositories are registered in the driver and are used to access the data in the storage Such composable architecture allows for custom storages to implement a specific set of repositories, and to reuse the same driver for different providers. In mtcute, these sets of repositories are defined: * [IMtStorageProvider](https://ref.mtcute.dev/types/_mtcute_core.index.IMtStorageProvider), used by `BaseTelegramClient` for low-level MTProto data storage * [ITelegramStorageProvider](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ITelegramStorageProvider), used by `TelegramClient` for basic caching and update handling operations required for the client to work * [IStateStorageProvider](https://ref.mtcute.dev/types/_mtcute_dispatcher.IStateStorageProvider), used by `Dispatcher` for FSM and Scenes storage --- --- url: 'https://mtcute.dev/guide/topics/transport.md' --- # Transport Transport is a way for mtcute to communicate with Telegram servers. mtcute comes bundled with TCP and WebSocket transport, and also supports proxies via additional packages. ## TCP transport TCP transport is the default transport for Node.js, and is implemented using `net.Socket` in `@mtcute/node`: ```ts{5} import { TcpTransport } from '@mtcute/node' const tg = new TelegramClient({ // ... transport: new TcpTransport() }) ``` ::: tip In Node.js it is used automatically, you don't need to pass this explicitly ::: ## WebSocket transport WebSocket transport is mostly used for the browser, but can also be used in Node.js. It is implemented in `@mtcute/web`: ```ts{5} import { WebSocketTransport } from '@mtcute/web' const tg = new TelegramClient({ // ... transport: new WebSocketTransport() }) ``` ::: tip In browser, it is used automatically, you don't need to pass this explicitly ::: ## HTTP(s) Proxy transport To access Telegram via HTTP(s) proxy, you can use `HttpProxyTcpTransport`, which is provided by runtime-specific packages: ```ts{5-8} import { HttpProxyTcpTransport } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' const tg = new TelegramClient({ // ... transport: new HttpProxyTcpTransport({ host: '127.0.0.1', port: 8080 }) }) ``` ## SOCKS4/5 Proxy transport To access Telegram via SOCKS4/5 proxy, you can use `SocksProxyTcpTransport`, which is provided by runtime-specific packages: ```ts{5-8} import { SocksProxyTcpTransport } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' const tg = new TelegramClient({ // ... transport: new SocksProxyTcpTransport({ host: '127.0.0.1', port: 8080 }) }) ``` ## MTProxy transport To access Telegram via MTProxy (MTProto proxy), you can use `MtProxyTcpTransport`, which is provided by runtime-specific packages: ```ts{5-8} import { MtProxyTcpTransport } from '@mtcute/node' // or '@mtcute/bun' / '@mtcute/deno' const tg = new TelegramClient({ // ... transport: new MtProxyTcpTransport({ host: '127.0.0.1', port: 8080, secret: '0123456789abcdef0123456789abcdef' }) }) ``` ::: tip mtcute supports all kinds of MTProxies, including the newer ones with Fake TLS ⚡️ ::: ## Changing transport at runtime It is possible to change transport at runtime. For example, this could be used to change proxy used to connect to Telegram. To change the transport, simply call `changeTransport`: ```ts tg.mt.network.changeTransport(new MtProxyTcpTransport({...})) ``` > Note: the `mt` field is only available on `BaseTelegramClient` instances. ## Implementing custom transport When targeting an environment which is not supported already, you can implement a custom transport on your own. In fact, it is much simpler than it sounds! You can check out source code for the bundled transports to get the basic idea [here](https://github.com/mtcute/mtcute/tree/master/packages/core/src/network/transports), and re-use any packet codecs that are included. Transports in mtcute are built on top of [`@fuman/net`](https://github.com/teidesu/fuman/tree/main/packages/net), which is an in-house networking abstraction library used by mtcute. It is a very powerful library which makes it super easy to implement custom transports. There isn't much documentation, but feel free to check out the source code [here](https://github.com/teidesu/fuman/blob/main/packages/node/src/net/connection.ts). --- --- url: 'https://mtcute.dev/guide/advanced/treeshaking.md' --- # Tree-shaking Being a ESM-first library, mtcute supports tree-shaking out of the box. This means that you can import only the parts of the library that you need, and the bundler will remove all the unused code. ## Usage To start using tree-shaking, there are a few things to keep in mind: * Do not use `TelegramClient`. Use `BaseTelegramClient` instead, and import the needed methods. For example, instead of this: ```ts import { TelegramClient } from '@mtcute/web' const tg = new TelegramClient({ ... }) await tg.sendText(...) ``` you should use this: ```ts import { BaseTelegramClient } from '@mtcute/web' import { sendText } from '@mtcute/web/methods.js' const tg = new BaseTelegramClient({ ... }) await sendText(tg, ...) ``` * TL serialization is currently not tree-shakeable, because it is done via a global map of constructors. There's no ETA on when (or whether at all) this will be changed, so there *will* be ~300 KB of non-shakeable code. --- --- url: 'https://mtcute.dev/guide/advanced/workers.md' --- # Workers To facilitate parallel processing and avoid blocking the main thread, mtcute supports extractnig the heavy lifting to the workers. This is especially useful in the browser, where the main thread is often busy with rendering and other tasks. ::: warning Workers support is still experimental and may have some rough edges. If something doesn't work in a worker, but works when used directly, please open an issue. ::: ## Browser `@mtcute/web` package exports a `TelegramWorker` and `TelegramWorkerPort` classes, which can be used to create workers and communicate with them. To create a worker, use the `TelegramWorker` class: ```ts import { BaseTelegramClient, TelegramWorker } from '@mtcute/web' const tg = new BaseTelegramClient({ apiId: 123456, apiHash: '...', }) new TelegramWorker({ client: tg, }).mount() ``` `TelegramWorker` construction is side-effect-free. Call `.mount()` once to bind it to the worker transport. `TelegramWorkerPort.destroy()` only closes that logical port. It does not destroy the shared client inside the worker. To communicate with the worker, use the `TelegramWorkerPort` class and pass an instance of `Worker` (or `SharedWorker`) to it: ```ts import { TelegramWorkerPort } from '@mtcute/web' const port = new TelegramWorkerPort({ worker: new Worker( new URL('./worker.ts', import.meta.url), { type: 'module' }, }) }) ``` ## Node.js On the surface, the API is largely the same, but is slightly different under the hood and uses `worker_threads` instead of web workers. The worker is created the same way, but using `TelegramWorker` class from `@mtcute/node`: ```ts import { BaseTelegramClient, TelegramWorker } from '@mtcute/node' const tg = new BaseTelegramClient({ apiId: 123456, apiHash: '...', }) new TelegramWorker({ client: tg, }).mount() ``` Then, to communicate with the worker, use the `TelegramWorkerPort` class and pass an instance of `Worker` to it: ```ts import { Worker } from 'worker_threads' import { TelegramWorkerPort } from '@mtcute/node' const port = new TelegramWorkerPort({ worker: new Worker( new URL('./worker.js', import.meta.url), { type: 'module' }, ), }) ``` ## Usage `TelegramWorkerPort` is a drop-in replacement for `BaseTelegramClient`, and since it implements `ITelegramClient`, you can pass it to any method that expects a client: ```ts import { sendText } from '@mtcute/web/methods.js' await sendText(port, 'me', 'Hello from worker!') ``` Alternatively, you can pass the port as a cliant to `TelegramClient` to bind it to all methods (not recommended in browser, see [Tree-shaking](./treeshaking.md)): ```ts const tg = new TelegramClient({ client: port }) await tg.sendText('me', 'Hello from worker!') ``` ## Multiple clients in a single worker If you want to use multiple clients in a single worker, you can set per-worker unique `workerId` option when creating the worker: ```ts new TelegramWorker({ client: tg, workerId: '1', }).mount() new TelegramWorker({ client: tg2, workerId: '2', }).mount() ``` Then, you can pass the worker id to the port constructor: ```ts const worker = new Worker( new URL('./worker.js', import.meta.url), { type: 'module' }, ) const port1 = new TelegramWorkerPort({ worker, workerId: '1', }) const port2 = new TelegramWorkerPort({ worker, workerId: '2', }) ``` If you want, you can even have custom message handler that will not interfere with mtcute workers: ```ts self.addEventListener('message', message => { if ('_mtcuteWorkerId' in message) return // ignore mtcute workers messages // do whatever }) ``` ::: tip `workerId` does not need to be unique across the entire application, just within the same worker. ::: ## MessagePort and multiple ports `TelegramWorkerPort` can talk not only to `Worker` / `SharedWorker`, but also to a `MessagePort`. This is useful when one worker owns the mtcute client, and another thread gets a dedicated port into it. ```ts // main.ts import { MessageChannel } from 'worker_threads' import { Worker } from 'worker_threads' const { port1, port2 } = new MessageChannel() const worker = new Worker(new URL('./worker.js', import.meta.url)) // assuming the same code as above const helper = new Worker(new URL('./helper.js', import.meta.url)) worker.on('message', (message) => channel.port1.postMessage(message.payload)) channel.port1.on('message', (message) => worker.postMessage(message)) helper.postMessage({ type: 'init', port: channel.port2, }, [channel.port2]) ``` You can also create multiple `TelegramWorkerPort` instances for the same underlying worker: ```ts const worker = new Worker( new URL('./worker.js', import.meta.url), ) const port1 = new TelegramWorkerPort({ worker }) const port2 = new TelegramWorkerPort({ worker }) ``` Each port gets its own logical connection. Requests, aborts and keepalives are isolated per port, while the underlying mtcute client inside the worker is shared. This means: * disconnecting or releasing one port does not affect the other ports * one port can have pending RPC calls without interfering with another * worker-owned keepalives are cleaned up per port when that port is released If you use `SharedWorker`, this is the normal model: each page gets its own port, all backed by the same worker client. ### Cleanup policies Worker-side cleanup when the last port disappears is controlled by `onLastDisconnected`: * `'nothing'` (default): keep the client alive * `'disconnect'`: disconnect the client, but keep the worker process alive * `'destroy'`: destroy the client ```ts new TelegramWorker({ client: tg, onLastDisconnected: 'disconnect', }).mount() ``` If you need to destroy the shared client regardless of other ports, use `unsafeForceDestroy()`. Use `destroy()` only when you want to release the current port. ## Other runtimes In other runtimes it may also make sense to use workers. If your runtime supports web workers, you can use the `@mtcute/web` package to create workers - it should work just fine. Otherwise, Please refer to [Web](https://github.com/mtcute/mtcute/blob/master/packages/web/src/worker.ts)/[Node.js](https://github.com/mtcute/mtcute/blob/master/packages/node/src/worker.ts) for the platform-specific worker implementations, and use them as a reference to create your own worker implementation.