chore: moved docs inside the main repo
Some checks failed
Tests / e2e (push) Blocked by required conditions
Tests / e2e-deno (push) Blocked by required conditions
Tests / test-deno (push) Successful in 1m53s
Tests / test-bun (push) Successful in 2m1s
Tests / test-node (node22) (push) Successful in 2m8s
Tests / test-node (node20) (push) Successful in 2m10s
Tests / test-node (node18) (push) Successful in 2m16s
Tests / test-web (chromium) (push) Successful in 2m18s
Tests / test-web (firefox) (push) Successful in 2m31s
Tests / lint (push) Has been cancelled
Build and deploy typedoc / build (push) Has been cancelled

Co-authored-by: Kamilla 'ova <me@kamillaova.dev>
Co-authored-by: Alina Chebakova <chebakov05@gmail.com>
Co-authored-by: Kravets <57632712+kravetsone@users.noreply.github.com>
Co-authored-by: starkow <hello@starkow.dev>
Co-authored-by: sireneva <150665887+sireneva@users.noreply.github.com>
This commit is contained in:
alina 🌸 2025-01-17 08:40:57 +03:00
parent 9f3ef993c0
commit dc08d93d2b
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
53 changed files with 7298 additions and 28 deletions

View file

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.json",
"exclude": [
"../**/*.test.ts",
"../**/*.test-utils.ts",
"../**/__fixtures__/**"
]
}

View file

@ -1,37 +1,38 @@
name: Docs
name: Build and deploy docs
on:
push:
branches:
- master
pull_request:
branches: [ master ]
branches: [master]
paths:
- 'docs/**'
workflow_dispatch:
concurrency:
group: pages
group: docs
cancel-in-progress: false
jobs:
build:
runs-on: node22
runs-on: node20
steps:
- uses: actions/checkout@v4
- uses: ./.forgejo/actions/init
- name: Build docs
- uses: pnpm/action-setup@v2
- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: docs
- name: Build with VitePress
working-directory: docs
run: |
pnpm run docs
touch docs/.nojekyll
echo "ref.mtcute.dev" > docs/CNAME
pnpm run build
touch .vitepress/dist/.nojekyll
echo mtcute.dev > .vitepress/dist/CNAME
echo "ignore-workspace-root-check=true" >> .npmrc
curl -O https://gist.githubusercontent.com/j0nl1/7f9b5210c9e6ecbabe322baa16dcb5db/raw/760de77327bf83671cfb6bd4e64181299ba26113/typedoc-fix-cf.mjs
node typedoc-fix-cf.mjs docs
- name: Deploy
# do not run on forks and releases
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.actor == 'desu-bot'
# do not run on forks
if: github.repository == 'teidesu/mtcute'
uses: https://github.com/cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy docs --project-name=mtcute-apiref
command: pages deploy docs/.vitepress/dist --project-name=mtcute-docs

View file

@ -0,0 +1,34 @@
name: Build and deploy typedoc
on:
push:
branches:
- master
pull_request:
branches: [master]
workflow_dispatch:
concurrency:
group: typedoc
cancel-in-progress: false
jobs:
build:
runs-on: node22
steps:
- uses: actions/checkout@v4
- uses: ./.forgejo/actions/init
- name: Build docs
run: |
pnpm run docs
touch dist/typedoc/.nojekyll
echo "ref.mtcute.dev" > dist/typedoc/CNAME
echo "ignore-workspace-root-check=true" >> .npmrc
- name: Deploy
# only run on releases
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.actor == 'desu-bot'
uses: https://github.com/cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/typedoc --project-name=mtcute-apiref

3
.gitignore vendored
View file

@ -7,9 +7,6 @@ private/
.vscode
*.log
# docs are generated in ci
docs
coverage
.rollup.cache
*.tsbuildinfo

View file

