diff --git a/.config/tsconfig.build.json b/.config/tsconfig.build.json
deleted file mode 100644
index bd168a0e..00000000
--- a/.config/tsconfig.build.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "extends": "../tsconfig.json",
- "exclude": [
- "../**/*.test.ts",
- "../**/*.test-utils.ts",
- "../**/__fixtures__/**"
- ]
-}
diff --git a/.forgejo/workflows/docs.yaml b/.forgejo/workflows/docs.yaml
index 8a07ff4c..9895a28c 100644
--- a/.forgejo/workflows/docs.yaml
+++ b/.forgejo/workflows/docs.yaml
@@ -1,15 +1,14 @@
-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:
@@ -17,21 +16,27 @@ jobs:
runs-on: node22
steps:
- uses: actions/checkout@v4
- - uses: ./.forgejo/actions/init
- - name: Build docs
+ - uses: pnpm/action-setup@v2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+ - 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
diff --git a/.forgejo/workflows/typedoc.yaml b/.forgejo/workflows/typedoc.yaml
new file mode 100644
index 00000000..5d321196
--- /dev/null
+++ b/.forgejo/workflows/typedoc.yaml
@@ -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
diff --git a/.gitignore b/.gitignore
index cc1e7c88..c6906aaa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,9 +7,6 @@ private/
.vscode
*.log
-# docs are generated in ci
-docs
-
coverage
.rollup.cache
*.tsbuildinfo
diff --git a/build.config.js b/build.config.js
index b01338d3..57302c8a 100644
--- a/build.config.js
+++ b/build.config.js
@@ -69,6 +69,7 @@ export default {
},
},
typedoc: {
+ out: 'dist/typedoc',
excludePackages: [
'@mtcute/tl',
'@mtcute/create-bot',
diff --git a/docs/.editorconfig b/docs/.editorconfig
new file mode 100644
index 00000000..5257de37
--- /dev/null
+++ b/docs/.editorconfig
@@ -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
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100755
index 00000000..3df26ab4
--- /dev/null
+++ b/docs/.gitignore
@@ -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
diff --git a/docs/.vitepress/components/EmbedPost.vue b/docs/.vitepress/components/EmbedPost.vue
new file mode 100644
index 00000000..114d0abf
--- /dev/null
+++ b/docs/.vitepress/components/EmbedPost.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/.vitepress/components/Tag.vue b/docs/.vitepress/components/Tag.vue
new file mode 100755
index 00000000..38552cb9
--- /dev/null
+++ b/docs/.vitepress/components/Tag.vue
@@ -0,0 +1,24 @@
+
+
+ {{ text }}
+
+
+
+
+
+
diff --git a/docs/.vitepress/components/VImg.vue b/docs/.vitepress/components/VImg.vue
new file mode 100755
index 00000000..7eb716ca
--- /dev/null
+++ b/docs/.vitepress/components/VImg.vue
@@ -0,0 +1,45 @@
+
+
+
+ {{ caption || alt }}
+
+
+
+
+
+
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
new file mode 100644
index 00000000..189c579c
--- /dev/null
+++ b/docs/.vitepress/config.mts
@@ -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(
+ '
',
+ ''
+ )
+ },
+ 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: ``,
+ },
+ 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 CC BY 4.0
' +
+ 'Logo by @dotvhs
' +
+ '© Copyright 2021-present, teidesu ❤️',
+ },
+ },
+
+ markdown: {
+ config: (md) => {
+ md.use(markdownItFootnotes);
+ },
+ },
+});
diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue
new file mode 100644
index 00000000..dd917d32
--- /dev/null
+++ b/docs/.vitepress/theme/Layout.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
new file mode 100644
index 00000000..1c13943e
--- /dev/null
+++ b/docs/.vitepress/theme/index.ts
@@ -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
diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css
new file mode 100644
index 00000000..162007aa
--- /dev/null
+++ b/docs/.vitepress/theme/style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..d9ee50fd
--- /dev/null
+++ b/docs/README.md
@@ -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
+```
diff --git a/docs/guide/advanced/net-middlewares.md b/docs/guide/advanced/net-middlewares.md
new file mode 100644
index 00000000..e1a5b450
--- /dev/null
+++ b/docs/guide/advanced/net-middlewares.md
@@ -0,0 +1,154 @@
+# Network middlewares
+
+In some cases it may make sense to intercept *all* outgoing requests and control the request flow.
+
+## Default middlewares
+
+By default, mtcute uses two middlewares: flood-waiter and internal errors handler.
+The combined default middleware is exported in `networkMiddlewares.basic`, and can be configured as follows:
+
+```ts
+const tg = new TelegramClient({
+ ...,
+ network: {
+ middlewares: networkMiddlewares.basic({
+ floodWaiter: { maxWait: 5000, maxRetries: 5 },
+ internalErrors: { maxRetries: 5 }
+ })
+ }
+})
+```
+
+Flood-waiter and internal errors handler middlewares themselves are exported under
+`networkMiddlewares.floodWaiter` and `networkMiddlewares.internalErrorsHandler` respectively.
+
+## Writing middlewares
+
+Middleware is simply an async function that takes `ctx` and `next` as arguments.
+
+The `ctx` object contains information about the RPC call, including the request itself and
+any additional parameters that were passed along, and `next` function can be used to call the
+next middleware in the chain, returning the call result (or an [error](#errors-in-middlewares)):
+
+```ts
+const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
+ if (ctx.request._ === 'help.getConfig') {
+ return myConfig
+ }
+
+ return next(ctx)
+}
+```
+
+::: info
+If you are familiar with grammY/telegraf or koa middlewares,
+you might find the `ctx, next` syntax familiar.
+Indeed, these middlewares were heavily inspired by them.
+
+However, they work slightly different here, as the task is slightly different too.
+
+Unlike grammY-style middlewares, `next` *can* be called multiple times,
+and the last pseudo-"middleware" in the chain will actually execute
+the request contained in the `ctx` (instead of being a no-op).
+
+And because of that, `ctx` is always passed explicitly,
+allowing to execute multiple different requests from a single middleware.
+:::
+
+### Errors in middlewares
+
+To improve performance, RPC errors in middlewares are monadic, meaning that an RPC error is
+considered a valid result.
+
+To check if the call resulted in an error, you can use `isTlRpcError` handler:
+
+```ts
+const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
+ const res = await next(ctx)
+
+ if (isTlRpcError(res) && res.errorMessage === 'PEER_ID_INVALID') {
+ logPeerIdInvalid(ctx.request)
+ }
+
+ return res
+}
+```
+
+You can also use `networkMiddlewares.onRpcError` helper to create a middleware that only handles RPC errors:
+
+```ts
+const client = new TelegramClient({
+ ...,
+ network: {
+ middlewares: [
+ networkMiddlewares.onRpcError(async (ctx, error) => {
+ if (error.errorMessage === 'PEER_ID_INVALID') {
+ logPeerIdInvalid(ctx.request)
+ }
+ }),
+ networkMiddlewares.basic()
+ ]
+ }
+})
+```
+
+### Modifying request
+
+In some cases, it might make sense to modify the request before sending.
+
+One way to do so is to overwrite the `ctx` fields:
+
+```ts
+const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
+ if (ctx.request._ === 'users.getFullUser') {
+ ctx.request.id = { _: 'inputUserSelf' }
+ }
+
+ return next(ctx)
+}
+```
+
+Alternatively, you can construct your own context:
+
+```ts
+const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
+ if (ctx.request._ === 'users.getFullUser') {
+ return next({
+ manager: ctx.manager,
+ params: ctx.params,
+ request: {
+ _: 'users.getFullUser',
+ id: { _: 'inputUserSelf' }
+ }
+ })
+ }
+
+ return next(ctx)
+}
+```
+
+### Applying middlewares
+
+Once you're done writing your middleware, you need to connect it to the client.
+That's done by passing an array to the `middlewares` option, like this:
+
+```ts
+const tg = new TelegramClient({
+ ...,
+ network: {
+ middlewares: [
+ myMiddleware,
+ myOtherMiddleware,
+ // You'll probably also want to include all the default
+ // middlewares, as passing this option overrides them.
+ ...networkMiddlewares.basic()
+ ]
+ }
+})
+```
+
+::: info
+**Middleware order matters**, which is why we include the basic middlewares last —
+we want `myMiddleware` and `myOtherMiddleware` to also benefit from them
+(i.e. have flood waits and internal errors handled)
+:::
\ No newline at end of file
diff --git a/docs/guide/advanced/session-convert.md b/docs/guide/advanced/session-convert.md
new file mode 100644
index 00000000..5616a03e
--- /dev/null
+++ b/docs/guide/advanced/session-convert.md
@@ -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: [],
+ }
+})
+```
diff --git a/docs/guide/advanced/treeshaking.md b/docs/guide/advanced/treeshaking.md
new file mode 100644
index 00000000..1d4d7e88
--- /dev/null
+++ b/docs/guide/advanced/treeshaking.md
@@ -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.
\ No newline at end of file
diff --git a/docs/guide/advanced/workers.md b/docs/guide/advanced/workers.md
new file mode 100644
index 00000000..63e6463a
--- /dev/null
+++ b/docs/guide/advanced/workers.md
@@ -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.
+
diff --git a/docs/guide/dispatcher/children.md b/docs/guide/dispatcher/children.md
new file mode 100755
index 00000000..a6000972
--- /dev/null
+++ b/docs/guide/dispatcher/children.md
@@ -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.
diff --git a/docs/guide/dispatcher/di.md b/docs/guide/dispatcher/di.md
new file mode 100644
index 00000000..d343c66d
--- /dev/null
+++ b/docs/guide/dispatcher/di.md
@@ -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.
+:::
\ No newline at end of file
diff --git a/docs/guide/dispatcher/errors.md b/docs/guide/dispatcher/errors.md
new file mode 100755
index 00000000..659b0f90
--- /dev/null
+++ b/docs/guide/dispatcher/errors.md
@@ -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
+```
+
+
diff --git a/docs/guide/dispatcher/filters.md b/docs/guide/dispatcher/filters.md
new file mode 100755
index 00000000..1be0167d
--- /dev/null
+++ b/docs/guide/dispatcher/filters.md
@@ -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
+ }
+)
+```
+
+### Logical addition
+
+Logical addition (i.e. OR operator) is supported with `filters.or`
+
+```ts
+dp.onNewMessage(
+ filters.or(filters.video, filters.photo),
+ async (msg) => {
+ // msg.media is Photo | Video
+ }
+)
+```
+
+### Logical multiplication
+
+Logical multiplication (i.e. AND operator) is supported with `filters.and`.
+
+```ts
+dp.onNewMessage(
+ filters.and(filters.chat('private'), filters.photo),
+ async (msg) => {
+ // msg.media is Photo
+ }
+)
+```
+
+## Custom filters
+
+Sometimes pre-existing filters are just not enough. Then, you
+can write a custom filter.
+
+### Simple custom filter
+
+Under the hood, filters are simply functions, so you can do the following:
+
+```ts
+dp.onNewMessage(
+ (msg) => msg.sender.isVerified,
+ async (msg) => {
+ // ...
+ }
+)
+```
+
+It can even be asynchronous:
+
+```ts
+dp.onNewMessage(
+ async (msg) => await shouldProcessMessage(msg.id),
+ async (msg) => {
+ // ...
+ }
+)
+```
+
+### Parameters
+
+To accept parameters in your custom filter, simply create a function
+that returns a filter:
+
+```ts
+const usernameRegex = (regex: RegExp): UpdateFilter =>
+ (msg) => {
+ const m = msg.sender.username?.match(regex)
+
+ return !!m
+ }
+
+dp.onNewMessage(
+ usernameRegex(/some_regex/),
+ async (msg) => {
+ // ...
+ }
+)
+```
+
+### Adding type modification
+
+You can also add type modification to your custom filter:
+
+```ts
+const fromChat: UpdateFilter =
+ (msg) => msg.sender.type === 'chat'
+
+dp.onNewMessage(
+ fromChat,
+ async (msg) => {
+ // msg.sender is Chat
+ }
+)
+```
+
+::: warning
+This is used as-is, it is not checked if the filter actually
+checks for the given modification due to TypeScript limitations.
+:::
+
+### Additional fields
+
+You can add additional fields to the update using type
+modifications. This may be useful in multiple cases, for example,
+to add the result of the parametrized filter:
+
+```ts
+const usernameRegex = (regex: RegExp): UpdateFilter<
+ Message,
+ { usernameMatch: RegExpMatchArray }
+> => (msg) => {
+ const m = msg.sender.username?.match(regex)
+
+ if (m) {
+ ;(obj as any).usernameMatch = m
+ return true
+ }
+ return false
+}
+
+dp.onNewMessage(
+ usernameRegex(/some_regex/),
+ async (msg) => {
+ // msg.usernameMatch is RegExpMatchArray
+ }
+)
+```
+
+Or, you can add some additional fields similar to a middleware
+in other frameworks:
+
+```ts
+const loadSenderFromDb: UpdateFilter =
+ async (msg) => {
+ ;(msg as any).senderDb = await db.loadUser(msg.sender.id)
+ return true
+ }
+
+dp.onNewMessage(
+ filters.and(filters.chat('private'), loadSenderFromDb),
+ async (msg) => {
+ // msg.senderDb is UserModel
+ }
+)
+```
+
+::: warning
+While this is possible, this is **strongly not recommended**.
+
+Instead, do this right inside the handler code:
+
+```ts
+dp.onNewMessage(
+ filters.chat('private'),
+ async (msg) => {
+ const senderDb = await db.loadUser(msg.sender.id)
+ }
+)
+```
+:::
diff --git a/docs/guide/dispatcher/groups-propagation.md b/docs/guide/dispatcher/groups-propagation.md
new file mode 100755
index 00000000..37a426a1
--- /dev/null
+++ b/docs/guide/dispatcher/groups-propagation.md
@@ -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',
+ () => { ... }
+)
+```
diff --git a/docs/guide/dispatcher/handlers.md b/docs/guide/dispatcher/handlers.md
new file mode 100755
index 00000000..1d57dacf
--- /dev/null
+++ b/docs/guide/dispatcher/handlers.md
@@ -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.')
+})
+```
+
+* Telegram might decide not to send these updates
+in case this message is old enough.
+
+## Message group
+
+When message grouping is enabled (see [here](/guide/intro/updates.md#message-grouping)),
+`message_group` handlers are dispatched when a media group (aka album) is received:
+
+```ts
+dp.onMessageGroup(async (upd) => {
+ await upd.replyText('Thanks for the media!')
+})
+```
+
+## Delete message
+
+Whenever a message is deleted (and client receives an update about that*),
+`delete_message` handlers are dispatched. Note that these updates
+only contain message ID(s), and not its contents, and it is up to you
+to check what that ID corresponds to.
+
+```ts
+dp.onDeleteMessage(async (upd) => {
+ if (upd.messageIds.includes(42)) {
+ console.log('Magic message deleted :c')
+ }
+})
+```
+
+Note that for private chats, this does not include
+user's ID, so you may want to implement some caching
+if you need that info.
+
+* Telegram might decide not to send these updates
+in case this message is old enough.
+
+## Chat member
+
+Whenever chat member status is changed in a channel/supergroup where the bot
+is an administrator, `chat_member` handlers are dispatched.
+
+```ts
+dp.onChatMemberUpdate(async (upd) => {
+ console.log(`${upd.user.displayName} ${upd.type} by ${upd.actor.displayName}`)
+})
+```
+
+::: tip
+You can filter by update type using `filters.chatMember`:
+
+```ts
+dp.onChatMemberUpdate(
+ filters.chatMember('joined'),
+ async (upd) => {
+ await upd.client.sendText(upd.chat, `${upd.user.mention()}, welcome to the chat!`)
+ }
+)
+```
+:::
+
+## Inline query
+
+Whenever an inline query is sent by user to your bot, `inline_query`
+handlers are dispatched:
+
+```ts
+dp.onInlineQuery(async (upd) => {
+ await upd.answer([], {
+ switchPm: {
+ text: 'Hello!',
+ parameter: 'inline_hello'
+ }
+ })
+})
+```
+
+You can learn more about inline queries in [Inline Mode](./inline-mode.html) section.
+
+## Chosen inline result
+
+When a user selects an inline result, and assuming that you have
+**inline feedback** feature enabled, `chosen_inline_result` handlers
+are dispatched:
+
+```ts
+dp.onChosenInlineResult(async (upd) => {
+ await upd.editMessage({
+ text: `${result.user.displayName}, thanks for using inline!`
+ })
+})
+```
+
+As mentioned, these updates are only sent by Telegram when you
+have enabled **inline feedback** feature. You can enable it
+in [@BotFather](https://t.me/botfather).
+
+It is however noted by Telegram that this should only be used
+for statistical purposes, and even if probability setting is 100%,
+not all chosen inline results may be reported
+([source](https://core.telegram.org/api/bots/inline#inline-feedback)).
+
+## Callback query
+
+Whenever user clicks on a [callback button](../topics/keyboards.html#inline-keyboards),
+`callback_query` or `inline_callback_query` handlers are dispatched, based on the origin of the message:
+
+```ts
+dp.onCallbackQuery(async (upd) => {
+ await upd.answer({ text: '🌸' })
+})
+
+dp.onInlineCallbackQuery(async (upd) => {
+ await upd.answer({ text: '🌸' })
+})
+```
+
+For messages sent normally by the bot (e.g. using `sendText`), `callback_query` handlers are dispatched.
+For messages sent from an inline query (e.g. inside `onInlineQuery`), `inline_callback_query` handlers are dispatched.
+
+## Poll update
+
+Whenever a poll state is updated (stopped, anonymous user has voted, etc.),
+`poll` handlers are dispatched:
+
+```ts
+dp.onPollUpdate(async (upd) => {
+ // do something
+})
+```
+
+Bots only receive updates about polls that they have sent.
+
+Note that due to Telegram limitation, sometimes the update does not
+include the poll itself, and mtcute creates a "stub" poll,
+that is missing most of the information (including question text,
+answers text, missing flags like `quiz`, etc.). Number of votes per-answer
+is always there, though.
+
+If you need that missing information, you will need to implement
+caching by yourself. Bot API (strictly speaking, TDLib) does it internally
+and thus is able to provide all the needed information autonomously.
+This is not implemented in mtcute yet.
+
+## Poll vote
+
+When a user votes in a public poll, `poll_vote` handlers are dispatched:
+
+```ts
+dp.onPollVote(async (upd) => {
+ upd.user.sendText('Thanks for voting!')
+})
+```
+
+Bots only receive updates about polls that they have sent.
+
+This update currently doesn't contain information about the poll,
+only the poll ID, so if you need that info,
+you'll have to implement caching yourself.
+
+
+## User status
+
+When a user's online status changes (e.g. user goes offline),
+and client receives an update about that*,
+`user_status` handlers are dispatched:
+
+```ts
+dp.onUserStatusUpdate(async (upd) => {
+ console.log(`User ${upd.userId} is now ${upd.status}`)
+})
+```
+
+* Telegram might decide not to send these updates
+in many cases, for example: you don't have an active PM
+with this user, this user is from a large chat that
+you aren't currently chatting in, etc.
+
+## User typing
+
+When a user's typing status changes,
+and client receives an update about that*,
+`user_status` handlers are dispatched:
+
+```ts
+dp.onUserTyping(async (upd) => {
+ console.log(`${upd.userId} is ${upd.status} in ${upd.chatId}`)
+})
+```
+
+* Telegram might decide not to send these updates
+in many cases, for example: you haven't talked
+to this user for some time, that user/chat is archived, etc.
+
+## History read
+
+When history is read in a chat (either by the other party or by you from another client),
+and client receives an update about that, `history_read` handlers are dispatched:
+
+```ts
+dp.onHistoryRead(async (upd) => {
+ console.log(`History read in ${upd.chatId} up to ${upd.maxReadId}`)
+})
+```
+
+## Bot stopped
+
+When a user clicks "Stop bot" button in the bot's profile,
+`bot_stopped` handlers are dispatched:
+
+```ts
+dp.onBotStopped(async (upd) => {
+ console.log(`Bot stopped by ${upd.user.id}`)
+})
+```
+
+## Join requests
+
+These updates differ depending on whether the currently logged in user is a bot or not.
+
+### Bot
+
+When a user requests to join a group/channel where the current bot is an admin,
+`bot_chat_join_request` handlers are dispatched:
+
+```ts
+dp.onBotChatJoinRequest(async (upd) => {
+ console.log(`User ${upd.user.id} wants to join ${upd.chat.id}`)
+
+ if (upd.user.id === DUROV) {
+ await upd.decline()
+ }
+})
+```
+
+### User
+
+When a user requests to join a group/channel where the current user is an admin,
+`chat_join_request` handlers are dispatched:
+
+```ts
+dp.onChatJoinRequest(async (upd) => {
+ console.log(`User ${upd.recentRequesters[0].id} wants to join ${upd.chatId}`)
+})
+```
+
+These updates contain less information than bot join requests,
+and additional info should be fetched manually if needed
+
+## Pre-checkout query
+
+When a user clicks "Pay" button, `pre_checkout_query` handlers are dispatched:
+
+```ts
+dp.onPreCheckoutQuery(async (upd) => {
+ await upd.approve()
+})
+```
+
+## Story updates
+
+When a story is posted or modified, `story` handlers are dispatched:
+
+```ts
+dp.onStoryUpdate(async (upd) => {
+ console.log(`${upd.peer.id} posted or modified a story!`)
+})
+```
+
+## Delete story updates
+
+When a story is deleted, `delete_story` handlers are dispatched:
+
+```ts
+dp.onDeleteStory(async (upd) => {
+ console.log(`${upd.peer.id} deleted story ${upd.storyId}!`)
+})
+```
+
+## Raw updates
+
+Dispatcher only implements the most commonly used updates,
+but you can still add a handler for a custom MTProto update:
+
+```ts
+dp.onRawUpdate(
+ (cl, upd) => upd._ === 'updateUserName',
+ async (
+ client: TelegramClient,
+ update_: tl.TypeUpdate,
+ peers: PeersIndex,
+ ) => {
+ const update = update_ as tl.RawUpdateUserName
+ // ...
+ }
+)
+```
+
+Note that the signature is slightly different: since the update
+is not parsed by the library, client, raw update and entities
+are provided as-is.
+
+Raw update handlers are dispatched independently of parsed update handlers
+([learn more](groups-propagation.html#raw-updates)).
+
+## `addUpdateHandler`
+
+Another way to register a handler is to use `addUpdateHandler` method.
+
+It supports all the updates that have a specialized methods, and this
+method is actually used by `on*` under the hood. It accepts an object,
+containing update type, its handler and optionally a `check` function,
+which checks if the update should be handled by this handler
+(basically a [Filter](filters.html)):
+
+```ts
+dp.addUpdateHandler({
+ 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')
+```
diff --git a/docs/guide/dispatcher/inline-mode.md b/docs/guide/dispatcher/inline-mode.md
new file mode 100755
index 00000000..25322cda
--- /dev/null
+++ b/docs/guide/dispatcher/inline-mode.md
@@ -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)
+
+
+
+## Handling inline queries
+
+To handle inline queries to your bot, simply use `onInlineQuery`:
+
+```ts
+dp.onInlineQuery(async (query) => {
+ // ...
+})
+```
+
+You can also use `filters.regex` to filter based on the query text:
+
+```ts
+dp.onInlineQuery(
+ filters.regex(/^cats /),
+ async (query) => {
+ // ...
+ }
+)
+```
+
+## Answering to inline queries
+
+Answering an inline query means sending results of the query to Telegram.
+The results may contain any media, including articles, photos, videos,
+stickers, etc, and can also be heterogeneous, which means that you can use
+different media types in results for the same query.
+
+To answer a query, simply use `.answer()` and pass an array that
+contains [results](#results):
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([...])
+})
+```
+
+::: tip
+Clients wait about 10 seconds for results, after which they assume
+the bot timed out. Make sure not to do heavy computations there!
+:::
+
+## Results
+
+Every inline result must contain a **unique** result ID, which
+can be later used in [chosen inline result updates](#chosen-inline-results)
+updates to determine which one was chosen.
+
+Media inside inline results *must* be provided via either HTTP URL, or
+re-used from Telegram (e.g. using [File IDs](/guide/topics/files.md#file-ids)).
+Telegram does not allow uploading new files directly to inline results.
+
+When choosing a result, by default, a message containing the respective
+result is sent, but the message contents can be [customized](#custom-message)
+using `message` field.
+
+Telegram supports a bunch of result types, and mtcute supports sending all of
+them as an inline result. In the below examples we'll be using direct URLs
+to content, but you can also use File IDs instead.
+
+### Article
+
+An article is a result that contains a title, description and *optionally*
+a thumbnail and a URL:
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.article(
+ 'RESULT_ID',
+ {
+ title: 'Article title',
+ description: 'Article description',
+ thumb: 'https://example.com/image.jpg',
+ url: 'https://example.com/some-article.html'
+ }
+ )
+ ])
+})
+```
+
+When choosing this result, by default, a message with the following
+text is sent (Handlebars syntax is used here):
+
+```handlebars
+{{#if url}}
+{{title}}
+{{else}}
+{{title}}
+{{/if}}
+{{#if description}}
+{{description}}
+{{/if}}
+```
+
+For the above example, this would result in the following message:
+
+
+
+### GIF
+
+You can send an animated GIF (either real GIF, or an MP4 without sound)
+as a result.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.gif(
+ 'RESULT_ID',
+ 'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
+ )
+ ])
+})
+```
+
+You can also add title and description, however only some clients display them
+(e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.gif(
+ 'RESULT_ID',
+ 'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
+ {
+ title: 'GIF title',
+ description: 'GIF description',
+ }
+ )
+ ])
+})
+```
+
+### Video
+
+You can send an MP4 video as an inline result.
+
+When sending by direct URL, there's a file size limit of 20 MB,
+and you *must* provide a thumbnail, otherwise the result will be ignored.
+Thumbnail is only used until the video file is cached by Telegram, and is then
+overridden by Telegram-generated video thumbnail.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.video(
+ 'RESULT_ID',
+ 'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
+ {
+ thumb:
+ 'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
+ title: 'Video title',
+ description: 'Video description',
+ }
+ )
+ ])
+})
+```
+
+Alternatively, you can send a video by its URL (e.g. from YouTube) using
+`isEmbed: true`:
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.video(
+ 'RESULT_ID',
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ {
+ isEmbed: true,
+ thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
+ title: 'Video title',
+ description: 'Video description',
+ }
+ )
+ ])
+})
+```
+
+Choosing such result would send a message containing URL to that video:
+
+
+
+### Audio
+
+You can send an MPEG audio file as an inline result.
+
+When sending by direct URL, there's a file size limit of 20 MB.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.audio(
+ 'RESULT_ID',
+ 'https://vk.com/mp3/cc_ice_melts.mp3',
+ {
+ performer: 'Griby',
+ title: 'Tayet Lyod',
+ }
+ )
+ ])
+})
+```
+
+::: tip NOTE
+Performer, title and other meta can't be changed once the file is
+cached by Telegram (they will still be displayed in the results,
+but not in the message). To avoid caching when sending by URL, add
+a random query parameter (e.g. `?notgcache`), which will make
+Telegram think this is a new file.
+:::
+
+### Voice
+
+You can send an OGG file as a voice note inline result.
+
+Waveform seems to only be generated for OGG files encoded with OPUS,
+so if it is not generated, try re-encoding your file with OPUS.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.voice(
+ 'RESULT_ID',
+ 'https://tei.su/test_voice.ogg',
+ {
+ title: 'Voice title',
+ }
+ )
+ ])
+})
+```
+
+### Photo
+
+You can send an image as an inline result.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg'
+ )
+ ])
+})
+```
+
+You can also add title and description, however only some clients display them
+(e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ title: 'Photo title',
+ description: 'Photo description',
+ }
+ )
+ ])
+})
+```
+
+### Sticker
+
+You can send a sticker as an inline result. You can't send a
+sticker by URL ([Telegram limitation](https://t.me/tdlibchat/17923)),
+only by File ID.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.sticker(
+ 'RESULT_ID',
+ 'CAACAgIAAxk...JtzysqiUK3IAQ'
+ )
+ ])
+})
+```
+
+### File
+
+You can send a file as an inline result.
+
+Due to Telegram limitations, when using URLs, you can only send PDF
+and ZIP files, and must set `mime` accordingly
+(`application/pdf` and `application/zip` MIMEs respectively).
+With File IDs, you can send any file.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.file(
+ 'RESULT_ID',
+ 'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
+ {
+ mime: 'application/pdf',
+ title: 'File title',
+ description: 'File description'
+ }
+ )
+ ])
+})
+```
+
+### Geolocation
+
+You can send a geolocation as an inline result.
+
+By default, Telegram generates `thumb` for result based on the
+location provided.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.geo(
+ 'RESULT_ID',
+ {
+ latitude: 55.751999,
+ longitude: 37.617734,
+ title: 'Kremlin'
+ }
+ ),
+ ])
+})
+```
+
+### Venue
+
+You can send a venue as an inline result.
+
+By default, Telegram generates `thumb` for result based on the
+location provided.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.venue(
+ 'RESULT_ID',
+ {
+ latitude: 55.751999,
+ longitude: 37.617734,
+ title: 'Kremlin',
+ address: 'Red Square'
+ }
+ ),
+ ])
+})
+```
+
+### Contact
+
+You can send a contact as an inline result.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.contact(
+ 'RESULT_ID',
+ {
+ firstName: 'Alice',
+ phone: '+79001234567',
+ thumb: 'https://avatars.githubusercontent.com/u/86301490'
+ }
+ ),
+ ])
+})
+```
+
+### Games
+
+Finally, you can send a game as an inline result.
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.game('RESULT_ID', 'game_short_name'),
+ ])
+})
+```
+
+
+## Custom message
+
+By default, mtcute generates a message based on result contents. However,
+you can override the message that will be sent using `message`
+
+### Text
+
+Instead of media or default article message, you may want to send a custom
+text message:
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ message: BotInlineMessage.text(
+ 'Ha-ha, just kidding. No Renge for you :p'
+ )
+ }
+ )
+ ])
+})
+```
+
+### Media
+
+You can customize media message (for photos, videos, voices, documents, etc.)
+with custom caption, keyboard, etc:
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
+ }
+ )
+ ])
+})
+```
+
+Sadly, Telegram does not allow sending another media instead of the one
+you provided in the result (however, you *could* use web previews to
+accomplish similar results)
+
+### Geolocation
+
+Instead of sending the default message, you can send geolocation,
+or even live geolocation:
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ // or BotInlineMessage.geoLive
+ message: BotInlineMessage.geo({
+ latitude: 55.751999,
+ longitude: 37.617734,
+ }),
+ }
+ )
+ ])
+})
+```
+
+### Venue
+
+Instead of sending the default message, you can send a venue
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ message: BotInlineMessage.venue({
+ latitude: 55.751999,
+ longitude: 37.617734,
+ title: 'Kremlin',
+ address: 'Red Square'
+ }),
+ }
+ )
+ ])
+})
+```
+
+### Contact
+
+Instead of sending the default message, you can send a contact
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.photo(
+ 'RESULT_ID',
+ 'https://nyanpa.su/renge.jpg',
+ {
+ message: BotInlineMessage.contact({
+ firstName: 'Alice',
+ phone: '+79001234567',
+ }),
+ }
+ )
+ ])
+})
+```
+
+### Game
+
+For inline results containing a game, you can customize keyboard
+under the message ([learn more](../topics/keyboards.html)):
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([
+ BotInline.game('RESULT_ID', 'test', {
+ message: BotInlineMessage.game({
+ replyMarkup: ...
+ })
+ }),
+ ])
+})
+```
+
+## Switch to PM
+
+Some bots may benefit from switching to PM with the bot
+for some action (e.g. logging in with your account).
+For that, you can use `switchPm` button along with your results:
+
+
+
+```ts
+dp.onInlineQuery(async (query) => {
+ query.answer([], {
+ switchPm: {
+ text: 'Log in',
+ parameter: 'login_inline'
+ }
+ })
+})
+
+dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
+ await msg.answerText('Thanks for logging in!')
+})
+```
+
+## Chosen inline results
+
+When a user selects an inline result, and assuming that you have
+**inline feedback** feature enabled, an update is received,
+which can be handled:
+
+```ts
+dp.onChosenInlineResult(async (result) => {
+ await result.editMessage({
+ text: `${result.user.displayName}, thanks for using inline!`
+ })
+})
+```
+
+You can use `filters.regex` to filter by chosen result ID:
+
+```ts
+dp.onChosenInlineResult(
+ filters.regex(/^CATS_/),
+ async (result) => {
+ // ...
+ }
+)
+```
+
+These updates are only sent by Telegram when you
+have enabled **inline feedback** feature. You can enable it
+in [@BotFather](https://t.me/botfather).
+
+It is however noted by Telegram that this should only be used
+for statistical purposes, and even if probability setting is 100%,
+not all chosen inline results may be reported
+([source](https://core.telegram.org/api/bots/inline#inline-feedback)).
+
+
+## Editing inline message
+
+You can edit an inline message in Chosen inline query update handlers, and
+in Callback query updates:
+
+
+::: tip NOTE
+In the below examples, it is assumed that callback query
+originates from an inline message.
+:::
+
+```ts
+dp.onChosenInlineResult(async (result) => {
+ await result.editMessage({...})
+})
+
+dp.onCallbackQuery(async (query) => {
+ await query.editMessage({...})
+})
+```
+
+You can also save inline message ID and edit the message later:
+
+```ts
+function updateMessageLater(msgId: string) {
+ setTimeout(() => {
+ tg.editInlineMessage(msgId, {...})
+ .catch(console.error)
+ }, 5000)
+}
+
+dp.onChosenInlineResult(async (result) => {
+ updateMessageLater(result.messageIdStr)
+})
+
+dp.onCallbackQuery(async (query) => {
+ updateMessageLater(query.inlineMessageIdStr)
+})
+```
+
+::: tip
+`messageIdStr` and `inlineMessageIdStr` contain string representation
+of the inline message ID, to simplify its storage.
+
+You can also use the `messageId` and `inlineMessageId` respectively,
+which contain a TL object, in case you need to use Raw API. In mtcute,
+they are interchangeable.
+:::
+
+You can also edit media inside inline messages, and even upload new media
+directly to them (unlike Bot API):
+
+```ts
+dp.onChosenInlineResult(async (result) => {
+ const link = await getDownloadLink(result.id)
+
+ await result.editMessage({
+ media: InputMedia.audio(await fetch(link))
+ })
+})
+```
diff --git a/docs/guide/dispatcher/intro.md b/docs/guide/dispatcher/intro.md
new file mode 100755
index 00000000..fbf33497
--- /dev/null
+++ b/docs/guide/dispatcher/intro.md
@@ -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)
+```
+
diff --git a/docs/guide/dispatcher/middlewares.md b/docs/guide/dispatcher/middlewares.md
new file mode 100755
index 00000000..49f721b1
--- /dev/null
+++ b/docs/guide/dispatcher/middlewares.md
@@ -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((upd) => {
+ upd.start = Date.now()
+})
+
+dp.onPostUpdate((handled, upd) => {
+ if (handled) {
+ console.log(`handled ${upd.name} in ${Date.now() - upd.start} ms`)
+ }
+})
+
+dp.onError((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)).
diff --git a/docs/guide/dispatcher/rate-limit.md b/docs/guide/dispatcher/rate-limit.md
new file mode 100755
index 00000000..aeeb84d5
--- /dev/null
+++ b/docs/guide/dispatcher/rate-limit.md
@@ -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()}`)
+```
\ No newline at end of file
diff --git a/docs/guide/dispatcher/scenes.md b/docs/guide/dispatcher/scenes.md
new file mode 100755
index 00000000..f17ea2a4
--- /dev/null
+++ b/docs/guide/dispatcher/scenes.md
@@ -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.
+
+
+
+* Only updates that can be keyed are supported
+
+## Creating a scene
+
+A scene is created by using `Dispatcher.scene`:
+
+```ts
+interface SceneState { ... }
+
+const dp = Dispatcher.scene('scene-name')
+// add handlers to `dp`
+
+export const SomeScene = dp
+
+// then in the main file:
+dp.addScene(SomeScene)
+```
+
+If you don't use state within your scene, just don't pass anything:
+
+```ts
+const scene = 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(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()
+// 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:
+
+```ts
+interface RegForm {
+ name?: string
+}
+
+const wizard = new WizardScene('REGISTRATION')
+
+wizard.addStep(async (msg) => {
+ await msg.answerText('What is your name?', {
+ replyMarkup: BotKeyboard.inline([[BotKeyboard.callback('Skip', 'SKIP')]]),
+ })
+
+ return WizardSceneAction.Next
+})
+
+wizard.onCallbackQuery(filters.and(wizard.onCurrentStep(), filters.equals('SKIP')), async (upd, state) => {
+ await state.merge({ name: 'Anonymous' })
+ await wizard.skip(state)
+
+ await upd.client.sendText(upd.chatId, 'Alright, "Anonymous" then\n\nNow enter your email')
+})
+
+wizard.addStep(async (msg, state) => {
+ // simple validation
+ if (msg.text.length < 3) {
+ await msg.replyText('Invalid name!')
+ return WizardSceneAction.Stay
+ }
+
+ await state.set({ name: msg.text.trim() })
+ await msg.answerText('Enter your email')
+
+ return WizardSceneAction.Next
+})
+
+wizard.addStep(async (msg, state) => {
+ const { name } = (await state.get())!
+
+ console.log({ name, email: msg.text })
+
+ await msg.answerText('Thanks!')
+ return WizardSceneAction.Exit
+})
+```
+
+If you are using some custom state, you may want to set the default
+state for the wizard:
+
+```ts
+wizard.setDefaultState({ name: 'Ivan' })
+```
+
+By default, `{}` is used as the default state.
+
+## Transition updates
+
+Whenever you `.enter()` or `.exit()` a scene, the dispatcher will also emit
+a transition update, which can be caught by using `onSceneTransition`:
+
+```ts
+scene.onSceneTransition(async (upd, state) => {
+ console.log(`Transition from ${upd.previousScene} to SomeScene`)
+})
+```
+
+These handlers are called **before** any of the scene's handlers are called,
+even if `PropagationAction.ToScene` is used, and can be used to cancel the transition:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ await state.enter(SomeScene)
+ return PropagationAction.ToScene
+})
+
+SomeScene.onSceneTransition(async (upd, state) => {
+ await state.exit()
+ return PropagationAction.Stop
+})
+
+SomeScene.onNewMessage(async (msg, state) => {
+ await msg.replyText('This will never be called')
+})
+```
+
+The update which triggered the transition is passed to the handler, and
+you can use it to decide whether to cancel the transition or not:
+
+```ts
+SomeScene.onSceneTransition(async (upd, state) => {
+ if (upd.message.text === 'cancel') {
+ return PropagationAction.Stop
+ }
+})
+```
diff --git a/docs/guide/dispatcher/state.md b/docs/guide/dispatcher/state.md
new file mode 100755
index 00000000..df429fdd
--- /dev/null
+++ b/docs/guide/dispatcher/state.md
@@ -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.
+
+
+
+::: tip
+FSM is even more useful when used with [Scenes](scenes.html)
+:::
+
+## Setup
+
+Dispatcher natively supports FSM. To set it up, simply pass
+a [storage](#storage) to the constructor:
+
+```ts
+interface BotState { ... }
+
+const dp = Dispatcher.for(tg, {
+ storage: new MemoryStateStorage()
+})
+// or, for children
+const dp = Dispatcher.child()
+```
+
+You **must** provide some state type in order to use FSM (in the example above,
+`BotState`). You *can* use `any`, but this is not recommended.
+
+Then, the update state argument will be available in every handler
+that supports FSM (that is: `new_message`, `edit_message`, `message_group`, `callback_query`)
+as well as to their filters:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ // ...
+})
+```
+
+::: warning
+Type parameter for `Dispatcher` (in this case, `BotState`) is only
+used as a *hint* for the compiler.
+
+It is not checked at runtime.
+:::
+
+## Getting current state
+
+To retrieve the current state, use `state.get`:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ const current = await state.get()
+})
+```
+
+By default, if there's no state stored, `null` is returned.
+However, you can provide the default fallback state,
+which will be used instead:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ const current = await state.get({ ... })
+ // or a function
+ const current = await state.get(() => ({ ... }))
+})
+```
+
+## Updating state
+
+To update the state, use `state.set`:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ await state.set({ ... })
+})
+```
+
+You can also set a TTL, after which the newly set state
+will be considered "stale" and removed:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ // ttl = 1 hour
+ await state.set({ ... }, 3600)
+})
+```
+
+You can also modify the existing state by only
+providing the modification (under the hood, the library will
+fetch the current state automatically):
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ await state.merge({ ... })
+})
+```
+
+If the state can be empty, make sure to pass the default state,
+otherwise an error will be thrown:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ await state.merge({ ... }, defaultState)
+})
+```
+
+## Removing state
+
+To remove currently stored state, use `state.delete`:
+
+```ts
+dp.onNewMessage(async (msg, state) => {
+ await state.delete()
+})
+```
+
+## Related filters
+
+As mentioned above, state (and its type!) is also available to the filters,
+so you can make [custom filters](filters.html#custom-filters) that use it:
+
+```ts
+dp.onNewMessage(
+ (msg, state) => state.get().then((res) => res?.action === 'ENTER_PASSWORD'),
+ async (msg, state) => {
+ // ...
+ }
+)
+```
+
+However, the above isn't very clean, so the library provides `filters.state`:
+
+```ts
+dp.onNewMessage(
+ filters.state((state) => state.action === 'ENTER_PASSWORD'),
+ async (msg, state: UpdateState) => {
+ const current = await state.get()
+ // or, if you have strict null checks
+ const current = (await state.get())!
+ }
+)
+```
+
+Note that here we explicitly pass inner type, because due to TypeScript
+limitations, we can't automatically derive state type from the predicate.
+
+`filters.state` *does not* match empty state, instead,
+use `filters.stateEmpty`:
+
+```ts
+dp.onNewMessage(
+ filters.stateEmpty,
+ async (msg, state) => {
+ // ...
+ }
+)
+```
+
+## Keying
+
+FSM may look like magic, but in fact it is not. Under the hood,
+user's state is stored in the [storage](#storage), and the key is derived
+from the update object.
+
+By default, `defaultStateKeyDelegate` is used, which derives
+the key as follows:
+- If private chat, `msg.chat.id`
+- If group chat, `msg.chat.id + '_' + msg.sender.id`
+- If channel, `msg.chat.id`
+- If callback query from a non-inline message:
+ - If in private chat (i.e. `upd.chatType === 'user'`), `upd.user.id`
+ - If in group/channel/supergroup (i.e. `upd.chatType !== 'user'`),
+ `upd.chatId + '_' + upd.user.id`
+
+This is meant to be a pretty opinionated default, but you can use custom
+keying mechanism too, if you want:
+
+```ts
+const customKey = (upd) => ...
+
+const dp = new Dispatcher(tg, storage, customKey)
+// or, locally for a child dispatcher:
+const dp = new Dispatcher(customKey)
+```
+
+
+## Getting state from outside
+
+In some cases, you may need to access the state (and maybe even alter it)
+outside of handlers, in the handler that does not support state, or using a different key.
+
+You can do so by using `.getState`:
+
+```ts
+const state = await dp.getState(await msg.getReply())
+// you can also pass User/Chat instances:
+const state = await dp.getState(msg.sender)
+// and then, for example
+await state.delete()
+```
+
+When providing an object, dispatcher will use its own keying
+mechanism(s). You can provide a key manually to avoid that:
+
+```ts
+const target = msg.getReply()
+const state = dp.getState(defaultStateKeyDelegate(target))
+// or even manually
+const state = await dp.getState(`${target.chat.id}`)
+```
+
+You can also provide a totally custom key
+to store arbitrary data:
+
+```ts
+// tip: prefix the key with $ and then something unique
+// to avoid clashing with FSM and Scenes
+const state = await dp.getState(`$internal-user-pref:${userId}`)
+```
+
+::: warning
+`getState` **DOES NOT** guarantee type of the state,
+because it can not determine the origin of state key.
+
+By default, it uses dispatcher's state type, but you can also
+override this with type parameter:
+
+```ts
+const state = await dp.getState(...)
+```
+:::
+
+## Storage
+
+Storage is the backend used by Dispatcher to store state related information.
+A storage is a class that implements [`IStateStorageProvider`](https://ref.mtcute.dev/types/_mtcute_dispatcher.IStateStorageProvider.html).
+
+```ts
+const dp = Dispatcher.for(tg, { storage: new MemoryStorage() })
+// or, locally for a child dispatcher:
+const dp = Dispatcher.child({ 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(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(tg, {
+ storage: new SqliteStateStorage(new SqliteStorageDriver('my-state'))
+})
+```
\ No newline at end of file
diff --git a/docs/guide/index.md b/docs/guide/index.md
new file mode 100755
index 00000000..4435299a
--- /dev/null
+++ b/docs/guide/index.md
@@ -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 MTCute!`)
+```
+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.
+:::
\ No newline at end of file
diff --git a/docs/guide/intro/errors.md b/docs/guide/intro/errors.md
new file mode 100755
index 00000000..9806b421
--- /dev/null
+++ b/docs/guide/intro/errors.md
@@ -0,0 +1,148 @@
+# Handling errors
+
+> There are two ways to write error-free programs; only the third one works
+>
+> © Alan J. Perlis
+
+Errors are an inevitable part of any software development, especially
+when working with external APIs, and it is important to know how to handle them.
+
+## RPC Errors
+
+Almost any RPC call can result in an RPC error
+(like `FLOOD_WAIT_%d`, `CHAT_ID_INVALID`, etc.).
+
+All these RPC errors are instances of `tl.RpcError`.
+
+Sadly, JavaScript does not provide a nice syntax to handle different kinds
+of errors, so you will need to write a bit of boilerplate:
+
+```ts
+try {
+ // your code //
+} catch (e) {
+ if (tl.RpcError.is(e, 'FLOOD_WAIT_%d')) {
+ // handle...
+ } else throw e
+}
+```
+
+::: tip
+mtcute automatically handles flood waits smaller than `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`).
diff --git a/docs/guide/intro/faq.md b/docs/guide/intro/faq.md
new file mode 100755
index 00000000..eeeb5e5d
--- /dev/null
+++ b/docs/guide/intro/faq.md
@@ -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):
+
+
+
+```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:
+
+
+
+## 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()
+ ]
+ }
+})
\ No newline at end of file
diff --git a/docs/guide/intro/mtproto-vs-bot-api.md b/docs/guide/intro/mtproto-vs-bot-api.md
new file mode 100755
index 00000000..a32ec9b4
--- /dev/null
+++ b/docs/guide/intro/mtproto-vs-bot-api.md
@@ -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:
+
+
+
+Apart from smaller overhead, using MTProto has many other advantages, including:
+
+| | Bot API | MTProto |
+|---|---|---|
+| Userbots | Only bots | Both bots and users.
+| Files | 20 MB download, 50 MB upload. | No limits (except global limit of 2000 MB)
+| Objects | Often brief and non-exhaustive | Exposes anything you can think of
+| Updates | Only a limited subset | Updates about virtually anything that had happened
+| Errors | Often non-informative (e.g. slow mode is the same as flood wait) and not machine-readable | Informative and simple to use
+| Version | Receives updates slower | Updates with the TL layer
+| Compatibility | Updates randomly, you have to prepare for the upcoming breaking changes.
Attempts to stay backwards-compatible, leading to weird hacks | You can stay on older version for as long as you need
+
+> **Note**: the above table assumes official Bot API instance, hosted at `api.telegram.org`.
+> Self-hosted and/or custom Bot API instances bypass some of these limitations.
+
+## Why not MTProto?
+
+Everything has its drawbacks though.
+
+Using mtcute instead of Bot API (or TDLib) for high-load bots might currently
+not be the best idea, since TDLib caches basically everything, while mtcute doesn't.
+This is our primary focus for the upcoming releases, though.
+
+If your bot is high-load, and you receive errors like 500 and 429, this
+definitely means a problem on mtcute side. Please
+[let us know](https://t.me/mt_cute), so we can
+investigate further.
+
+Another drawback is that due to mtcute being an enthusiast project,
+it does not offer the same level of robustness and support as Bot API,
+and is missing some features that Bot API has.
\ No newline at end of file
diff --git a/docs/guide/intro/sign-in.md b/docs/guide/intro/sign-in.md
new file mode 100755
index 00000000..714adf36
--- /dev/null
+++ b/docs/guide/intro/sign-in.md
@@ -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
+
+
+
+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.
\ No newline at end of file
diff --git a/docs/guide/intro/updates.md b/docs/guide/intro/updates.md
new file mode 100755
index 00000000..1ab14383
--- /dev/null
+++ b/docs/guide/intro/updates.md
@@ -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).
diff --git a/docs/guide/topics/conversation.md b/docs/guide/topics/conversation.md
new file mode 100755
index 00000000..39951c45
--- /dev/null
+++ b/docs/guide/topics/conversation.md
@@ -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.
+
+
+
+::: 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.
diff --git a/docs/guide/topics/files.md b/docs/guide/topics/files.md
new file mode 100755
index 00000000..efd84bff
--- /dev/null
+++ b/docs/guide/topics/files.md
@@ -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))
+```
diff --git a/docs/guide/topics/inline-mode.md b/docs/guide/topics/inline-mode.md
new file mode 100755
index 00000000..7b669ad9
--- /dev/null
+++ b/docs/guide/topics/inline-mode.md
@@ -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)
+```
diff --git a/docs/guide/topics/keyboards.md b/docs/guide/topics/keyboards.md
new file mode 100755
index 00000000..5bc38b4b
--- /dev/null
+++ b/docs/guide/topics/keyboards.md
@@ -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.
+
+
+
+```ts
+await tg.sendText('username', 'Awesome keyboard!', {
+ replyMarkup: BotKeyboard.reply([
+ [BotKeyboard.text('First button')],
+ [BotKeyboard.text('Second button')],
+ ])
+})
+```
+
+You can only use the following button types with reply keyboards:
+
+| Name | Type | Notes |
+| ------------------- | ---------------------------- | ----------------------- |
+| Text-only | `BotKeyboard.text` | |
+| Request contact | `BotKeyboard.requestContact` | only for private chats. |
+| Request geolocation | `BotKeyboard.requestGeo` | only for private chats. |
+| Request poll | `BotKeyboard.requestPoll` | only for private chats. |
+
+Using any other will result in an error by Telegram.
+
+You can also instruct the client to hide a previously
+sent reply keyboard:
+
+```ts
+await tg.sendText('username', 'No more keyboard :p', {
+ replyMarkup: BotKeyboard.hideReply()
+})
+```
+
+Or, ask the user to reply to this message with custom text:
+
+```ts
+await tg.sendText('username', 'What is your name?', {
+ replyMarkup: BotKeyboard.forceReply()
+})
+```
+
+
+## Inline keyboards
+
+Inline keyboard is a keyboard that is shown under the message.
+When user taps on some button, a client does some action that particular
+button instructs it to do.
+
+
+
+```ts
+await tg.sendText('username', 'Awesome keyboard!', {
+ replyMarkup: BotKeyboard.inline([
+ [BotKeyboard.callback('First button', 'btn:1')],
+ [BotKeyboard.callback('Second button', 'btn:2')],
+ ])
+})
+```
+
+You can only use the following button types with inline keyboards:
+
+| Name | Type | Notes |
+| -------------- | -------------------------- | ----------------------------------------------------------------------------------- |
+| Callback | `BotKeyboard.callback` | When clicked a callback query will be sent to the bot. |
+| URL | `BotKeyboard.url` | When clicked the client will open the given URL. |
+| Switch inline | `BotKeyboard.switchInline` | When clicked the client will open an inline query to this bot with the given query. |
+| "Play game" | `BotKeyboard.game` | Must be the first one, must be used with `InputMedia.game` as the media. |
+| "Pay" | `BotKeyboard.pay` | Must be the first one, must be used with `InputMedia.invoice` as the media. |
+| Seamless login | `BotKeyboard.urlAuth` | [Learn more](https://corefork.telegram.org/constructor/inputKeyboardButtonUrlAuth) |
+| WebView | `BotKeyboard.webView` | [Learn more](https://corefork.telegram.org/api/bots/webapps) |
+| Open user | `BotKeyboard.userProfile` | When clicked the client will open the given user's profile |
+| Request peer | `BotKeyboard.requestPeer` | When clicked the client will ask the user to choose a peer and will send a message with [`ActionPeerChosen`](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ActionPeerChosen.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.
+
+
+
+### 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.
diff --git a/docs/guide/topics/parse-modes.md b/docs/guide/topics/parse-modes.md
new file mode 100755
index 00000000..39112a96
--- /dev/null
+++ b/docs/guide/topics/parse-modes.md
@@ -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, ${msg.sender.username}`)
+})
+```
+
+**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`${user.displayName}`
+ const text = md`**Hello**, ${user}!`
+ ```
+- falsy value (i.e. `null`, `undefined`, `false`) - will be ignored
+
+### Unsafe interpolation
+
+In some cases, you may already have a string with entities, and want to parse it to entities.
+
+In this case, you can use the method as a function:
+
+```ts
+const text = 'Hello, **User**!'
+
+await tg.sendText('self', md(text))
+// or even
+await tg.sendText('self', md`${md(text)} What's new?`)
+```
+
+## Un-parsing
+
+Both HTML and Markdown parsers also provide an `unparse` method,
+which can be used to convert the message back to the original text:
+
+```ts
+import { html } from '@mtcute/html-parser'
+
+const msg = await tg.sendText('Hi, User!', { parseMode: 'html' })
+
+console.log(msg.text)
+// Hi, User!
+console.log(html.unparse())
+// Hi, User!
+```
diff --git a/docs/guide/topics/peers.md b/docs/guide/topics/peers.md
new file mode 100755
index 00000000..de2ed8c7
--- /dev/null
+++ b/docs/guide/topics/peers.md
@@ -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
+```
+
+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
+ }
+)
+```
\ No newline at end of file
diff --git a/docs/guide/topics/raw-api.md b/docs/guide/topics/raw-api.md
new file mode 100755
index 00000000..e7650152
--- /dev/null
+++ b/docs/guide/topics/raw-api.md
@@ -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. md\`\*\*Hello!**\`
),
+`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))
diff --git a/docs/guide/topics/storage.md b/docs/guide/topics/storage.md
new file mode 100755
index 00000000..2c7dbf25
--- /dev/null
+++ b/docs/guide/topics/storage.md
@@ -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
diff --git a/docs/guide/topics/transport.md b/docs/guide/topics/transport.md
new file mode 100755
index 00000000..546ba7b9
--- /dev/null
+++ b/docs/guide/topics/transport.md
@@ -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.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..f59e54f0
--- /dev/null
+++ b/docs/index.md
@@ -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
+---
+
+
\ No newline at end of file
diff --git a/docs/package.json b/docs/package.json
new file mode 100755
index 00000000..cdb8092a
--- /dev/null
+++ b/docs/package.json
@@ -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"
+}
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
new file mode 100644
index 00000000..74b9b9d5
--- /dev/null
+++ b/docs/pnpm-lock.yaml
@@ -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
diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml
new file mode 100644
index 00000000..f3c47d11
--- /dev/null
+++ b/docs/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+# empty workspace to prevent pnpm from installing docs deps unnecessarily
+packages:
+ - .
\ No newline at end of file
diff --git a/docs/public/assets/mtproto_vs_botapi.svg b/docs/public/assets/mtproto_vs_botapi.svg
new file mode 100644
index 00000000..d5dc791e
--- /dev/null
+++ b/docs/public/assets/mtproto_vs_botapi.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/docs/public/mtcute-logo.png b/docs/public/mtcute-logo.png
new file mode 100644
index 00000000..270eaa78
Binary files /dev/null and b/docs/public/mtcute-logo.png differ
diff --git a/docs/public/mtcute-logo.svg b/docs/public/mtcute-logo.svg
new file mode 100644
index 00000000..eaff0b1f
--- /dev/null
+++ b/docs/public/mtcute-logo.svg
@@ -0,0 +1,5 @@
+