@ -69,6 +69,7 @@ export default {
},
},
typedoc: {
out: 'dist/typedoc',
excludePackages: [
'@mtcute/tl',
'@mtcute/create-bot',

16
docs/.editorconfig Normal file
View file

@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4
trim_trailing_whitespace = true
[*.md]
indent_size = 2
max_line_length = 0
trim_trailing_whitespace = false

13
docs/.gitignore vendored Executable file
View file

@ -0,0 +1,13 @@
pids
logs
node_modules
npm-debug.log
coverage/
run
dist
cache
.DS_Store
.nyc_output
.basement
config.local.js
basement_dist

View file

@ -0,0 +1,13 @@
<script setup lang="ts">
import { useData } from 'vitepress'
const { isDark } = useData()
const { link, height } = defineProps<{
link: string
height?: number
}>()
</script>
<template>
<iframe :src="`https://t.me/${link}?embed=1&color=d990c3&dark=${+isDark}`" :height="height"></iframe>
</template>

View file

@ -0,0 +1,24 @@
<template>
<span class="tag">
{{ text }}
</span>
</template>
<script>
export default {
name: 'Tag',
props: ['text'],
}
</script>
<style>
.tag {
font-size: 0.4em;
font-weight: 400;
border: 1px solid var(--vp-c-default-3);
padding: 2px 4px;
border-radius: 6px;
color: var(--vp-c-brand);
vertical-align: middle;
}
</style>

View file

@ -0,0 +1,45 @@
<template>
<p class="img-wrap">
<img
:src="src"
:alt="alt || caption"
:style="{'max-width': width ? width + 'px' : undefined}"
:class="{'img-adaptive': adaptive}"
>
<span v-if="caption">{{ caption || alt }}</span>
</p>
</template>
<script>
import mediumZoom from 'medium-zoom'
export default {
name: 'VImg',
props: ['src', 'alt', 'caption', 'width', 'adaptive'],
mounted() {
mediumZoom(this.$el.querySelector('img'), {
margin: 24,
background: 'var(--vp-backdrop-bg-color)',
})
},
}
</script>
<style>
.img-wrap {
display: flex;
flex-direction: column;
align-items: center;
margin: 24px 0;
}
.img-wrap span {
margin-top: 4px;
font-size: 14px;
color: var(--vp-c-text-2);
}
.dark .img-adaptive {
filter: invert(1);
}
</style>

132
docs/.vitepress/config.mts Normal file
View file

@ -0,0 +1,132 @@
import { defineConfig, HeadConfig } from "vitepress";
import markdownItFootnotes from "markdown-it-footnote";
// https://vitepress.dev/reference/site-config
export default ({ mode }) => defineConfig({
title: "mtcute",
description: "mtcute documentation",
lastUpdated: true,
head: [
["meta", { name: "theme-color", content: "#e9a1d9" }],
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
[
"meta",
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
],
['link', { rel: 'icon', href: '/mtcute-logo.png' }],
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fredoka:wght@500&text=mtcute' }],
...(mode === 'production' ? [
['script', { async: '', src: 'https://zond.tei.su/script.js', 'data-website-id': '968f50a2-4cf8-4e31-9f40-1abd48ba2086' }] as HeadConfig
] : []),
],
transformHtml(code) {
if (mode !== 'production') return code
// this is a hack but whatever
return code.replace(
'<body>',
'<body><noscript><div><img src="https://tei.su/zond.php?website=968f50a2-4cf8-4e31-9f40-1abd48ba2086" style="position:absolute; left:-9999px;" alt="" /></div></noscript>'
)
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Guide", link: "/guide/" },
{ text: "Reference", link: "//ref.mtcute.dev" },
],
socialLinks: [
{ icon: "github", link: "https://github.com/mtcute" },
{
icon: {
svg: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Telegram</title><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>`,
},
link: "https://t.me/mt_cute",
},
],
search: {
provider: "local",
},
editLink: {
pattern: "https://github.com/mtcute/docs/edit/master/:path",
},
outline: {
level: "deep",
},
sidebar: {
"/guide/": [
{
text: "Getting started",
items: [
{ text: "Quick start", link: "/guide/" },
{ text: "Signing in", link: "/guide/intro/sign-in" },
{ text: "Updates", link: "/guide/intro/updates" },
{ text: "Errors", link: "/guide/intro/errors" },
{
text: "MTProto vs Bot API",
link: "/guide/intro/mtproto-vs-bot-api",
},
{ text: "FAQ", link: "/guide/intro/faq" },
],
},
{
text: "Topics",
items: [
{ text: "Peers", link: "/guide/topics/peers" },
{ text: "Storage", link: "/guide/topics/storage" },
{ text: "Transport", link: "/guide/topics/transport" },
{ text: "Parse modes", link: "/guide/topics/parse-modes" },
{ text: "Files", link: "/guide/topics/files" },
{ text: "Keyboards", link: "/guide/topics/keyboards" },
{ text: "Inline mode", link: "/guide/topics/inline-mode" },
{ text: "Conversation", link: "/guide/topics/conversation" },
{ text: "Raw API", link: "/guide/topics/raw-api" },
],
},
{
text: "Dispatcher",
items: [
{ text: "Intro", link: "/guide/dispatcher/intro" },
{ text: "Handlers", link: "/guide/dispatcher/handlers" },
{ text: "Filters", link: "/guide/dispatcher/filters" },
{
text: "Groups & Propagation",
link: "/guide/dispatcher/groups-propagation",
},
{ text: "Errors", link: "/guide/dispatcher/errors" },
{ text: "Middlewares", link: "/guide/dispatcher/middlewares" },
{ text: "Inline mode", link: "/guide/dispatcher/inline-mode" },
{ text: "State", link: "/guide/dispatcher/state" },
{ text: "Rate limit", link: "/guide/dispatcher/rate-limit" },
{ text: "Child Dispatchers", link: "/guide/dispatcher/children" },
{ text: "Scenes", link: "/guide/dispatcher/scenes" },
{ text: "Dependency Injection", link: "/guide/dispatcher/di" },
],
},
{
text: 'Advanced',
items: [
{ text: "Tree-shaking", link: "/guide/advanced/treeshaking" },
{ text: "Workers", link: "/guide/advanced/workers" },
{ text: "Converting sessions", link: "/guide/advanced/session-convert" },
{ text: "Network middlewares", link: "/guide/advanced/net-middlewares" },
]
}
],
},
footer: {
message: "mtcute is not affiliated with Telegram.",
copyright:
'This documentation is licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a><br/>' +
'Logo by <a href="//t.me/AboutTheDot">@dotvhs</a><br/>' +
'© Copyright 2021-present, <a href="//github.com/teidesu">teidesu</a> ❤️',
},
},
markdown: {
config: (md) => {
md.use(markdownItFootnotes);
},
},
});

View file

@ -0,0 +1,70 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { nextTick, provide } from 'vue'
const { isDark } = useData()
const enableTransitions = () =>
'startViewTransition' in document &&
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
if (!enableTransitions()) {
isDark.value = !isDark.value
return
}
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)}px at ${x}px ${y}px)`
]
await document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
}).ready
document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
}
)
})
</script>
<template>
<DefaultTheme.Layout />
</template>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}
.VPSwitchAppearance {
width: 22px !important;
}
.VPSwitchAppearance .check {
transform: none !important;
}
</style>

View file

@ -0,0 +1,30 @@
// https://vitepress.dev/guide/custom-theme
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import vitepressBackToTop from 'vitepress-plugin-back-to-top'
import 'vitepress-plugin-back-to-top/dist/style.css'
// @ts-ignore
import EmbedPost from '../components/EmbedPost.vue'
// @ts-ignore
import VImg from '../components/VImg.vue'
// @ts-ignore
import Tag from '../components/Tag.vue'
// @ts-ignore
import Layout from './Layout.vue'
import './style.css'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ app, router, siteData }) {
app.component('v-img', VImg)
app.component('EmbedPost', EmbedPost)
app.component('Tag', Tag)
vitepressBackToTop({
threshold: 300
})
}
} satisfies Theme

View file

@ -0,0 +1,192 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attched to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: #b975a5;
--vp-c-brand-2: #d990c3;
--vp-c-brand-3: #f69ddc;
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
45deg,
#f69ddc 60%,
#c890d6
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#c890d6 50%,
#f69ddc 50%
);
--vp-home-hero-image-filter: blur(44px) opacity(0.2);
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
.VPFooter a {
color: var(--vp-c-brand-1);
}
.content-container iframe {
border: 0;
width: 100%;
}
.custom-block.tip code {
color: var(--vp-c-brand-1)
}
.vp-doc a,
.custom-block.tip a {
color: var(--vp-c-brand-2);
text-decoration: none;
}
.vp-doc a:hover,
.custom-block.tip a:hover {
text-decoration: underline;
}
.vp-doc a:active,
.custom-block.tip a:active {
color: var(--vp-c-brand-1);
}
.vp-doc code {
word-break: keep-all;
white-space: nowrap;
}
.vp-doc table {
display: table;
}
:root {
scrollbar-gutter: stable;
}
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 999;
}
.main .name .clip {
font-family: Fredoka, sans-serif;
font-weight: 500;
}
.dark .go-to-top {
background-color: var(--vp-c-bg-elv);
}
.dark .go-to-top:hover {
background-color: var(--vp-c-bg-alt);
}
.index-footnote {
display: block;
text-align: center;
margin-top: 32px;
}

11
docs/README.md Normal file
View file

@ -0,0 +1,11 @@
# 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
```

View file

@ -0,0 +1,154 @@
# Network middlewares <Tag text="v0.16.0+" />
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)
:::

View file

@ -0,0 +1,118 @@
# 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("..."))
```
## [MTKruto](https://github.com/MTKruto/MTKruto)
```ts
import { convertFromMtkrutoSession } from '@mtcute/convert'
const client = new TelegramClient({ ... })
await client.importSession(convertFromMtkrutoSession("..."))
```
## 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]
```
If you don't know such information at all, you can just always use the DC 2 (as above), and mtcute will handle the rest
### 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: [],
}
})
```

View file

@ -0,0 +1,34 @@
# 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.

View file

@ -0,0 +1,109 @@
# 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,
})
```
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,
})
```
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!')
```
## 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.

View file

@ -0,0 +1,81 @@
# 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.

View file

@ -0,0 +1,43 @@
# 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.
:::

142
docs/guide/dispatcher/errors.md Executable file
View file

@ -0,0 +1,142 @@
# 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((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 = new Dispatcher(tg)
const dp1 = new Dispatcher()
const dp2 = new Dispatcher()
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 = new Dispatcher(tg)
const dp1 = new Dispatcher()
const dp2 = new Dispatcher()
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
```

331
docs/guide/dispatcher/filters.md Executable file
View file

@ -0,0 +1,331 @@
# 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.html)
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<MessageMedia, Photo>
}
)
```
### 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<Message> =>
(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<Message, { sender: Chat }> =
(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<Message, { senderDb: UserModel }> =
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)
}
)
```
:::

View file

@ -0,0 +1,191 @@
# 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.StopChidlren` 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.StopChidlren
}
)
const dp1 = new Dispatcher()
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',
() => { ... }
)
```

402
docs/guide/dispatcher/handlers.md Executable file
View file

@ -0,0 +1,402 @@
# 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.html)
::: 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.')
})
```
<small>* Telegram might decide not to send these updates
in case this message is old enough.</small>
## 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.
<small>* Telegram might decide not to send these updates
in case this message is old enough.</small>
## 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}`)
})
```
<small>* 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.</small>
## 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}`)
})
```
<small>* 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.</small>
## 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({
type: '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')
```

View file

@ -0,0 +1,731 @@
# 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)
<v-img
src="https://core.telegram.org/file/811140592/2/P4-tFhmBsCg/57418af08f1a252d45"
width="320"
caption="Example of an inline bot"
/>
## 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:
<v-img
src="https://i.gyazo.com/cb212ab91102bf3dd1c7306c943dee37.png"
caption="Article result example"
/>
```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}}
<a href="{{url}}"><b>{{title}}</b></a>
{{else}}
<b>{{title}}</b>
{{/if}}
{{#if description}}
{{description}}
{{/if}}
```
For the above example, this would result in the following message:
<v-img src="https://i.gyazo.com/c97de58996a6e38c40623e5f64c44d9b.png" />
### GIF
You can send an animated GIF (either real GIF, or an MP4 without sound)
as a result.
<v-img
src="https://i.gyazo.com/5ecfac354b2068cdb7f3dd9b4f90d5ef.png"
caption="GIF result example"
/>
```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)
<v-img
src="https://i.gyazo.com/13ba734bf42d5762b1b4b00543653bcf.jpg"
width="480"
caption="GIF result with title"
/>
```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.
<v-img
src="https://i.gyazo.com/3b328516687c2719d828d051211e8d5d.png"
caption="Video result example"
/>
```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`:
<v-img
src="https://i.gyazo.com/9281ab09bfde7ee013dc0c75fd1f939f.png"
caption="Video page result example"
/>
```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:
<v-img src="https://i.gyazo.com/818b6651333ee2a7e5dd13ebcfc9febc.png" />
### 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.
<v-img
src="https://i.gyazo.com/a044dcbca5d2b32b6885aa8cc5565aab.png"
caption="Audio result example"
/>
```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.
<v-img
src="https://i.gyazo.com/b99d48dc8ce2b8f6bd3bd8a645eedc9b.png"
caption="Voice result example"
/>
```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.
<v-img
src="https://i.gyazo.com/c7fafa82712981248212663e13024fa1.png"
caption="Photo result example"
/>
```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)
<v-img
src="https://i.gyazo.com/47e35f56ceb8685e745a5f17a580b7c8.jpg"
width="480"
caption="Photo result with title"
/>
```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.
<v-img
src="https://i.gyazo.com/54327f78ab6eb996fd534254678f96d6.png"
caption="Sticker result example"
/>
```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.
<v-img
src="https://i.gyazo.com/856a21545de951bfa1fe063af2db7b80.png"
caption="File result example"
/>
```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.
<v-img
src="https://i.gyazo.com/95d87196080108e090b11904e98e4818.png"
caption="Geo result example"
/>
```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.
<v-img
src="https://i.gyazo.com/57e67771353dc895fadca3a5029181dd.png"
caption="Venue result example"
/>
```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.
<v-img
src="https://i.gyazo.com/eb4ac5feaa7717ea7c00fe0dbc562e8f.png"
caption="Contact result example"
/>
```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.
<v-img
src="https://i.gyazo.com/dfc4b6702db4f586c92ac243b5d28bcb.png"
caption="Game result example"
/>
```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:
<v-img
src="https://i.gyazo.com/3cbf356545dbd696d128c060389ab0f5.png"
caption="Switch to PM example"
/>
```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))
})
})
```

20
docs/guide/dispatcher/intro.md Executable file
View file

@ -0,0 +1,20 @@
# 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)
```

View file

@ -0,0 +1,67 @@
# 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<TimerContext>((upd) => {
upd.start = Date.now()
})
dp.onPostUpdate<TimerContext>((handled, upd) => {
if (handled) {
console.log(`handled ${upd.name} in ${Date.now() - upd.start} ms`)
}
})
dp.onError<TimerContext>((err, upd) => {
console.log(`error for ${upd.name} after ${Date.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)).

View file

@ -0,0 +1,61 @@
# 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()}`)
```

245
docs/guide/dispatcher/scenes.md Executable file
View file

@ -0,0 +1,245 @@
# 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.
<!-- Full example: TODO LINK -->
<p><small>* Only updates that can be <a href="./state#keying">keyed</a> are supported</small></p>
## Creating a scene
A scene is created by using `Dispatcher.scene`:
```ts
interface SceneState { ... }
const dp = Dispatcher.scene<SceneState>('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 = new Dispatcher()
```
::: 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<BotState>(msg)
const global = await globalState.get()
})
```
Alternatively, you can disable isolated storage for FSM altogether and use
global state directly:
```ts
const dp = new Dispatcher<BotState>()
// 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 `WizardAction.Stay` in the same step, proceed to the
`WizardAction.Next` step, or `WizardAction.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 (full example TODO LINK): -->
A simple example:
```ts
interface RegForm {
name?: string
}
const wizard = new WizardScene<RegForm>('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
}
})
```

271
docs/guide/dispatcher/state.md Executable file
View file

@ -0,0 +1,271 @@
# 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.
<!-- Full code example with FSM: TODO LINK -->
::: 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<BotState>(tg, {
storage: new MemoryStateStorage()
})
// or, for children
const dp = Dispatcher.child<BotState>()
```
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<ActionEnterPassword>) => {
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 = new Dispatcher<BotState>(tg, storage, customKey)
// or, locally for a child dispatcher:
const dp = new Dispatcher<BotState>(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<UserPref>(`$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<SomeInternalState>(...)
```
:::
## 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.html).
```ts
const dp = Dispatcher.for<BotState>(tg, { storage: new MemoryStorage() })
// or, locally for a child dispatcher:
const dp = Dispatcher.child<BotState>({ storage: new MemoryStorage() })
```
### SQLite storage
You can re-use your existing SQLite storage for FSM:
```ts
import { SqliteStorage } from '@mtcute/sqlite'
import { SqliteStateStorage } from '@mtcute/dispatcher'
const storage = new SqliteStorage('my-account')
const tg = new TelegramClient({ ..., storage })
const dp = Dispatcher.for<BotState>(tg, {
storage: SqliteStateStorage.from(storage)
})
```
Alternatively, you can create a new SQLite storage specifically for FSM:
```ts
import { SqliteStorageDriver } from '@mtcute/sqlite'
import { SqliteStateStorage } from '@mtcute/dispatcher'
const dp = Dispatcher.for<BotState>(tg, {
storage: new SqliteStateStorage(new SqliteStorageDriver('my-state'))
})
```

170
docs/guide/index.md Executable file
View file

@ -0,0 +1,170 @@
# 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
[https://my.telegram.org/apps](https://my.telegram.org/apps).
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 <b>MTCute</b>!`)
```
4. That's literally it! Happy hacking 🚀
### Native crypto addon
mtcute also provides `@mtcute/crypto-node` package, that implements
a native Node.js addon for crypto functions used in MTProto.
Using this addon improves overall library performance (especially when uploading/downloading files),
so it is advised that you install it as well:
```bash
pnpm add @mtcute/crypto-node
```
When using `@mtcute/node`, native addon is loaded automatically,
no extra steps are required.
## Bun
Experimental 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
Experimental 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.
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'
import { setPlatform } from '@mtcute/core/platform.js'
setPlatform(new MyPlatform())
const tg = new TelegramClient({
...,
storage: new MyStorage(),
crypto: new MyCrypto()
transport: () => new MyTransport(),
})
```
::: info
You only need to call `setPlatform` once, before creating any clients.
Platform is set once globally and cannot be changed afterwards.
It is safe to call `setPlatform` multiple times, as long as the constructor is the same - it will be ignored if the platform is already set.
A good starting point might be to use [WebPlatform](https://ref.mtcute.dev/classes/_mtcute_web.WebPlatform.html),
since it implements everything in portable JavaScript.
:::

148
docs/guide/intro/errors.md Executable file
View file

@ -0,0 +1,148 @@
# Handling errors
> There are two ways to write error-free programs; only the third one works
>
> &copy; 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 `floodWaitThreshold`
by sleeping for that amount of seconds.
:::
### Unknown errors
Sometimes, Telegram with return an error which is not documented (yet).
In this case, it will still be an `RpcError`, but will have `.unknown = true`
If you are feeling generous and want to help improve the docs for everyone,
you can opt into sending unknown errors to [danog](https://github.com/danog)'s
[error reporting service](https://rpc.pwrtelegram.xyz/).
This is fully anonymous (except maybe IP) and is only used to improve the library
and developer experience for everyone working with MTProto.
To enable, pass `enableErrorReporting: true` to the client options:
```ts
const tg = new TelegramClient({
...
enableErrorReporting: true
})
```
### 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((err, conn) => {
if (conn) {
// `err` is the error
// `conn` is the connection where the error happened
console.log(err, conn)
}
// `err` is not a connection-related error
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
:::
## 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 = new Dispatcher()
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` (`conn` will be `undefined`).

230
docs/guide/intro/faq.md Executable file
View file

@ -0,0 +1,230 @@
# 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):
<!-- TODO link to repl -->
```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.html#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.html#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.html#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).
Anya from [@theyforcedme](https://t.me/theyforcedme) has shared her thoughts on this
in Pyrogram chat:
<EmbedPost link="PyrogramChat/69424" height="331px" />
## 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:
[recover@telegram.org](mailto:recover@telegram.org).
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()
]
}
})

View file

@ -0,0 +1,74 @@
# 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:
<v-img
src="/assets/mtproto_vs_botapi.svg"
adaptive="true"
/>
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.<br/>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.

222
docs/guide/intro/sign-in.md Executable file
View file

@ -0,0 +1,222 @@
# 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 [https://my.telegram.org/apps](https://my.telegram.org/apps)
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 `readline` module in Node.js,
that will ask you for input in the console.
It's not available in `@mtcute/core`, since it is platform-agnostic
:::
## 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}`)
```
## 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 ts-node your-file.ts
```
## 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 they do provide some flexibility and convenience, they are not always
suitable for every use case. For example, when building a web application,
you might want to use a different way to ask for user input.
In that case, you can use the underlying sign-in methods directly
<!-- TODO link to full example -->
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.html),
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.html#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.

171
docs/guide/intro/updates.md Executable file
View file

@ -0,0 +1,171 @@
# 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.index.TelegramClient.html#on)):
```ts
tg.on('new_message', (msg) => {
console.log(msg.text)
})
// You can also handle any supported update at once:
tg.on('update', (upd) => {
if (upd.name === 'new_message') {
console.log(upd.data.text)
}
})
// As well as raw MTProto updates:
tg.on('raw_update', (upd, users, chats) => {
console.log(upd._)
})
```
::: tip
Client events are based on EventEmitter. It expects handlers to be synchronous,
so if you want to do something async, make sure to also handle the errors:
```ts
tg.on('new_message', 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 = new Dispatcher(tg)
dp.onNewMessage(async (msg) => {
await msg.forwardTo('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('me')
}
)
```
Filters can do a lot more than that, and
we will cover them further [later](../dispatcher/filters.html).

126
docs/guide/topics/conversation.md Executable file
View file

@ -0,0 +1,126 @@
# 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.
<!-- This is particularly useful when programmatically interacting with bots,
as you can see in this example TODO LINK -->
::: 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 ...
} catch (e) {}
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.

261
docs/guide/topics/files.md Executable file
View file

@ -0,0 +1,261 @@
# 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.on('new_message', 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.html):
```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.html#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.on('new_message', 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.on('new_message', 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))
```

View file

@ -0,0 +1,55 @@
# 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.on('inline_query', 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)
```

301
docs/guide/topics/keyboards.md Executable file
View file

@ -0,0 +1,301 @@
# 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.
<v-img
src="https://core.telegram.org/file/811140184/1/5YJxx-rostA/ad3f74094485fb97bd"
width="280"
caption="Example of a reply keyboard"
/>
```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.
<v-img
src="https://core.telegram.org/file/811140217/1/NkRCCLeQZVc/17a804837802700ea4"
width="280"
caption="Example of an inline keyboard"
/>
```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.html) |
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.
<!-- Full example TODO LINK -->
### 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.

107
docs/guide/topics/parse-modes.md Executable file
View file

@ -0,0 +1,107 @@
# 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.html)
to learn about the syntax.
## HTML
HTML parser is implemented in `@mtcute/html-parser` package:
```ts
import { html } from '@mtcute/html-parser'
dp.onNewMessage(async (msg) => {
await msg.answerText(html`Hello, <b>${msg.sender.username}</b>`)
})
```
**Note**: the syntax used by this parser is **not**
compatible with Bot API's HTML syntax.
See [documentation](https://ref.mtcute.dev/modules/_mtcute_html_parser.html)
to learn 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`<i>${user.displayName}</i>`
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('Hi, <b>User</b>!', { parseMode: 'html' })
console.log(msg.text)
// Hi, User!
console.log(html.unparse())
// Hi, <b>User</b>!
```

258
docs/guide/topics/peers.md Executable file
View file

@ -0,0 +1,258 @@
# 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/class/chat)** is a legacy 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/class/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/class/channel) where `.broadcast === true`
- A **supergroup** (also referred to as megagroup) is a
[Channel](https://core.telegram.org/class/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/class/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*.
Chats are still used *(probably?)* because they are enough for most people's
needs, and are also lighter on server resources. However, chats 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.html) type.
It is used to represent anything where there can be messages,
including users, legacy groups, supergroups, channels, etc.
It is mostly inspired by [Bot API's Chat](https://core.telegram.org/bots/api#chat)
type, and works in a very similar way.
## Fetching peers
Often, you'll need to interact with a peer later, when the respective
`User` or `Chat` 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<tl.TypeInputPeer>
```
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.html),
[getMarkedPeerId](https://ref.mtcute.dev/functions/_mtcute_core.index.getMarkedPeerId.html),
:::
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.html#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
}
)
```

198
docs/guide/topics/raw-api.md Executable file
View file

@ -0,0 +1,198 @@
# 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).
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 `to*` functions
respectively:
```ts
const result = await tg.call({
_: 'channels.reportSpam',
channel: toInputChannel(await tg.resolvePeer(...)),
userId: toInputUser(await tg.resolvePeer(...)),
id: [1, 2, 3]
})
```
These functions will throw in case the peer is of wrong type
## 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 simplify processing message entities, client has a special method:
```ts
_parseEntities(
text?: string | FormattedString,
mode?: string | null,
entities?: tl.TypeMessageEntity[]
): Promise<[string, tl.TypeMessageEntity[] | undefined]>
```
Here, `text` is user-provided text which may have formatted entities,
`mode` is the chosen parse mode (or `null` to disable), and `entities`
is the override message entities provided by the user.
If `FormattedString` is passed (e.g. <code>md\`\*\*Hello!**\`</code>),
`mode` parameter is ignored.
It returns a tuple containing text without any entities, and the entities
themselves (if applicable). If `text` was not provided, empty string
is returned.
## 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.customRequest` pseudo-method for that:
```ts
const res = await tg.call({
_: 'mtcute.customRequest',
bytes: Buffer.from('11223344', 'hex'),
})
```
`bytes` will be send 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.html))

174
docs/guide/topics/storage.md Executable file
View file

@ -0,0 +1,174 @@
# 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 Node.js 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 it in a separate package, `@mtcute/sqlite`, and internally
uses [better-sqlite3](https://www.npmjs.com/package/better-sqlite3)
```ts{4}
import { SqliteStorage } from '@mtcute/sqlite'
const tg = new TelegramClient({
storage: new SqliteStorage('my-account.session')
})
```
::: tip
If you are using `@mtcute/node`, 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, `@mtcute/sqlite` by default uses
WAL mode ([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.
## 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')
})
```
::: tip
In the browser, 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 **407** characters
:::
## Implementing custom storage
The easiest way to implement a custom storage would be to make a subclass of `MemoryStorage`,
or check the [source code of SqliteStorage](https://github.com/mtcute/mtcute/blob/master/packages/sqlite/src/index.ts)
and implement something similar with your DB of choice.
### 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.html), used by `BaseTelegramClient` for low-level
MTProto data storage
- [ITelegramStorageProvider](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ITelegramStorageProvider.html), 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.html), used by `Dispatcher` for FSM and Scenes storage

137
docs/guide/topics/transport.md Executable file
View file

@ -0,0 +1,137 @@
# 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 `@mtcute/http-proxy` (Node.js only):
```bash
pnpm add @mtcute/http-proxy
```
```ts{5-8}
import { HttpProxyTcpTransport } from '@mtcute/http-proxy'
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
`SocksTcpTransport`, which is provided
by `@mtcute/socks-proxy` (Node.js only):
```bash
pnpm add @mtcute/socks-proxy
```
```ts{5-8}
import { SocksTcpTransport } from '@mtcute/socks-proxy'
const tg = new TelegramClient({
// ...
transport: () => new SocksTcpTransport({
host: '127.0.0.1',
port: 8080
})
})
```
## MTProxy transport
To access Telegram via MTProxy (MTProto proxy), you can use
`MtProxyTcpTransport`, which is provided by `@mtcute/mtproxy` (Node.js only):
```bash
pnpm add @mtcute/mtproxy
```
```ts{5-8}
import { MtProxyTcpTransport } from '@mtcute/mtproxy'
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.changeTransport(() => new MtProxyTcpTransport({...}))
```
## 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.

41
docs/index.md Normal file
View file

@ -0,0 +1,41 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "mtcute"
tagline: Modern TypeScript library for MTProto
image:
src: /mtcute-logo.png
actions:
- theme: brand
text: Quick Start →
link: /guide/
- theme: alt
text: API Reference
link: //ref.mtcute.dev
features:
- icon: 🍰
title: Simple
details: mtcute hides all the complexity and provides a clean and modern API
- icon: ✨
title: Compatible
details: mtcute supports almost everything Bot API does, and even more!
- icon: 🍡
title: Lightweight
details: Running instance uses less than 50 MB of RAM*.
- icon: 🛡️
title: Type-safe
details: Most of the APIs (including MTProto) are strictly typed to help your workflow
- icon: ⚙️
title: Hackable
details: Almost every aspect of the library is customizable, including networking and storage
- icon: 🕙
title: Up-to-date
details: mtcute uses the latest TL schema to provide the newest features as soon as possible
---
<small class="index-footnote">
* Tested on a personal account and a few small-load bots
</small>

20
docs/package.json Executable file
View file

@ -0,0 +1,20 @@
{
"name": "@mtcute/docs",
"version": "1.0.0",
"description": "mtcute documentation",
"devDependencies": {
"vitepress": "1.0.0-rc.24",
"vue": "^3.3.7"
},
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview"
},
"dependencies": {
"markdown-it-footnote": "^3.0.3",
"medium-zoom": "^1.0.8",
"vitepress-plugin-back-to-top": "^1.0.1"
},
"packageManager": "pnpm@9.5.0"
}

995
docs/pnpm-lock.yaml Normal file
View file

@ -0,0 +1,995 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
markdown-it-footnote:
specifier: ^3.0.3
version: 3.0.3
medium-zoom:
specifier: ^1.0.8
version: 1.0.8
vitepress-plugin-back-to-top:
specifier: ^1.0.1
version: 1.0.1
devDependencies:
vitepress:
specifier: 1.0.0-rc.24
version: 1.0.0-rc.24(@algolia/client-search@4.20.0)(search-insights@2.9.0)
vue:
specifier: ^3.3.7
version: 3.3.7
packages:
'@algolia/autocomplete-core@1.9.3':
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
'@algolia/autocomplete-plugin-algolia-insights@1.9.3':
resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==}
peerDependencies:
search-insights: '>= 1 < 3'
'@algolia/autocomplete-preset-algolia@1.9.3':
resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/autocomplete-shared@1.9.3':
resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/cache-browser-local-storage@4.20.0':
resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==}
'@algolia/cache-common@4.20.0':
resolution: {integrity: sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ==}
'@algolia/cache-in-memory@4.20.0':
resolution: {integrity: sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg==}
'@algolia/client-account@4.20.0':
resolution: {integrity: sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q==}
'@algolia/client-analytics@4.20.0':
resolution: {integrity: sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug==}
'@algolia/client-common@4.20.0':
resolution: {integrity: sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ==}
'@algolia/client-personalization@4.20.0':
resolution: {integrity: sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ==}
'@algolia/client-search@4.20.0':
resolution: {integrity: sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg==}
'@algolia/logger-common@4.20.0':
resolution: {integrity: sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ==}
'@algolia/logger-console@4.20.0':
resolution: {integrity: sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA==}
'@algolia/requester-browser-xhr@4.20.0':
resolution: {integrity: sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw==}
'@algolia/requester-common@4.20.0':
resolution: {integrity: sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng==}
'@algolia/requester-node-http@4.20.0':
resolution: {integrity: sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng==}
'@algolia/transporter@4.20.0':
resolution: {integrity: sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg==}
'@babel/helper-string-parser@7.22.5':
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.22.20':
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.23.0':
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.23.0':
resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
engines: {node: '>=6.9.0'}
'@docsearch/css@3.5.2':
resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==}
'@docsearch/js@3.5.2':
resolution: {integrity: sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==}
'@docsearch/react@3.5.2':
resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==}
peerDependencies:
'@types/react': '>= 16.8.0 < 19.0.0'
react: '>= 16.8.0 < 19.0.0'
react-dom: '>= 16.8.0 < 19.0.0'
search-insights: '>= 1 < 3'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
react-dom:
optional: true
search-insights:
optional: true
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
'@types/linkify-it@3.0.4':
resolution: {integrity: sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ==}
'@types/markdown-it@13.0.5':
resolution: {integrity: sha512-QhJP7hkq3FCrFNx0szMNCT/79CXfcEgUIA3jc5GBfeXqoKsk3R8JZm2wRXJ2DiyjbPE4VMFOSDemLFcUTZmHEQ==}
'@types/mdurl@1.0.4':
resolution: {integrity: sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A==}
'@types/web-bluetooth@0.0.18':
resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==}
'@vitejs/plugin-vue@4.3.1':
resolution: {integrity: sha512-tUBEtWcF7wFtII7ayNiLNDTCE1X1afySEo+XNVMNkFXaThENyCowIEX095QqbJZGTgoOcSVDJGlnde2NG4jtbQ==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^4.0.0
vue: ^3.2.25
'@vue/compiler-core@3.3.7':
resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
'@vue/compiler-dom@3.3.7':
resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
'@vue/compiler-sfc@3.3.7':
resolution: {integrity: sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==}
'@vue/compiler-ssr@3.3.7':
resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
'@vue/devtools-api@6.5.1':
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
'@vue/reactivity-transform@3.3.7':
resolution: {integrity: sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==}
'@vue/reactivity@3.3.7':
resolution: {integrity: sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==}
'@vue/runtime-core@3.3.7':
resolution: {integrity: sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==}
'@vue/runtime-dom@3.3.7':
resolution: {integrity: sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==}
'@vue/server-renderer@3.3.7':
resolution: {integrity: sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==}
peerDependencies:
vue: 3.3.7
'@vue/shared@3.3.7':
resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
'@vueuse/core@10.5.0':
resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==}
'@vueuse/integrations@10.5.0':
resolution: {integrity: sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==}
peerDependencies:
async-validator: '*'
axios: '*'
change-case: '*'
drauu: '*'
focus-trap: '*'
fuse.js: '*'
idb-keyval: '*'
jwt-decode: '*'
nprogress: '*'
qrcode: '*'
sortablejs: '*'
universal-cookie: '*'
peerDependenciesMeta:
async-validator:
optional: true
axios:
optional: true
change-case:
optional: true
drauu:
optional: true
focus-trap:
optional: true
fuse.js:
optional: true
idb-keyval:
optional: true
jwt-decode:
optional: true
nprogress:
optional: true
qrcode:
optional: true
sortablejs:
optional: true
universal-cookie:
optional: true
'@vueuse/metadata@10.5.0':
resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==}
'@vueuse/shared@10.5.0':
resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==}
algoliasearch@4.20.0:
resolution: {integrity: sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g==}
ansi-sequence-parser@1.1.1:
resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
hasBin: true
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
focus-trap@7.5.4:
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
magic-string@0.30.5:
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
engines: {node: '>=12'}
mark.js@8.11.1:
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
markdown-it-footnote@3.0.3:
resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==}
medium-zoom@1.0.8:
resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==}
minisearch@6.2.0:
resolution: {integrity: sha512-BECkorDF1TY2rGKt9XHdSeP9TP29yUbrAaCh/C03wpyf1vx3uYcP/+8XlMcpTkgoU0rBVnHMAOaP83Rc9Tm+TQ==}
nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
preact@10.18.1:
resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==}
rollup@3.29.4:
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
search-insights@2.9.0:
resolution: {integrity: sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==}
shiki@0.14.5:
resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==}
source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
vite@4.5.0:
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
vitepress-plugin-back-to-top@1.0.1:
resolution: {integrity: sha512-fQUzhkV6SGEmYcO56fv6Jbz5ifseAZuoi+lPDGg0c8gpMVHgqs/D1GbfX4tbvUAtuFNv4kz88wuQRK024iZ2sQ==}
vitepress@1.0.0-rc.24:
resolution: {integrity: sha512-RpnL8cnOGwiRlBbrYQUm9sYkJbtyOt/wYXk2diTcokY4yvks/5lq9LuSt+MURWB6ZqwpSNHvTmxgaSfLoG0/OA==}
hasBin: true
peerDependencies:
markdown-it-mathjax3: ^4.3.2
postcss: ^8.4.31
peerDependenciesMeta:
markdown-it-mathjax3:
optional: true
postcss:
optional: true
vscode-oniguruma@1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
vscode-textmate@8.0.0:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
vue-demi@0.14.6:
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue@3.3.7:
resolution: {integrity: sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
snapshots:
'@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)':
dependencies:
'@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
- search-insights
'@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)':
dependencies:
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
search-insights: 2.9.0
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
'@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)':
dependencies:
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
'@algolia/client-search': 4.20.0
algoliasearch: 4.20.0
'@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)':
dependencies:
'@algolia/client-search': 4.20.0
algoliasearch: 4.20.0
'@algolia/cache-browser-local-storage@4.20.0':
dependencies:
'@algolia/cache-common': 4.20.0
'@algolia/cache-common@4.20.0': {}
'@algolia/cache-in-memory@4.20.0':
dependencies:
'@algolia/cache-common': 4.20.0
'@algolia/client-account@4.20.0':
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/transporter': 4.20.0
'@algolia/client-analytics@4.20.0':
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
'@algolia/client-common@4.20.0':
dependencies:
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
'@algolia/client-personalization@4.20.0':
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
'@algolia/client-search@4.20.0':
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
'@algolia/logger-common@4.20.0': {}
'@algolia/logger-console@4.20.0':
dependencies:
'@algolia/logger-common': 4.20.0
'@algolia/requester-browser-xhr@4.20.0':
dependencies:
'@algolia/requester-common': 4.20.0
'@algolia/requester-common@4.20.0': {}
'@algolia/requester-node-http@4.20.0':
dependencies:
'@algolia/requester-common': 4.20.0
'@algolia/transporter@4.20.0':
dependencies:
'@algolia/cache-common': 4.20.0
'@algolia/logger-common': 4.20.0
'@algolia/requester-common': 4.20.0
'@babel/helper-string-parser@7.22.5': {}
'@babel/helper-validator-identifier@7.22.20': {}
'@babel/parser@7.23.0':
dependencies:
'@babel/types': 7.23.0
'@babel/types@7.23.0':
dependencies:
'@babel/helper-string-parser': 7.22.5
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
'@docsearch/css@3.5.2': {}
'@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)':
dependencies:
'@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)
preact: 10.18.1
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/react'
- react
- react-dom
- search-insights
'@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)':
dependencies:
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)
'@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
'@docsearch/css': 3.5.2
algoliasearch: 4.20.0
search-insights: 2.9.0
transitivePeerDependencies:
- '@algolia/client-search'
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@jridgewell/sourcemap-codec@1.4.15': {}
'@types/linkify-it@3.0.4': {}
'@types/markdown-it@13.0.5':
dependencies:
'@types/linkify-it': 3.0.4
'@types/mdurl': 1.0.4
'@types/mdurl@1.0.4': {}
'@types/web-bluetooth@0.0.18': {}
'@vitejs/plugin-vue@4.3.1(vite@4.5.0)(vue@3.3.7)':
dependencies:
vite: 4.5.0
vue: 3.3.7
'@vue/compiler-core@3.3.7':
dependencies:
'@babel/parser': 7.23.0
'@vue/shared': 3.3.7
estree-walker: 2.0.2
source-map-js: 1.0.2
'@vue/compiler-dom@3.3.7':
dependencies:
'@vue/compiler-core': 3.3.7
'@vue/shared': 3.3.7
'@vue/compiler-sfc@3.3.7':
dependencies:
'@babel/parser': 7.23.0
'@vue/compiler-core': 3.3.7
'@vue/compiler-dom': 3.3.7
'@vue/compiler-ssr': 3.3.7
'@vue/reactivity-transform': 3.3.7
'@vue/shared': 3.3.7
estree-walker: 2.0.2
magic-string: 0.30.5
postcss: 8.4.31
source-map-js: 1.0.2
'@vue/compiler-ssr@3.3.7':
dependencies:
'@vue/compiler-dom': 3.3.7
'@vue/shared': 3.3.7
'@vue/devtools-api@6.5.1': {}
'@vue/reactivity-transform@3.3.7':
dependencies:
'@babel/parser': 7.23.0
'@vue/compiler-core': 3.3.7
'@vue/shared': 3.3.7
estree-walker: 2.0.2
magic-string: 0.30.5
'@vue/reactivity@3.3.7':
dependencies:
'@vue/shared': 3.3.7
'@vue/runtime-core@3.3.7':
dependencies:
'@vue/reactivity': 3.3.7
'@vue/shared': 3.3.7
'@vue/runtime-dom@3.3.7':
dependencies:
'@vue/runtime-core': 3.3.7
'@vue/shared': 3.3.7
csstype: 3.1.2
'@vue/server-renderer@3.3.7(vue@3.3.7)':
dependencies:
'@vue/compiler-ssr': 3.3.7
'@vue/shared': 3.3.7
vue: 3.3.7
'@vue/shared@3.3.7': {}
'@vueuse/core@10.5.0(vue@3.3.7)':
dependencies:
'@types/web-bluetooth': 0.0.18
'@vueuse/metadata': 10.5.0
'@vueuse/shared': 10.5.0(vue@3.3.7)
vue-demi: 0.14.6(vue@3.3.7)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/integrations@10.5.0(focus-trap@7.5.4)(vue@3.3.7)':
dependencies:
'@vueuse/core': 10.5.0(vue@3.3.7)
'@vueuse/shared': 10.5.0(vue@3.3.7)
focus-trap: 7.5.4
vue-demi: 0.14.6(vue@3.3.7)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@10.5.0': {}
'@vueuse/shared@10.5.0(vue@3.3.7)':
dependencies:
vue-demi: 0.14.6(vue@3.3.7)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
algoliasearch@4.20.0:
dependencies:
'@algolia/cache-browser-local-storage': 4.20.0
'@algolia/cache-common': 4.20.0
'@algolia/cache-in-memory': 4.20.0
'@algolia/client-account': 4.20.0
'@algolia/client-analytics': 4.20.0
'@algolia/client-common': 4.20.0
'@algolia/client-personalization': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/logger-common': 4.20.0
'@algolia/logger-console': 4.20.0
'@algolia/requester-browser-xhr': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/requester-node-http': 4.20.0
'@algolia/transporter': 4.20.0
ansi-sequence-parser@1.1.1: {}
csstype@3.1.2: {}
esbuild@0.18.20:
optionalDependencies:
'@esbuild/android-arm': 0.18.20
'@esbuild/android-arm64': 0.18.20
'@esbuild/android-x64': 0.18.20
'@esbuild/darwin-arm64': 0.18.20
'@esbuild/darwin-x64': 0.18.20
'@esbuild/freebsd-arm64': 0.18.20
'@esbuild/freebsd-x64': 0.18.20
'@esbuild/linux-arm': 0.18.20
'@esbuild/linux-arm64': 0.18.20
'@esbuild/linux-ia32': 0.18.20
'@esbuild/linux-loong64': 0.18.20
'@esbuild/linux-mips64el': 0.18.20
'@esbuild/linux-ppc64': 0.18.20
'@esbuild/linux-riscv64': 0.18.20
'@esbuild/linux-s390x': 0.18.20
'@esbuild/linux-x64': 0.18.20
'@esbuild/netbsd-x64': 0.18.20
'@esbuild/openbsd-x64': 0.18.20
'@esbuild/sunos-x64': 0.18.20
'@esbuild/win32-arm64': 0.18.20
'@esbuild/win32-ia32': 0.18.20
'@esbuild/win32-x64': 0.18.20
estree-walker@2.0.2: {}
focus-trap@7.5.4:
dependencies:
tabbable: 6.2.0
fsevents@2.3.3:
optional: true
jsonc-parser@3.2.0: {}
magic-string@0.30.5:
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
mark.js@8.11.1: {}
markdown-it-footnote@3.0.3: {}
medium-zoom@1.0.8: {}
minisearch@6.2.0: {}
nanoid@3.3.6: {}
picocolors@1.0.0: {}
postcss@8.4.31:
dependencies:
nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
preact@10.18.1: {}
rollup@3.29.4:
optionalDependencies:
fsevents: 2.3.3
search-insights@2.9.0: {}
shiki@0.14.5:
dependencies:
ansi-sequence-parser: 1.1.1
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 8.0.0
source-map-js@1.0.2: {}
tabbable@6.2.0: {}
to-fast-properties@2.0.0: {}
vite@4.5.0:
dependencies:
esbuild: 0.18.20
postcss: 8.4.31
rollup: 3.29.4
optionalDependencies:
fsevents: 2.3.3
vitepress-plugin-back-to-top@1.0.1:
dependencies:
vue: 3.3.7
transitivePeerDependencies:
- typescript
vitepress@1.0.0-rc.24(@algolia/client-search@4.20.0)(search-insights@2.9.0):
dependencies:
'@docsearch/css': 3.5.2
'@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)
'@types/markdown-it': 13.0.5
'@vitejs/plugin-vue': 4.3.1(vite@4.5.0)(vue@3.3.7)
'@vue/devtools-api': 6.5.1
'@vueuse/core': 10.5.0(vue@3.3.7)
'@vueuse/integrations': 10.5.0(focus-trap@7.5.4)(vue@3.3.7)
focus-trap: 7.5.4
mark.js: 8.11.1
minisearch: 6.2.0
shiki: 0.14.5
vite: 4.5.0
vue: 3.3.7
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
- '@types/react'
- '@vue/composition-api'
- async-validator
- axios
- change-case
- drauu
- fuse.js
- idb-keyval
- jwt-decode
- less
- lightningcss
- nprogress
- qrcode
- react
- react-dom
- sass
- search-insights
- sortablejs
- stylus
- sugarss
- terser
- typescript
- universal-cookie
vscode-oniguruma@1.7.0: {}
vscode-textmate@8.0.0: {}
vue-demi@0.14.6(vue@3.3.7):
dependencies:
vue: 3.3.7
vue@3.3.7:
dependencies:
'@vue/compiler-dom': 3.3.7
'@vue/compiler-sfc': 3.3.7
'@vue/runtime-dom': 3.3.7
'@vue/server-renderer': 3.3.7(vue@3.3.7)
'@vue/shared': 3.3.7

3
docs/pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,3 @@
# empty workspace to prevent pnpm from installing docs deps unnecessarily
packages:
- .

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/public/mtcute-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="363" viewBox="0 0 400 363">
<defs/>
<path fill="#DE6FBE" d="M299.961,8.049 C328.659,24.617 338.492,61.312 321.923,90.01 L304.003,121.049 L340,121.049 C373.137,121.049 400,147.911 400,181.048 C400,214.186 373.137,241.048 340,241.048 L303.766,241.048 L321.923,272.497 C338.492,301.195 328.659,337.89 299.961,354.459 C271.264,371.027 234.568,361.195 218,332.497 L78,90.01 C61.431,61.312 71.264,24.617 99.961,8.049 C128.659,-8.52 165.354,1.312 181.923,30.01 L199.961,61.254 L218,30.01 C234.568,1.312 271.264,-8.52 299.961,8.049 Z"/>
<path fill="#F69DDC" d="M252.093,210.96 C268.661,182.262 258.829,145.567 230.131,128.998 C219.617,122.928 208.028,120.401 196.75,121.048 L60,121.048 C26.863,121.048 0,147.911 0,181.049 C0,214.186 26.863,241.049 60,241.049 L96.157,241.049 L78.169,272.204 C61.601,300.901 71.433,337.596 100.131,354.165 C128.829,370.734 165.524,360.901 182.093,332.204 L252.093,210.96 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B