chore: moved docs inside the main repo
All checks were successful
Build and deploy typedoc / build (push) Successful in 5m15s
All checks were successful
Build and deploy typedoc / build (push) Successful in 5m15s
Co-authored-by: Kamilla 'ova <me@kamillaova.dev> Co-authored-by: Alina Chebakova <chebakov05@gmail.com> Co-authored-by: Kravets <57632712+kravetsone@users.noreply.github.com> Co-authored-by: starkow <hello@starkow.dev> Co-authored-by: sireneva <150665887+sireneva@users.noreply.github.com>
This commit is contained in:
parent
9f3ef993c0
commit
690948b8b1
53 changed files with 7299 additions and 30 deletions
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"exclude": [
|
|
||||||
"../**/*.test.ts",
|
|
||||||
"../**/*.test-utils.ts",
|
|
||||||
"../**/__fixtures__/**"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,37 +1,37 @@
|
||||||
name: Docs
|
name: Build and deploy docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [master]
|
||||||
- master
|
paths:
|
||||||
pull_request:
|
- 'docs/**'
|
||||||
branches: [ master ]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pages
|
group: docs
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: node22
|
runs-on: node20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.forgejo/actions/init
|
- uses: pnpm/action-setup@v2
|
||||||
- name: Build docs
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
working-directory: docs
|
||||||
|
- name: Build with VitePress
|
||||||
|
working-directory: docs
|
||||||
run: |
|
run: |
|
||||||
pnpm run docs
|
pnpm run build
|
||||||
touch docs/.nojekyll
|
touch .vitepress/dist/.nojekyll
|
||||||
echo "ref.mtcute.dev" > docs/CNAME
|
echo mtcute.dev > .vitepress/dist/CNAME
|
||||||
echo "ignore-workspace-root-check=true" >> .npmrc
|
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
|
- name: Deploy
|
||||||
# do not run on forks and releases
|
# do not run on forks
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.actor == 'desu-bot'
|
if: github.repository == 'teidesu/mtcute'
|
||||||
uses: https://github.com/cloudflare/wrangler-action@v3
|
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
command: pages deploy docs --project-name=mtcute-apiref
|
command: pages deploy docs/.vitepress/dist --project-name=mtcute-docs
|
||||||
|
|
34
.forgejo/workflows/typedoc.yaml
Normal file
34
.forgejo/workflows/typedoc.yaml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name: Build and deploy typedoc
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: typedoc
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: node22
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.forgejo/actions/init
|
||||||
|
- name: Build docs
|
||||||
|
run: |
|
||||||
|
pnpm run docs
|
||||||
|
touch dist/typedoc/.nojekyll
|
||||||
|
echo "ref.mtcute.dev" > dist/typedoc/CNAME
|
||||||
|
echo "ignore-workspace-root-check=true" >> .npmrc
|
||||||
|
- name: Deploy
|
||||||
|
# only run on releases
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.actor == 'desu-bot'
|
||||||
|
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: pages deploy dist/typedoc --project-name=mtcute-apiref
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,9 +7,6 @@ private/
|
||||||
.vscode
|
.vscode
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# docs are generated in ci
|
|
||||||
docs
|
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
.rollup.cache
|
.rollup.cache
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
|
@ -69,6 +69,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typedoc: {
|
typedoc: {
|
||||||
|
out: 'dist/typedoc',
|
||||||
excludePackages: [
|
excludePackages: [
|
||||||
'@mtcute/tl',
|
'@mtcute/tl',
|
||||||
'@mtcute/create-bot',
|
'@mtcute/create-bot',
|
||||||
|
|
16
docs/.editorconfig
Normal file
16
docs/.editorconfig
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 0
|
||||||
|
trim_trailing_whitespace = false
|
13
docs/.gitignore
vendored
Executable file
13
docs/.gitignore
vendored
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
pids
|
||||||
|
logs
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
coverage/
|
||||||
|
run
|
||||||
|
dist
|
||||||
|
cache
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
|
.basement
|
||||||
|
config.local.js
|
||||||
|
basement_dist
|
13
docs/.vitepress/components/EmbedPost.vue
Normal file
13
docs/.vitepress/components/EmbedPost.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
|
||||||
|
const { isDark } = useData()
|
||||||
|
const { link, height } = defineProps<{
|
||||||
|
link: string
|
||||||
|
height?: number
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<iframe :src="`https://t.me/${link}?embed=1&color=d990c3&dark=${+isDark}`" :height="height"></iframe>
|
||||||
|
</template>
|
24
docs/.vitepress/components/Tag.vue
Executable file
24
docs/.vitepress/components/Tag.vue
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<span class="tag">
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Tag',
|
||||||
|
props: ['text'],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tag {
|
||||||
|
font-size: 0.4em;
|
||||||
|
font-weight: 400;
|
||||||
|
border: 1px solid var(--vp-c-default-3);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
45
docs/.vitepress/components/VImg.vue
Executable file
45
docs/.vitepress/components/VImg.vue
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<p class="img-wrap">
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
:alt="alt || caption"
|
||||||
|
:style="{'max-width': width ? width + 'px' : undefined}"
|
||||||
|
:class="{'img-adaptive': adaptive}"
|
||||||
|
>
|
||||||
|
<span v-if="caption">{{ caption || alt }}</span>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mediumZoom from 'medium-zoom'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VImg',
|
||||||
|
props: ['src', 'alt', 'caption', 'width', 'adaptive'],
|
||||||
|
mounted() {
|
||||||
|
mediumZoom(this.$el.querySelector('img'), {
|
||||||
|
margin: 24,
|
||||||
|
background: 'var(--vp-backdrop-bg-color)',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.img-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-wrap span {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .img-adaptive {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
</style>
|
132
docs/.vitepress/config.mts
Normal file
132
docs/.vitepress/config.mts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import { defineConfig, HeadConfig } from "vitepress";
|
||||||
|
import markdownItFootnotes from "markdown-it-footnote";
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default ({ mode }) => defineConfig({
|
||||||
|
title: "mtcute",
|
||||||
|
description: "mtcute documentation",
|
||||||
|
lastUpdated: true,
|
||||||
|
head: [
|
||||||
|
["meta", { name: "theme-color", content: "#e9a1d9" }],
|
||||||
|
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
|
||||||
|
],
|
||||||
|
['link', { rel: 'icon', href: '/mtcute-logo.png' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fredoka:wght@500&text=mtcute' }],
|
||||||
|
...(mode === 'production' ? [
|
||||||
|
['script', { async: '', src: 'https://zond.tei.su/script.js', 'data-website-id': '968f50a2-4cf8-4e31-9f40-1abd48ba2086' }] as HeadConfig
|
||||||
|
] : []),
|
||||||
|
],
|
||||||
|
transformHtml(code) {
|
||||||
|
if (mode !== 'production') return code
|
||||||
|
|
||||||
|
// this is a hack but whatever
|
||||||
|
return code.replace(
|
||||||
|
'<body>',
|
||||||
|
'<body><noscript><div><img src="https://tei.su/zond.php?website=968f50a2-4cf8-4e31-9f40-1abd48ba2086" style="position:absolute; left:-9999px;" alt="" /></div></noscript>'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
|
nav: [
|
||||||
|
{ text: "Guide", link: "/guide/" },
|
||||||
|
{ text: "Reference", link: "//ref.mtcute.dev" },
|
||||||
|
],
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: "github", link: "https://github.com/mtcute" },
|
||||||
|
{
|
||||||
|
icon: {
|
||||||
|
svg: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Telegram</title><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>`,
|
||||||
|
},
|
||||||
|
link: "https://t.me/mt_cute",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
search: {
|
||||||
|
provider: "local",
|
||||||
|
},
|
||||||
|
editLink: {
|
||||||
|
pattern: "https://github.com/mtcute/docs/edit/master/:path",
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
level: "deep",
|
||||||
|
},
|
||||||
|
|
||||||
|
sidebar: {
|
||||||
|
"/guide/": [
|
||||||
|
{
|
||||||
|
text: "Getting started",
|
||||||
|
items: [
|
||||||
|
{ text: "Quick start", link: "/guide/" },
|
||||||
|
{ text: "Signing in", link: "/guide/intro/sign-in" },
|
||||||
|
{ text: "Updates", link: "/guide/intro/updates" },
|
||||||
|
{ text: "Errors", link: "/guide/intro/errors" },
|
||||||
|
{
|
||||||
|
text: "MTProto vs Bot API",
|
||||||
|
link: "/guide/intro/mtproto-vs-bot-api",
|
||||||
|
},
|
||||||
|
{ text: "FAQ", link: "/guide/intro/faq" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Topics",
|
||||||
|
items: [
|
||||||
|
{ text: "Peers", link: "/guide/topics/peers" },
|
||||||
|
{ text: "Storage", link: "/guide/topics/storage" },
|
||||||
|
{ text: "Transport", link: "/guide/topics/transport" },
|
||||||
|
{ text: "Parse modes", link: "/guide/topics/parse-modes" },
|
||||||
|
{ text: "Files", link: "/guide/topics/files" },
|
||||||
|
{ text: "Keyboards", link: "/guide/topics/keyboards" },
|
||||||
|
{ text: "Inline mode", link: "/guide/topics/inline-mode" },
|
||||||
|
{ text: "Conversation", link: "/guide/topics/conversation" },
|
||||||
|
{ text: "Raw API", link: "/guide/topics/raw-api" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Dispatcher",
|
||||||
|
items: [
|
||||||
|
{ text: "Intro", link: "/guide/dispatcher/intro" },
|
||||||
|
{ text: "Handlers", link: "/guide/dispatcher/handlers" },
|
||||||
|
{ text: "Filters", link: "/guide/dispatcher/filters" },
|
||||||
|
{
|
||||||
|
text: "Groups & Propagation",
|
||||||
|
link: "/guide/dispatcher/groups-propagation",
|
||||||
|
},
|
||||||
|
{ text: "Errors", link: "/guide/dispatcher/errors" },
|
||||||
|
{ text: "Middlewares", link: "/guide/dispatcher/middlewares" },
|
||||||
|
{ text: "Inline mode", link: "/guide/dispatcher/inline-mode" },
|
||||||
|
{ text: "State", link: "/guide/dispatcher/state" },
|
||||||
|
{ text: "Rate limit", link: "/guide/dispatcher/rate-limit" },
|
||||||
|
{ text: "Child Dispatchers", link: "/guide/dispatcher/children" },
|
||||||
|
{ text: "Scenes", link: "/guide/dispatcher/scenes" },
|
||||||
|
{ text: "Dependency Injection", link: "/guide/dispatcher/di" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Advanced',
|
||||||
|
items: [
|
||||||
|
{ text: "Tree-shaking", link: "/guide/advanced/treeshaking" },
|
||||||
|
{ text: "Workers", link: "/guide/advanced/workers" },
|
||||||
|
{ text: "Converting sessions", link: "/guide/advanced/session-convert" },
|
||||||
|
{ text: "Network middlewares", link: "/guide/advanced/net-middlewares" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: "mtcute is not affiliated with Telegram.",
|
||||||
|
copyright:
|
||||||
|
'This documentation is licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a><br/>' +
|
||||||
|
'Logo by <a href="//t.me/AboutTheDot">@dotvhs</a><br/>' +
|
||||||
|
'© Copyright 2021-present, <a href="//github.com/teidesu">teidesu</a> ❤️',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
config: (md) => {
|
||||||
|
md.use(markdownItFootnotes);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
70
docs/.vitepress/theme/Layout.vue
Normal file
70
docs/.vitepress/theme/Layout.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import { nextTick, provide } from 'vue'
|
||||||
|
|
||||||
|
const { isDark } = useData()
|
||||||
|
|
||||||
|
const enableTransitions = () =>
|
||||||
|
'startViewTransition' in document &&
|
||||||
|
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||||
|
|
||||||
|
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
|
||||||
|
if (!enableTransitions()) {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipPath = [
|
||||||
|
`circle(0px at ${x}px ${y}px)`,
|
||||||
|
`circle(${Math.hypot(
|
||||||
|
Math.max(x, innerWidth - x),
|
||||||
|
Math.max(y, innerHeight - y)
|
||||||
|
)}px at ${x}px ${y}px)`
|
||||||
|
]
|
||||||
|
|
||||||
|
await document.startViewTransition(async () => {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
await nextTick()
|
||||||
|
}).ready
|
||||||
|
|
||||||
|
document.documentElement.animate(
|
||||||
|
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
|
||||||
|
{
|
||||||
|
duration: 300,
|
||||||
|
easing: 'ease-in',
|
||||||
|
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DefaultTheme.Layout />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
.dark::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root),
|
||||||
|
.dark::view-transition-old(root) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSwitchAppearance {
|
||||||
|
width: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSwitchAppearance .check {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
30
docs/.vitepress/theme/index.ts
Normal file
30
docs/.vitepress/theme/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import type { Theme } from 'vitepress'
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
|
||||||
|
import vitepressBackToTop from 'vitepress-plugin-back-to-top'
|
||||||
|
import 'vitepress-plugin-back-to-top/dist/style.css'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import EmbedPost from '../components/EmbedPost.vue'
|
||||||
|
// @ts-ignore
|
||||||
|
import VImg from '../components/VImg.vue'
|
||||||
|
// @ts-ignore
|
||||||
|
import Tag from '../components/Tag.vue'
|
||||||
|
// @ts-ignore
|
||||||
|
import Layout from './Layout.vue'
|
||||||
|
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout,
|
||||||
|
enhanceApp({ app, router, siteData }) {
|
||||||
|
app.component('v-img', VImg)
|
||||||
|
app.component('EmbedPost', EmbedPost)
|
||||||
|
app.component('Tag', Tag)
|
||||||
|
vitepressBackToTop({
|
||||||
|
threshold: 300
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} satisfies Theme
|
192
docs/.vitepress/theme/style.css
Normal file
192
docs/.vitepress/theme/style.css
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*
|
||||||
|
* Each colors have exact same color scale system with 3 levels of solid
|
||||||
|
* colors with different brightness, and 1 soft color.
|
||||||
|
*
|
||||||
|
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||||
|
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||||
|
*
|
||||||
|
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||||
|
*
|
||||||
|
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||||
|
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||||
|
* top of it.
|
||||||
|
*
|
||||||
|
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||||
|
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||||
|
* on top of it.
|
||||||
|
*
|
||||||
|
* The soft color must be semi transparent alpha channel. This is crucial
|
||||||
|
* because it allows adding multiple "soft" colors on top of each other
|
||||||
|
* to create a accent, such as when having inline code block inside
|
||||||
|
* custom containers.
|
||||||
|
*
|
||||||
|
* - `default`: The color used purely for subtle indication without any
|
||||||
|
* special meanings attched to it such as bg color for menu hover state.
|
||||||
|
*
|
||||||
|
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||||
|
* brand theme, etc.
|
||||||
|
*
|
||||||
|
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||||
|
* brand color for this by default.
|
||||||
|
*
|
||||||
|
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||||
|
* container, badges, etc.
|
||||||
|
*
|
||||||
|
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||||
|
* in custom container, badges, etc.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-c-default-1: var(--vp-c-gray-1);
|
||||||
|
--vp-c-default-2: var(--vp-c-gray-2);
|
||||||
|
--vp-c-default-3: var(--vp-c-gray-3);
|
||||||
|
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||||
|
|
||||||
|
--vp-c-brand-1: #b975a5;
|
||||||
|
--vp-c-brand-2: #d990c3;
|
||||||
|
--vp-c-brand-3: #f69ddc;
|
||||||
|
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||||
|
|
||||||
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
|
||||||
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-button-brand-border: transparent;
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
|
--vp-button-brand-hover-border: transparent;
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
|
--vp-button-brand-active-border: transparent;
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#f69ddc 60%,
|
||||||
|
#c890d6
|
||||||
|
);
|
||||||
|
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#c890d6 50%,
|
||||||
|
#f69ddc 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(44px) opacity(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-border: transparent;
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFooter a {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container iframe {
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-block.tip code {
|
||||||
|
color: var(--vp-c-brand-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a,
|
||||||
|
.custom-block.tip a {
|
||||||
|
color: var(--vp-c-brand-2);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:hover,
|
||||||
|
.custom-block.tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:active,
|
||||||
|
.custom-block.tip a:active {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc code {
|
||||||
|
word-break: keep-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-zoom-overlay,
|
||||||
|
.medium-zoom-image--opened {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .name .clip {
|
||||||
|
font-family: Fredoka, sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .go-to-top {
|
||||||
|
background-color: var(--vp-c-bg-elv);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .go-to-top:hover {
|
||||||
|
background-color: var(--vp-c-bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-footnote {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
11
docs/README.md
Normal file
11
docs/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# mtcute docs
|
||||||
|
|
||||||
|
Documentation website for [mtcute](https://github.com/mtcute/mtcute),
|
||||||
|
built with [VitePress](https://vitepress.dev/).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run dev
|
||||||
|
```
|
154
docs/guide/advanced/net-middlewares.md
Normal file
154
docs/guide/advanced/net-middlewares.md
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
# Network middlewares <Tag text="v0.16.0+" />
|
||||||
|
|
||||||
|
In some cases it may make sense to intercept *all* outgoing requests and control the request flow.
|
||||||
|
|
||||||
|
## Default middlewares
|
||||||
|
|
||||||
|
By default, mtcute uses two middlewares: flood-waiter and internal errors handler.
|
||||||
|
The combined default middleware is exported in `networkMiddlewares.basic`, and can be configured as follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
...,
|
||||||
|
network: {
|
||||||
|
middlewares: networkMiddlewares.basic({
|
||||||
|
floodWaiter: { maxWait: 5000, maxRetries: 5 },
|
||||||
|
internalErrors: { maxRetries: 5 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Flood-waiter and internal errors handler middlewares themselves are exported under
|
||||||
|
`networkMiddlewares.floodWaiter` and `networkMiddlewares.internalErrorsHandler` respectively.
|
||||||
|
|
||||||
|
## Writing middlewares
|
||||||
|
|
||||||
|
Middleware is simply an async function that takes `ctx` and `next` as arguments.
|
||||||
|
|
||||||
|
The `ctx` object contains information about the RPC call, including the request itself and
|
||||||
|
any additional parameters that were passed along, and `next` function can be used to call the
|
||||||
|
next middleware in the chain, returning the call result (or an [error](#errors-in-middlewares)):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
|
||||||
|
if (ctx.request._ === 'help.getConfig') {
|
||||||
|
return myConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
If you are familiar with grammY/telegraf or koa middlewares,
|
||||||
|
you might find the `ctx, next` syntax familiar.
|
||||||
|
Indeed, these middlewares were heavily inspired by them.
|
||||||
|
|
||||||
|
However, they work slightly different here, as the task is slightly different too.
|
||||||
|
|
||||||
|
Unlike grammY-style middlewares, `next` *can* be called multiple times,
|
||||||
|
and the last pseudo-"middleware" in the chain will actually execute
|
||||||
|
the request contained in the `ctx` (instead of being a no-op).
|
||||||
|
|
||||||
|
And because of that, `ctx` is always passed explicitly,
|
||||||
|
allowing to execute multiple different requests from a single middleware.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Errors in middlewares
|
||||||
|
|
||||||
|
To improve performance, RPC errors in middlewares are monadic, meaning that an RPC error is
|
||||||
|
considered a valid result.
|
||||||
|
|
||||||
|
To check if the call resulted in an error, you can use `isTlRpcError` handler:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
|
||||||
|
const res = await next(ctx)
|
||||||
|
|
||||||
|
if (isTlRpcError(res) && res.errorMessage === 'PEER_ID_INVALID') {
|
||||||
|
logPeerIdInvalid(ctx.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `networkMiddlewares.onRpcError` helper to create a middleware that only handles RPC errors:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const client = new TelegramClient({
|
||||||
|
...,
|
||||||
|
network: {
|
||||||
|
middlewares: [
|
||||||
|
networkMiddlewares.onRpcError(async (ctx, error) => {
|
||||||
|
if (error.errorMessage === 'PEER_ID_INVALID') {
|
||||||
|
logPeerIdInvalid(ctx.request)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
networkMiddlewares.basic()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying request
|
||||||
|
|
||||||
|
In some cases, it might make sense to modify the request before sending.
|
||||||
|
|
||||||
|
One way to do so is to overwrite the `ctx` fields:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
|
||||||
|
if (ctx.request._ === 'users.getFullUser') {
|
||||||
|
ctx.request.id = { _: 'inputUserSelf' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can construct your own context:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myMiddleware: RpcCallMiddleware = async (ctx, next) => {
|
||||||
|
if (ctx.request._ === 'users.getFullUser') {
|
||||||
|
return next({
|
||||||
|
manager: ctx.manager,
|
||||||
|
params: ctx.params,
|
||||||
|
request: {
|
||||||
|
_: 'users.getFullUser',
|
||||||
|
id: { _: 'inputUserSelf' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Applying middlewares
|
||||||
|
|
||||||
|
Once you're done writing your middleware, you need to connect it to the client.
|
||||||
|
That's done by passing an array to the `middlewares` option, like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
...,
|
||||||
|
network: {
|
||||||
|
middlewares: [
|
||||||
|
myMiddleware,
|
||||||
|
myOtherMiddleware,
|
||||||
|
// You'll probably also want to include all the default
|
||||||
|
// middlewares, as passing this option overrides them.
|
||||||
|
...networkMiddlewares.basic()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
**Middleware order matters**, which is why we include the basic middlewares last —
|
||||||
|
we want `myMiddleware` and `myOtherMiddleware` to also benefit from them
|
||||||
|
(i.e. have flood waits and internal errors handled)
|
||||||
|
:::
|
118
docs/guide/advanced/session-convert.md
Normal file
118
docs/guide/advanced/session-convert.md
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
# Converting sessions
|
||||||
|
|
||||||
|
If you're coming from another library, you might already have a session
|
||||||
|
lying around.
|
||||||
|
|
||||||
|
mtcute provides a way to convert sessions from some other libraries
|
||||||
|
to mtcute's format in `@mtcute/convert` package.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Please, **only use this to convert your own sessions**.
|
||||||
|
|
||||||
|
**DO NOT** use this to convert stolen sessions or sessions you don't own.
|
||||||
|
Please be a decent person.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## [Telethon v1.x](https://github.com/LonamiWebs/Telethon)
|
||||||
|
|
||||||
|
> Telethon v2 seems to have removed the ability to export sessions,
|
||||||
|
> so it's currently not supported
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { convertFromTelethonSession } from '@mtcute/convert'
|
||||||
|
|
||||||
|
const client = new TelegramClient({ ... })
|
||||||
|
await client.importSession(convertFromTelethonSession("..."))
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Pyrogram](https://github.com/pyrogram/pyrogram)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { convertFromPyrogramSession } from '@mtcute/convert'
|
||||||
|
|
||||||
|
const client = new TelegramClient({ ... })
|
||||||
|
await client.importSession(convertFromPyrogramSession("..."))
|
||||||
|
```
|
||||||
|
|
||||||
|
## [GramJS](https://github.com/gram-js/gramjs)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { convertFromGramjsSession } from '@mtcute/convert'
|
||||||
|
|
||||||
|
const client = new TelegramClient({ ... })
|
||||||
|
await client.importSession(convertFromGramjsSession("..."))
|
||||||
|
```
|
||||||
|
|
||||||
|
## [MTKruto](https://github.com/MTKruto/MTKruto)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { convertFromMtkrutoSession } from '@mtcute/convert'
|
||||||
|
|
||||||
|
const client = new TelegramClient({ ... })
|
||||||
|
await client.importSession(convertFromMtkrutoSession("..."))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backwards
|
||||||
|
|
||||||
|
If you need to convert a session from mtcute to another library, you can use the `convertTo*` functions instead:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log(convertToTelethonSession(await client.exportSession()))
|
||||||
|
```
|
||||||
|
|
||||||
|
## String-to-string
|
||||||
|
|
||||||
|
Once converted, you can use `writeStringSession` to convert the session to a string:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log(writeStringSession(convertFromTelethonSession("...")))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual
|
||||||
|
|
||||||
|
If your library is not supported, you can still convert the session manually.
|
||||||
|
|
||||||
|
In the most simple case, you'll only need `auth_key` and data center information:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dc = {
|
||||||
|
id: 2,
|
||||||
|
ipAddress: '149.154.167.41',
|
||||||
|
port: 443,
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.importSession({
|
||||||
|
version: 3,
|
||||||
|
testMode: false,
|
||||||
|
primaryDcs: { main: dc, media: dc },
|
||||||
|
authKey: new Uint8Array([ /* ... */ ]),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data center information
|
||||||
|
|
||||||
|
If you only know DC ID and not the IP address, you can use the mappings from `@mtcute/convert` to resolve it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { DC_MAPPING_PROD } from '@mtcute/convert'
|
||||||
|
|
||||||
|
const dc = DC_MAPPING_PROD[2]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't know such information at all, you can just always use the DC 2 (as above), and mtcute will handle the rest
|
||||||
|
|
||||||
|
### User information
|
||||||
|
|
||||||
|
If you happen to know some information about the user logged in, it might help to provide it as well:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await client.importSession({
|
||||||
|
...,
|
||||||
|
self: {
|
||||||
|
userId: 777000,
|
||||||
|
isBot: false,
|
||||||
|
isPremium: false,
|
||||||
|
usernames: [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
34
docs/guide/advanced/treeshaking.md
Normal file
34
docs/guide/advanced/treeshaking.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Tree-shaking
|
||||||
|
|
||||||
|
Being a ESM-first library, mtcute supports tree-shaking out of the box.
|
||||||
|
|
||||||
|
This means that you can import only the parts of the library that you need,
|
||||||
|
and the bundler will remove all the unused code.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To start using tree-shaking, there are a few things to keep in mind:
|
||||||
|
- Do not use `TelegramClient`. Use `BaseTelegramClient` instead, and import the needed methods.
|
||||||
|
|
||||||
|
For example, instead of this:
|
||||||
|
```ts
|
||||||
|
import { TelegramClient } from '@mtcute/web'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({ ... })
|
||||||
|
|
||||||
|
await tg.sendText(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
you should use this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BaseTelegramClient } from '@mtcute/web'
|
||||||
|
import { sendText } from '@mtcute/web/methods.js'
|
||||||
|
|
||||||
|
const tg = new BaseTelegramClient({ ... })
|
||||||
|
|
||||||
|
await sendText(tg, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
- TL serialization is currently not tree-shakeable, because it is done via a global map of constructors.
|
||||||
|
There's no ETA on when (or whether at all) this will be changed, so there *will* be ~300 KB of non-shakeable code.
|
109
docs/guide/advanced/workers.md
Normal file
109
docs/guide/advanced/workers.md
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# Workers
|
||||||
|
|
||||||
|
To facilitate parallel processing and avoid blocking the main thread, mtcute
|
||||||
|
supports extractnig the heavy lifting to the workers. This is especially useful
|
||||||
|
in the browser, where the main thread is often busy with rendering and other tasks.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Workers support is still experimental and may have some rough edges.
|
||||||
|
If something doesn't work in a worker, but works when used directly, please open an issue.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
`@mtcute/web` package exports a `TelegramWorker` and `TelegramWorkerPort` classes,
|
||||||
|
which can be used to create workers and communicate with them.
|
||||||
|
|
||||||
|
To create a worker, use the `TelegramWorker` class:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BaseTelegramClient, TelegramWorker } from '@mtcute/web'
|
||||||
|
|
||||||
|
const tg = new BaseTelegramClient({
|
||||||
|
apiId: 123456,
|
||||||
|
apiHash: '...',
|
||||||
|
})
|
||||||
|
|
||||||
|
new TelegramWorker({
|
||||||
|
client: tg,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To communicate with the worker, use the `TelegramWorkerPort` class and pass an instance of
|
||||||
|
`Worker` (or `SharedWorker`) to it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TelegramWorkerPort } from '@mtcute/web'
|
||||||
|
|
||||||
|
const port = new TelegramWorkerPort({
|
||||||
|
worker: new Worker(
|
||||||
|
new URL('./worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node.js
|
||||||
|
|
||||||
|
On the surface, the API is largely the same, but is slightly different under the hood
|
||||||
|
and uses `worker_threads` instead of web workers.
|
||||||
|
|
||||||
|
The worker is created the same way, but using `TelegramWorker` class from `@mtcute/node`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BaseTelegramClient, TelegramWorker } from '@mtcute/node'
|
||||||
|
|
||||||
|
const tg = new BaseTelegramClient({
|
||||||
|
apiId: 123456,
|
||||||
|
apiHash: '...',
|
||||||
|
})
|
||||||
|
|
||||||
|
new TelegramWorker({
|
||||||
|
client: tg,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, to communicate with the worker, use the `TelegramWorkerPort` class and pass an instance of `Worker` to it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Worker } from 'worker_threads'
|
||||||
|
import { TelegramWorkerPort } from '@mtcute/node'
|
||||||
|
|
||||||
|
const port = new TelegramWorkerPort({
|
||||||
|
worker: new Worker(
|
||||||
|
new URL('./worker.js', import.meta.url),
|
||||||
|
{ type: 'module' },
|
||||||
|
),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`TelegramWorkerPort` is a drop-in replacement for `BaseTelegramClient`, and since it
|
||||||
|
implements `ITelegramClient`, you can pass it to any method that expects a client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { sendText } from '@mtcute/web/methods.js'
|
||||||
|
|
||||||
|
await sendText(port, 'me', 'Hello from worker!')
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can pass the port as a cliant to `TelegramClient`
|
||||||
|
to bind it to all methods (not recommended in browser, see [Tree-shaking](./treeshaking.md)):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({ client: port })
|
||||||
|
|
||||||
|
await tg.sendText('me', 'Hello from worker!')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other runtimes
|
||||||
|
|
||||||
|
In other runtimes it may also make sense to use workers.
|
||||||
|
If your runtime supports web workers, you can use the `@mtcute/web` package to create workers - it should work just fine.
|
||||||
|
|
||||||
|
Otherwise, Please refer to
|
||||||
|
[Web](https://github.com/mtcute/mtcute/blob/master/packages/web/src/worker.ts)/[Node.js](https://github.com/mtcute/mtcute/blob/master/packages/node/src/worker.ts)
|
||||||
|
for the platform-specific worker implementations, and use them as a reference to create your own worker implementation.
|
||||||
|
|
81
docs/guide/dispatcher/children.md
Executable file
81
docs/guide/dispatcher/children.md
Executable file
|
@ -0,0 +1,81 @@
|
||||||
|
# Child Dispatchers
|
||||||
|
|
||||||
|
Child dispatchers is an elegant way to divide logic in your application.
|
||||||
|
|
||||||
|
Child dispatcher is an isolated dispatcher with its own dispatcher groups,
|
||||||
|
propagation and handlers that do not interfere with other dispatchers (the only
|
||||||
|
exception being `StopChildrenPropagation`)
|
||||||
|
|
||||||
|
## Creating a child
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const child = Dispatcher.child()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can register your handlers to `child`.
|
||||||
|
|
||||||
|
## Adding a child
|
||||||
|
|
||||||
|
Dispatcher on its own does nothing, it needs to be bound to a parent
|
||||||
|
to become a child dispatcher. That is done simply by calling `addChild`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.addChild(child)
|
||||||
|
```
|
||||||
|
|
||||||
|
Only dispatchers that are not bound to a Client can be used as a child.
|
||||||
|
This also means that a dispatcher can only be a child to one dispatcher,
|
||||||
|
i.e. the following code **will not work**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.addChild(child)
|
||||||
|
otherDp.addChild(child) // error!
|
||||||
|
```
|
||||||
|
|
||||||
|
However, you can use `.clone()` method to make this work:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.addChild(child)
|
||||||
|
otherDp.addChild(child.clone()) // ok
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing a child
|
||||||
|
|
||||||
|
When building some kind of modular architecture, it is useful to also
|
||||||
|
remove a child dispatcher. Luckily, it is just as easy:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.removeChild(child)
|
||||||
|
```
|
||||||
|
|
||||||
|
Do note, however, that if you are using a cloned dispatcher,
|
||||||
|
calling `removeChild` on the original dispatcher will do nothing.
|
||||||
|
Instead, you have to store the reference to the cloned dispatcher:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const childClone = child.clone()
|
||||||
|
otherDp.addChild(childClone)
|
||||||
|
|
||||||
|
// later
|
||||||
|
otherDp.removeChild(childClone)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending
|
||||||
|
|
||||||
|
Instead of using child dispatchers, you can extend the existing dispatcher
|
||||||
|
with another one:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.extend(child)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that in this case, `child` **will not** be isolated, and its handler
|
||||||
|
groups, children, scenes, etc. will be merged to the original dispatcher.
|
||||||
|
If `child` contains scenes with already registered names, they will be
|
||||||
|
overwritten.
|
||||||
|
|
||||||
|
Extending will not work if the child is using a custom storage or
|
||||||
|
a custom key delegate.
|
||||||
|
|
||||||
|
Using a dispatcher after it was `.extend()`-ed into another dispatcher
|
||||||
|
is undefined behaviour and should be avoided.
|
43
docs/guide/dispatcher/di.md
Normal file
43
docs/guide/dispatcher/di.md
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Dependency Injection
|
||||||
|
|
||||||
|
When scaling up your bot to multiple files, you may find it useful to inject dependencies
|
||||||
|
into the children dispatchers instead of having to pass them around manually.
|
||||||
|
|
||||||
|
`@mtcute/dispatcher` provides a simple service locator that you can use to inject dependencies:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// for typescript, you need to declare the dependencies
|
||||||
|
declare module '@mtcute/dispatcher' {
|
||||||
|
interface DispatcherDependencies {
|
||||||
|
db: Database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a root dispatcher
|
||||||
|
const dp = Dispatcher.for(tg)
|
||||||
|
|
||||||
|
// inject the database
|
||||||
|
dp.inject('db', new Database())
|
||||||
|
// or
|
||||||
|
dp.inject({ db: new Database() })
|
||||||
|
|
||||||
|
// and then add a child dispatcher
|
||||||
|
import { childDispatcher } from './file2'
|
||||||
|
dp.addChild(childDispatcher)
|
||||||
|
|
||||||
|
// file2.ts
|
||||||
|
const dp = Dispatcher.child()
|
||||||
|
|
||||||
|
dp.onNewMessage(async (ctx) => {
|
||||||
|
// the dependencies are available in dp.deps
|
||||||
|
const db = dp.deps.db
|
||||||
|
await db.saveMessage(ctx.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const childDispatcher = dp
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
You can only inject dependencies into the root dispatcher (the one created with `Dispatcher.for`),
|
||||||
|
and they will be available in *all* children dispatchers.
|
||||||
|
:::
|
142
docs/guide/dispatcher/errors.md
Executable file
142
docs/guide/dispatcher/errors.md
Executable file
|
@ -0,0 +1,142 @@
|
||||||
|
# Handling errors
|
||||||
|
|
||||||
|
We've already briefly touched handling errors
|
||||||
|
in dispatchers in the Getting Started section,
|
||||||
|
but let's dive a bit deeper.
|
||||||
|
|
||||||
|
## Registering a handler
|
||||||
|
|
||||||
|
An error handler for a Dispatcher is simply a function
|
||||||
|
that is called whenever an error is thrown inside
|
||||||
|
one of the handlers.
|
||||||
|
|
||||||
|
For convenience, that function has access to the error itself,
|
||||||
|
the parsed update and the [state](./state) (if applicable):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.command('do_stuff'),
|
||||||
|
async (msg) => {
|
||||||
|
throw new Error('Some error')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onError(async (error, update, state) => {
|
||||||
|
if (update.name === 'new_message') {
|
||||||
|
await update.data.replyText(`Error: ${error.message}`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Error handler function is expected to return `boolean` indicating whether
|
||||||
|
the update was handled. If it was not, it will be propagated to Client.
|
||||||
|
|
||||||
|
`update` is an object that contains 2 fields: `name` and `data`.
|
||||||
|
|
||||||
|
`name` is simply update name (`new_message`, `edit_message`, etc.;
|
||||||
|
see [Handlers](handlers.html)), and `data` is the respective object.
|
||||||
|
|
||||||
|
|
||||||
|
## What errors are not handled
|
||||||
|
|
||||||
|
Errors inside [Raw update handlers](handlers.html#raw-updates) are not handled
|
||||||
|
by the error handler. Any other errors within the same dispatcher
|
||||||
|
(both handlers and filters) are handled by it.
|
||||||
|
|
||||||
|
## Propagation
|
||||||
|
|
||||||
|
### To Client
|
||||||
|
|
||||||
|
If the error handler is not registered, throws an error or returns `false`,
|
||||||
|
the error is propagated to Client's [error handler](../intro/errors.html#client-errors).
|
||||||
|
Obviously, in Client's error handler you won't have access to the update
|
||||||
|
that caused this error:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.command('do_stuff'),
|
||||||
|
async (msg) => {
|
||||||
|
throw new Error('Some error')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tg.onError((err) => {
|
||||||
|
// will be called since there's no `dp.onError`
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Within the Dispatcher
|
||||||
|
|
||||||
|
When an error is thrown by one of the handlers, propagation within this
|
||||||
|
dispatcher stops (the same way as if it returned `StopPropagation`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.command('do_stuff'),
|
||||||
|
async (msg) => {
|
||||||
|
throw new Error('Some error')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
async (msg) => {
|
||||||
|
// will not reach
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### To parent/children
|
||||||
|
|
||||||
|
Errors are **not** propagated to parent dispatcher or to any of the children
|
||||||
|
dispatchers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dp = new Dispatcher(tg)
|
||||||
|
const dp1 = new Dispatcher()
|
||||||
|
const dp2 = new Dispatcher()
|
||||||
|
|
||||||
|
dp.addChild(dp1)
|
||||||
|
dp1.addChild(dp2)
|
||||||
|
|
||||||
|
// dp --child--> dp1 --child--> dp2
|
||||||
|
|
||||||
|
dp.onError(() => console.log('DP caught error'))
|
||||||
|
dp1.onError(() => console.log('DP1 caught error'))
|
||||||
|
dp2.onError(() => console.log('DP2 caught error'))
|
||||||
|
|
||||||
|
dp1.onNewMessage(() => { throw new Error() })
|
||||||
|
|
||||||
|
// Only "DP1 caught error" will ever be printed
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if you need that behaviour, you can use `propagateErrorToParent`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dp = new Dispatcher(tg)
|
||||||
|
const dp1 = new Dispatcher()
|
||||||
|
const dp2 = new Dispatcher()
|
||||||
|
|
||||||
|
dp.addChild(dp1)
|
||||||
|
dp1.addChild(dp2)
|
||||||
|
|
||||||
|
// dp --child--> dp1 --child--> dp2
|
||||||
|
|
||||||
|
dp.onError(() => console.log('DP caught error'))
|
||||||
|
dp1.onError(() => {
|
||||||
|
console.log('DP1 caught error')
|
||||||
|
|
||||||
|
return dp1.propagateErrorToParent(...arguments)
|
||||||
|
})
|
||||||
|
dp2.onError(() => console.log('DP2 caught error'))
|
||||||
|
|
||||||
|
dp1.onNewMessage(() => { throw new Error() })
|
||||||
|
|
||||||
|
// "DP1 caught error" and "DP caught error" will be printed for each new message
|
||||||
|
```
|
||||||
|
|
||||||
|
|
331
docs/guide/dispatcher/filters.md
Executable file
331
docs/guide/dispatcher/filters.md
Executable file
|
@ -0,0 +1,331 @@
|
||||||
|
# Filters
|
||||||
|
|
||||||
|
Filters is a powerful concept that allows handlers to only process some of
|
||||||
|
the updates and not every single one.
|
||||||
|
|
||||||
|
mtcute comes with a lot of filters for different cases, and you can
|
||||||
|
write your own as well.
|
||||||
|
|
||||||
|
## Common filters
|
||||||
|
|
||||||
|
For the full reference, see `filters` namespace in the [API reference](https://ref.mtcute.dev/modules/_mtcute_dispatcher.filters.html)
|
||||||
|
|
||||||
|
For many updates, `filters.userId` is supported which filters by user ID(s)
|
||||||
|
that issued the update (i.e. message sender, poll voter, etc.):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.userId(12345678),
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
There's also `filters.chatId` that checks for the chat ID instead:
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.chatId(-100123456789),
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`filters.chat` allows you to filter by chat type(s):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.chat('private'),
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For every media type, there's a filter that will only match that media type:
|
||||||
|
`filters.photo`, `filters.video`, etc:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.photo,
|
||||||
|
async (msg) => {
|
||||||
|
await msg.replyText('Great photo though')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For service messages, there's `filters.action` that allows filtering by
|
||||||
|
service message action type(s):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.action('chat_created'),
|
||||||
|
async (msg) => {
|
||||||
|
await msg.answerText(`${msg.user.mention()} created ${msg.action.title}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
There are also `filters.command` and `filters.regex` that add information
|
||||||
|
about the match into the update object:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.command('start'),
|
||||||
|
async (msg) => {
|
||||||
|
if (msg.command[1] === 'from_inline') {
|
||||||
|
await msg.answerText('Thanks for using inline mode!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.regex(/^I'?m (\S+)/i),
|
||||||
|
async (msg) => {
|
||||||
|
await msg.replyText(`Hi ${msg.match[1]}, I'm Dad!`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For ChatMember updates, you can use `filters.chatMember` to filter by
|
||||||
|
change type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChatMemberUpdate(
|
||||||
|
filters.chatMember('joined'),
|
||||||
|
async (upd: ChatMemberUpdate) => {
|
||||||
|
await upd.chat.sendText(`${upd.user.mention()}, welcome to the chat!`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `filters.chatMemberSelf` to filter for actions that
|
||||||
|
were issued by the current user:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChatMemberUpdate(
|
||||||
|
filters.and(
|
||||||
|
filters.chatMemberSelf,
|
||||||
|
filters.chatMember('joined'),
|
||||||
|
),
|
||||||
|
async (upd: ChatMemberUpdate) => {
|
||||||
|
await addChatToDatabase(upd.chat)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type modification
|
||||||
|
|
||||||
|
Some filters like `filters.photo` only match in case `msg.media` is a `Photo`,
|
||||||
|
and it makes sense to make the handler aware of that and avoid redundant checks
|
||||||
|
in your code.
|
||||||
|
|
||||||
|
This is true for most of the built-in filters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo | Video | ... | null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.media,
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo | Video | ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.photo,
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Combining filters
|
||||||
|
|
||||||
|
Filters on their own are already pretty powerful, but you can also
|
||||||
|
combine and negate them.
|
||||||
|
|
||||||
|
This also modifies the [type modification](#type-modification) accordingly.
|
||||||
|
|
||||||
|
### Negating
|
||||||
|
|
||||||
|
To negate a filter, use `filters.not`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.photo,
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.not(filters.photo),
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Exclude<MessageMedia, Photo>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logical addition
|
||||||
|
|
||||||
|
Logical addition (i.e. OR operator) is supported with `filters.or`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.or(filters.video, filters.photo),
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo | Video
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logical multiplication
|
||||||
|
|
||||||
|
Logical multiplication (i.e. AND operator) is supported with `filters.and`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.and(filters.chat('private'), filters.photo),
|
||||||
|
async (msg) => {
|
||||||
|
// msg.media is Photo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom filters
|
||||||
|
|
||||||
|
Sometimes pre-existing filters are just not enough. Then, you
|
||||||
|
can write a custom filter.
|
||||||
|
|
||||||
|
### Simple custom filter
|
||||||
|
|
||||||
|
Under the hood, filters are simply functions, so you can do the following:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
(msg) => msg.sender.isVerified,
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
It can even be asynchronous:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
async (msg) => await shouldProcessMessage(msg.id),
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
To accept parameters in your custom filter, simply create a function
|
||||||
|
that returns a filter:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const usernameRegex = (regex: RegExp): UpdateFilter<Message> =>
|
||||||
|
(msg) => {
|
||||||
|
const m = msg.sender.username?.match(regex)
|
||||||
|
|
||||||
|
return !!m
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
usernameRegex(/some_regex/),
|
||||||
|
async (msg) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding type modification
|
||||||
|
|
||||||
|
You can also add type modification to your custom filter:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const fromChat: UpdateFilter<Message, { sender: Chat }> =
|
||||||
|
(msg) => msg.sender.type === 'chat'
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
fromChat,
|
||||||
|
async (msg) => {
|
||||||
|
// msg.sender is Chat
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
This is used as-is, it is not checked if the filter actually
|
||||||
|
checks for the given modification due to TypeScript limitations.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Additional fields
|
||||||
|
|
||||||
|
You can add additional fields to the update using type
|
||||||
|
modifications. This may be useful in multiple cases, for example,
|
||||||
|
to add the result of the parametrized filter:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const usernameRegex = (regex: RegExp): UpdateFilter<
|
||||||
|
Message,
|
||||||
|
{ usernameMatch: RegExpMatchArray }
|
||||||
|
> => (msg) => {
|
||||||
|
const m = msg.sender.username?.match(regex)
|
||||||
|
|
||||||
|
if (m) {
|
||||||
|
;(obj as any).usernameMatch = m
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
usernameRegex(/some_regex/),
|
||||||
|
async (msg) => {
|
||||||
|
// msg.usernameMatch is RegExpMatchArray
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can add some additional fields similar to a middleware
|
||||||
|
in other frameworks:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const loadSenderFromDb: UpdateFilter<Message, { senderDb: UserModel }> =
|
||||||
|
async (msg) => {
|
||||||
|
;(msg as any).senderDb = await db.loadUser(msg.sender.id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.and(filters.chat('private'), loadSenderFromDb),
|
||||||
|
async (msg) => {
|
||||||
|
// msg.senderDb is UserModel
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
While this is possible, this is **strongly not recommended**.
|
||||||
|
|
||||||
|
Instead, do this right inside the handler code:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.chat('private'),
|
||||||
|
async (msg) => {
|
||||||
|
const senderDb = await db.loadUser(msg.sender.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
:::
|
191
docs/guide/dispatcher/groups-propagation.md
Executable file
191
docs/guide/dispatcher/groups-propagation.md
Executable file
|
@ -0,0 +1,191 @@
|
||||||
|
# Groups and propagation
|
||||||
|
|
||||||
|
When you register multiple handlers with conflicting filters,
|
||||||
|
only the one registered the first will be executed, to avoid
|
||||||
|
handling the same update twice.
|
||||||
|
|
||||||
|
This is sometimes undesirable, and to handle these you can use
|
||||||
|
either [handler groups](#groups), or [propagation symbols](#propagation)
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
You can also use a [child Dispatcher](children.html). In this page, however,
|
||||||
|
it is assumed that all handlers are registered to one dispatcher.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
|
||||||
|
Your first option to handle the same update multiple times is by
|
||||||
|
using a separate handler group.
|
||||||
|
|
||||||
|
Handler groups are identified with a single number (group `0` is the
|
||||||
|
default group) and are processed within the same dispatcher one-by-one
|
||||||
|
in order (`..., -2, -1, 0, 1, 2, ...`).
|
||||||
|
|
||||||
|
For example, consider the following code
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.or(filters.text, filters.sticker),
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text or sticker')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In this code, the second handler will never be executed, because
|
||||||
|
the first one handles `text` messages as well.
|
||||||
|
|
||||||
|
To make the Dispatcher execute the second handler, you can
|
||||||
|
register it to a different group:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, this handler will be executed *after* the first one.
|
||||||
|
Alternatively, you can pass a negative number to make the second
|
||||||
|
handler execute *before* the first one:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Group can also be set using `addUpdateHandler`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.addUpdateHandler({ ... }, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Propagation
|
||||||
|
|
||||||
|
To customize the behaviour even further, you can use propagation symbols
|
||||||
|
that `@mtcute/dispatcher` exports in `PropagationAction` enum.
|
||||||
|
|
||||||
|
### Stop propagation
|
||||||
|
|
||||||
|
To prevent the update from being handled by any other handlers
|
||||||
|
within the same dispatcher, you can use `PropagationAction.Stop`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.or(filters.text, filters.sticker),
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text or sticker')
|
||||||
|
|
||||||
|
return PropagationAction.Stop
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, second handler *will not* be executed even though it is
|
||||||
|
in a separate group.
|
||||||
|
|
||||||
|
This will not, however, prevent the handlers from a
|
||||||
|
[child Dispatcher](children.html) to be executed.
|
||||||
|
|
||||||
|
### Stop children propagation
|
||||||
|
|
||||||
|
A bit ahead of ourselves, since we haven't covered child
|
||||||
|
Dispatchers yet, but the idea is pretty simple.
|
||||||
|
|
||||||
|
`PropagationAction.StopChidlren` is very similar to the previous one, but also
|
||||||
|
prevents the handlers from child dispatchers to be executed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.or(filters.text, filters.sticker),
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text or sticker')
|
||||||
|
|
||||||
|
return PropagationAction.StopChidlren
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const dp1 = new Dispatcher()
|
||||||
|
dp.addChild(dp1)
|
||||||
|
|
||||||
|
dp1.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, second executor will not be called, even though
|
||||||
|
it is in a child dispatcher.
|
||||||
|
|
||||||
|
### Continue propagation
|
||||||
|
|
||||||
|
As an alternative to [groups](#groups), you can use `PropagationAction.Continue`
|
||||||
|
symbol. It makes the dispatcher continue propagating this update within
|
||||||
|
the same group even though some handler from that group was already executed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.or(filters.text, filters.sticker),
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text or sticker')
|
||||||
|
|
||||||
|
return PropagationAction.Continue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.text,
|
||||||
|
async (msg) => {
|
||||||
|
console.log('Text only')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, the second dispatcher will be called for text messages
|
||||||
|
even though the first one also matches them.
|
||||||
|
|
||||||
|
Note that `Continue` only works within the same handler group.
|
||||||
|
|
||||||
|
## Raw updates
|
||||||
|
|
||||||
|
As mentioned earlier, raw updates are handled independently of parsed
|
||||||
|
updates, and they have their own groups and propagation chain.
|
||||||
|
|
||||||
|
This means that in the following example both handlers will be called,
|
||||||
|
despite returning `Stop` action:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(() => {
|
||||||
|
return PropagationAction.Stop
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onRawUpdate(
|
||||||
|
(cl, upd) => upd._ === 'updateNewMessage',
|
||||||
|
() => { ... }
|
||||||
|
)
|
||||||
|
```
|
402
docs/guide/dispatcher/handlers.md
Executable file
402
docs/guide/dispatcher/handlers.md
Executable file
|
@ -0,0 +1,402 @@
|
||||||
|
# Handlers
|
||||||
|
|
||||||
|
Dispatcher can process a lot of different update types,
|
||||||
|
and for each of them you can register as many handlers
|
||||||
|
as you want.
|
||||||
|
|
||||||
|
For each of the handler types, there are 2 ways you can add
|
||||||
|
a handler - either using a specialized method, or
|
||||||
|
with `addUpdateHandler` method, as [described below](#addupdatehandler)
|
||||||
|
|
||||||
|
See also: [Reference](https://ref.mtcute.dev/classes/_mtcute_dispatcher.Dispatcher.html)
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Do not add or remove handlers inside of another handler,
|
||||||
|
this may lead to undefined behaviour.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## New message
|
||||||
|
|
||||||
|
Whenever a new message is received by the bot, or a message is sent
|
||||||
|
by another client (mostly used for users), `new_message` handlers are
|
||||||
|
dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (upd) => {
|
||||||
|
await upd.answerText('Hey!')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edit message
|
||||||
|
|
||||||
|
Whenever a message is edited (and client receives an update about that*),
|
||||||
|
`edit_message` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onEditMessage(async (upd) => {
|
||||||
|
await upd.replyText('Yes.')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
<small>* Telegram might decide not to send these updates
|
||||||
|
in case this message is old enough.</small>
|
||||||
|
|
||||||
|
## Message group
|
||||||
|
|
||||||
|
When message grouping is enabled (see [here](/guide/intro/updates.md#message-grouping)),
|
||||||
|
`message_group` handlers are dispatched when a media group (aka album) is received:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onMessageGroup(async (upd) => {
|
||||||
|
await upd.replyText('Thanks for the media!')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete message
|
||||||
|
|
||||||
|
Whenever a message is deleted (and client receives an update about that*),
|
||||||
|
`delete_message` handlers are dispatched. Note that these updates
|
||||||
|
only contain message ID(s), and not its contents, and it is up to you
|
||||||
|
to check what that ID corresponds to.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onDeleteMessage(async (upd) => {
|
||||||
|
if (upd.messageIds.includes(42)) {
|
||||||
|
console.log('Magic message deleted :c')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that for private chats, this does not include
|
||||||
|
user's ID, so you may want to implement some caching
|
||||||
|
if you need that info.
|
||||||
|
|
||||||
|
<small>* Telegram might decide not to send these updates
|
||||||
|
in case this message is old enough.</small>
|
||||||
|
|
||||||
|
## Chat member
|
||||||
|
|
||||||
|
Whenever chat member status is changed in a channel/supergroup where the bot
|
||||||
|
is an administrator, `chat_member` handlers are dispatched.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChatMemberUpdate(async (upd) => {
|
||||||
|
console.log(`${upd.user.displayName} ${upd.type} by ${upd.actor.displayName}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
You can filter by update type using `filters.chatMember`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChatMemberUpdate(
|
||||||
|
filters.chatMember('joined'),
|
||||||
|
async (upd) => {
|
||||||
|
await upd.client.sendText(upd.chat, `${upd.user.mention()}, welcome to the chat!`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Inline query
|
||||||
|
|
||||||
|
Whenever an inline query is sent by user to your bot, `inline_query`
|
||||||
|
handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (upd) => {
|
||||||
|
await upd.answer([], {
|
||||||
|
switchPm: {
|
||||||
|
text: 'Hello!',
|
||||||
|
parameter: 'inline_hello'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can learn more about inline queries in [Inline Mode](./inline-mode.html) section.
|
||||||
|
|
||||||
|
## Chosen inline result
|
||||||
|
|
||||||
|
When a user selects an inline result, and assuming that you have
|
||||||
|
**inline feedback** feature enabled, `chosen_inline_result` handlers
|
||||||
|
are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChosenInlineResult(async (upd) => {
|
||||||
|
await upd.editMessage({
|
||||||
|
text: `${result.user.displayName}, thanks for using inline!`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
As mentioned, these updates are only sent by Telegram when you
|
||||||
|
have enabled **inline feedback** feature. You can enable it
|
||||||
|
in [@BotFather](https://t.me/botfather).
|
||||||
|
|
||||||
|
It is however noted by Telegram that this should only be used
|
||||||
|
for statistical purposes, and even if probability setting is 100%,
|
||||||
|
not all chosen inline results may be reported
|
||||||
|
([source](https://core.telegram.org/api/bots/inline#inline-feedback)).
|
||||||
|
|
||||||
|
## Callback query
|
||||||
|
|
||||||
|
Whenever user clicks on a [callback button](../topics/keyboards.html#inline-keyboards),
|
||||||
|
`callback_query` or `inline_callback_query` handlers are dispatched, based on the origin of the message:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onCallbackQuery(async (upd) => {
|
||||||
|
await upd.answer({ text: '🌸' })
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onInlineCallbackQuery(async (upd) => {
|
||||||
|
await upd.answer({ text: '🌸' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
For messages sent normally by the bot (e.g. using `sendText`), `callback_query` handlers are dispatched.
|
||||||
|
For messages sent from an inline query (e.g. inside `onInlineQuery`), `inline_callback_query` handlers are dispatched.
|
||||||
|
|
||||||
|
## Poll update
|
||||||
|
|
||||||
|
Whenever a poll state is updated (stopped, anonymous user has voted, etc.),
|
||||||
|
`poll` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onPollUpdate(async (upd) => {
|
||||||
|
// do something
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Bots only receive updates about polls that they have sent.
|
||||||
|
|
||||||
|
Note that due to Telegram limitation, sometimes the update does not
|
||||||
|
include the poll itself, and mtcute creates a "stub" poll,
|
||||||
|
that is missing most of the information (including question text,
|
||||||
|
answers text, missing flags like `quiz`, etc.). Number of votes per-answer
|
||||||
|
is always there, though.
|
||||||
|
|
||||||
|
If you need that missing information, you will need to implement
|
||||||
|
caching by yourself. Bot API (strictly speaking, TDLib) does it internally
|
||||||
|
and thus is able to provide all the needed information autonomously.
|
||||||
|
This is not implemented in mtcute yet.
|
||||||
|
|
||||||
|
## Poll vote
|
||||||
|
|
||||||
|
When a user votes in a public poll, `poll_vote` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onPollVote(async (upd) => {
|
||||||
|
upd.user.sendText('Thanks for voting!')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Bots only receive updates about polls that they have sent.
|
||||||
|
|
||||||
|
This update currently doesn't contain information about the poll,
|
||||||
|
only the poll ID, so if you need that info,
|
||||||
|
you'll have to implement caching yourself.
|
||||||
|
|
||||||
|
|
||||||
|
## User status
|
||||||
|
|
||||||
|
When a user's online status changes (e.g. user goes offline),
|
||||||
|
and client receives an update about that*,
|
||||||
|
`user_status` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onUserStatusUpdate(async (upd) => {
|
||||||
|
console.log(`User ${upd.userId} is now ${upd.status}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
<small>* Telegram might decide not to send these updates
|
||||||
|
in many cases, for example: you don't have an active PM
|
||||||
|
with this user, this user is from a large chat that
|
||||||
|
you aren't currently chatting in, etc.</small>
|
||||||
|
|
||||||
|
## User typing
|
||||||
|
|
||||||
|
When a user's typing status changes,
|
||||||
|
and client receives an update about that*,
|
||||||
|
`user_status` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onUserTyping(async (upd) => {
|
||||||
|
console.log(`${upd.userId} is ${upd.status} in ${upd.chatId}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
<small>* Telegram might decide not to send these updates
|
||||||
|
in many cases, for example: you haven't talked
|
||||||
|
to this user for some time, that user/chat is archived, etc.</small>
|
||||||
|
|
||||||
|
## History read
|
||||||
|
|
||||||
|
When history is read in a chat (either by the other party or by you from another client),
|
||||||
|
and client receives an update about that, `history_read` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onHistoryRead(async (upd) => {
|
||||||
|
console.log(`History read in ${upd.chatId} up to ${upd.maxReadId}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bot stopped
|
||||||
|
|
||||||
|
When a user clicks "Stop bot" button in the bot's profile,
|
||||||
|
`bot_stopped` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onBotStopped(async (upd) => {
|
||||||
|
console.log(`Bot stopped by ${upd.user.id}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Join requests
|
||||||
|
|
||||||
|
These updates differ depending on whether the currently logged in user is a bot or not.
|
||||||
|
|
||||||
|
### Bot
|
||||||
|
|
||||||
|
When a user requests to join a group/channel where the current bot is an admin,
|
||||||
|
`bot_chat_join_request` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onBotChatJoinRequest(async (upd) => {
|
||||||
|
console.log(`User ${upd.user.id} wants to join ${upd.chat.id}`)
|
||||||
|
|
||||||
|
if (upd.user.id === DUROV) {
|
||||||
|
await upd.decline()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
When a user requests to join a group/channel where the current user is an admin,
|
||||||
|
`chat_join_request` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChatJoinRequest(async (upd) => {
|
||||||
|
console.log(`User ${upd.recentRequesters[0].id} wants to join ${upd.chatId}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These updates contain less information than bot join requests,
|
||||||
|
and additional info should be fetched manually if needed
|
||||||
|
|
||||||
|
## Pre-checkout query
|
||||||
|
|
||||||
|
When a user clicks "Pay" button, `pre_checkout_query` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onPreCheckoutQuery(async (upd) => {
|
||||||
|
await upd.approve()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Story updates
|
||||||
|
|
||||||
|
When a story is posted or modified, `story` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onStoryUpdate(async (upd) => {
|
||||||
|
console.log(`${upd.peer.id} posted or modified a story!`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete story updates
|
||||||
|
|
||||||
|
When a story is deleted, `delete_story` handlers are dispatched:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onDeleteStory(async (upd) => {
|
||||||
|
console.log(`${upd.peer.id} deleted story ${upd.storyId}!`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Raw updates
|
||||||
|
|
||||||
|
Dispatcher only implements the most commonly used updates,
|
||||||
|
but you can still add a handler for a custom MTProto update:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onRawUpdate(
|
||||||
|
(cl, upd) => upd._ === 'updateUserName',
|
||||||
|
async (
|
||||||
|
client: TelegramClient,
|
||||||
|
update_: tl.TypeUpdate,
|
||||||
|
peers: PeersIndex,
|
||||||
|
) => {
|
||||||
|
const update = update_ as tl.RawUpdateUserName
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the signature is slightly different: since the update
|
||||||
|
is not parsed by the library, client, raw update and entities
|
||||||
|
are provided as-is.
|
||||||
|
|
||||||
|
Raw update handlers are dispatched independently of parsed update handlers
|
||||||
|
([learn more](groups-propagation.html#raw-updates)).
|
||||||
|
|
||||||
|
## `addUpdateHandler`
|
||||||
|
|
||||||
|
Another way to register a handler is to use `addUpdateHandler` method.
|
||||||
|
|
||||||
|
It supports all the updates that have a specialized methods, and this
|
||||||
|
method is actually used by `on*` under the hood. It accepts an object,
|
||||||
|
containing update type, its handler and optionally a `check` function,
|
||||||
|
which checks if the update should be handled by this handler
|
||||||
|
(basically a [Filter](filters.html)):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.addUpdateHandler({
|
||||||
|
type: 'new_message',
|
||||||
|
callback: async (msg) => {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
check: filters.media
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This may be useful in case you are loading your handlers dynamically.
|
||||||
|
|
||||||
|
## Removing handlers
|
||||||
|
|
||||||
|
It might be useful to remove a previously added handler.
|
||||||
|
|
||||||
|
### Removing a single handler
|
||||||
|
|
||||||
|
To remove a single handler, it must have been added using `addUpdateHandler`,
|
||||||
|
and then that object should be passed to `removeUpdateHandler`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handler = { ... }
|
||||||
|
dp.addUpdateHandler(handler)
|
||||||
|
|
||||||
|
// later
|
||||||
|
dp.removeUpdateHandler(handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
Handlers are matched by the object that wraps them, because
|
||||||
|
the same function may be used for multiple different handlers.
|
||||||
|
|
||||||
|
### Removing handlers by type
|
||||||
|
|
||||||
|
To remove all handlers that have the given type, pass this type
|
||||||
|
to `removeUpdateHandler`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.removeUpdateHandler('new_message')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing all handlers
|
||||||
|
|
||||||
|
Pass `all` to `removeUpdateHandler`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.removeUpdateHandler('all')
|
||||||
|
```
|
731
docs/guide/dispatcher/inline-mode.md
Executable file
731
docs/guide/dispatcher/inline-mode.md
Executable file
|
@ -0,0 +1,731 @@
|
||||||
|
# Inline mode
|
||||||
|
|
||||||
|
Users can interact with bots using inline queries, by starting a message
|
||||||
|
with bot's username and then typing their query.
|
||||||
|
|
||||||
|
You can learn more about them in
|
||||||
|
[Bot API docs](https://core.telegram.org/bots/inline)
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://core.telegram.org/file/811140592/2/P4-tFhmBsCg/57418af08f1a252d45"
|
||||||
|
width="320"
|
||||||
|
caption="Example of an inline bot"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Handling inline queries
|
||||||
|
|
||||||
|
To handle inline queries to your bot, simply use `onInlineQuery`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `filters.regex` to filter based on the query text:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(
|
||||||
|
filters.regex(/^cats /),
|
||||||
|
async (query) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Answering to inline queries
|
||||||
|
|
||||||
|
Answering an inline query means sending results of the query to Telegram.
|
||||||
|
The results may contain any media, including articles, photos, videos,
|
||||||
|
stickers, etc, and can also be heterogeneous, which means that you can use
|
||||||
|
different media types in results for the same query.
|
||||||
|
|
||||||
|
To answer a query, simply use `.answer()` and pass an array that
|
||||||
|
contains [results](#results):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([...])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Clients wait about 10 seconds for results, after which they assume
|
||||||
|
the bot timed out. Make sure not to do heavy computations there!
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
Every inline result must contain a **unique** result ID, which
|
||||||
|
can be later used in [chosen inline result updates](#chosen-inline-results)
|
||||||
|
updates to determine which one was chosen.
|
||||||
|
|
||||||
|
Media inside inline results *must* be provided via either HTTP URL, or
|
||||||
|
re-used from Telegram (e.g. using [File IDs](/guide/topics/files.md#file-ids)).
|
||||||
|
Telegram does not allow uploading new files directly to inline results.
|
||||||
|
|
||||||
|
When choosing a result, by default, a message containing the respective
|
||||||
|
result is sent, but the message contents can be [customized](#custom-message)
|
||||||
|
using `message` field.
|
||||||
|
|
||||||
|
Telegram supports a bunch of result types, and mtcute supports sending all of
|
||||||
|
them as an inline result. In the below examples we'll be using direct URLs
|
||||||
|
to content, but you can also use File IDs instead.
|
||||||
|
|
||||||
|
### Article
|
||||||
|
|
||||||
|
An article is a result that contains a title, description and *optionally*
|
||||||
|
a thumbnail and a URL:
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/cb212ab91102bf3dd1c7306c943dee37.png"
|
||||||
|
caption="Article result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.article(
|
||||||
|
'RESULT_ID',
|
||||||
|
{
|
||||||
|
title: 'Article title',
|
||||||
|
description: 'Article description',
|
||||||
|
thumb: 'https://example.com/image.jpg',
|
||||||
|
url: 'https://example.com/some-article.html'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When choosing this result, by default, a message with the following
|
||||||
|
text is sent (Handlebars syntax is used here):
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
{{#if url}}
|
||||||
|
<a href="{{url}}"><b>{{title}}</b></a>
|
||||||
|
{{else}}
|
||||||
|
<b>{{title}}</b>
|
||||||
|
{{/if}}
|
||||||
|
{{#if description}}
|
||||||
|
{{description}}
|
||||||
|
{{/if}}
|
||||||
|
```
|
||||||
|
|
||||||
|
For the above example, this would result in the following message:
|
||||||
|
|
||||||
|
<v-img src="https://i.gyazo.com/c97de58996a6e38c40623e5f64c44d9b.png" />
|
||||||
|
|
||||||
|
### GIF
|
||||||
|
|
||||||
|
You can send an animated GIF (either real GIF, or an MP4 without sound)
|
||||||
|
as a result.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/5ecfac354b2068cdb7f3dd9b4f90d5ef.png"
|
||||||
|
caption="GIF result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.gif(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also add title and description, however only some clients display them
|
||||||
|
(e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/13ba734bf42d5762b1b4b00543653bcf.jpg"
|
||||||
|
width="480"
|
||||||
|
caption="GIF result with title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.gif(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
|
||||||
|
{
|
||||||
|
title: 'GIF title',
|
||||||
|
description: 'GIF description',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video
|
||||||
|
|
||||||
|
You can send an MP4 video as an inline result.
|
||||||
|
|
||||||
|
When sending by direct URL, there's a file size limit of 20 MB,
|
||||||
|
and you *must* provide a thumbnail, otherwise the result will be ignored.
|
||||||
|
Thumbnail is only used until the video file is cached by Telegram, and is then
|
||||||
|
overridden by Telegram-generated video thumbnail.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/3b328516687c2719d828d051211e8d5d.png"
|
||||||
|
caption="Video result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.video(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
|
||||||
|
{
|
||||||
|
thumb:
|
||||||
|
'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
|
||||||
|
title: 'Video title',
|
||||||
|
description: 'Video description',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can send a video by its URL (e.g. from YouTube) using
|
||||||
|
`isEmbed: true`:
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/9281ab09bfde7ee013dc0c75fd1f939f.png"
|
||||||
|
caption="Video page result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.video(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
{
|
||||||
|
isEmbed: true,
|
||||||
|
thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
|
||||||
|
title: 'Video title',
|
||||||
|
description: 'Video description',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Choosing such result would send a message containing URL to that video:
|
||||||
|
|
||||||
|
<v-img src="https://i.gyazo.com/818b6651333ee2a7e5dd13ebcfc9febc.png" />
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
|
||||||
|
You can send an MPEG audio file as an inline result.
|
||||||
|
|
||||||
|
When sending by direct URL, there's a file size limit of 20 MB.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/a044dcbca5d2b32b6885aa8cc5565aab.png"
|
||||||
|
caption="Audio result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.audio(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://vk.com/mp3/cc_ice_melts.mp3',
|
||||||
|
{
|
||||||
|
performer: 'Griby',
|
||||||
|
title: 'Tayet Lyod',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip NOTE
|
||||||
|
Performer, title and other meta can't be changed once the file is
|
||||||
|
cached by Telegram (they will still be displayed in the results,
|
||||||
|
but not in the message). To avoid caching when sending by URL, add
|
||||||
|
a random query parameter (e.g. `?notgcache`), which will make
|
||||||
|
Telegram think this is a new file.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Voice
|
||||||
|
|
||||||
|
You can send an OGG file as a voice note inline result.
|
||||||
|
|
||||||
|
Waveform seems to only be generated for OGG files encoded with OPUS,
|
||||||
|
so if it is not generated, try re-encoding your file with OPUS.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/b99d48dc8ce2b8f6bd3bd8a645eedc9b.png"
|
||||||
|
caption="Voice result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.voice(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://tei.su/test_voice.ogg',
|
||||||
|
{
|
||||||
|
title: 'Voice title',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Photo
|
||||||
|
|
||||||
|
You can send an image as an inline result.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/c7fafa82712981248212663e13024fa1.png"
|
||||||
|
caption="Photo result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg'
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also add title and description, however only some clients display them
|
||||||
|
(e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/47e35f56ceb8685e745a5f17a580b7c8.jpg"
|
||||||
|
width="480"
|
||||||
|
caption="Photo result with title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
title: 'Photo title',
|
||||||
|
description: 'Photo description',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sticker
|
||||||
|
|
||||||
|
You can send a sticker as an inline result. You can't send a
|
||||||
|
sticker by URL ([Telegram limitation](https://t.me/tdlibchat/17923)),
|
||||||
|
only by File ID.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/54327f78ab6eb996fd534254678f96d6.png"
|
||||||
|
caption="Sticker result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.sticker(
|
||||||
|
'RESULT_ID',
|
||||||
|
'CAACAgIAAxk...JtzysqiUK3IAQ'
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
You can send a file as an inline result.
|
||||||
|
|
||||||
|
Due to Telegram limitations, when using URLs, you can only send PDF
|
||||||
|
and ZIP files, and must set `mime` accordingly
|
||||||
|
(`application/pdf` and `application/zip` MIMEs respectively).
|
||||||
|
With File IDs, you can send any file.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/856a21545de951bfa1fe063af2db7b80.png"
|
||||||
|
caption="File result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.file(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
|
||||||
|
{
|
||||||
|
mime: 'application/pdf',
|
||||||
|
title: 'File title',
|
||||||
|
description: 'File description'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geolocation
|
||||||
|
|
||||||
|
You can send a geolocation as an inline result.
|
||||||
|
|
||||||
|
By default, Telegram generates `thumb` for result based on the
|
||||||
|
location provided.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/95d87196080108e090b11904e98e4818.png"
|
||||||
|
caption="Geo result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.geo(
|
||||||
|
'RESULT_ID',
|
||||||
|
{
|
||||||
|
latitude: 55.751999,
|
||||||
|
longitude: 37.617734,
|
||||||
|
title: 'Kremlin'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Venue
|
||||||
|
|
||||||
|
You can send a venue as an inline result.
|
||||||
|
|
||||||
|
By default, Telegram generates `thumb` for result based on the
|
||||||
|
location provided.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/57e67771353dc895fadca3a5029181dd.png"
|
||||||
|
caption="Venue result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.venue(
|
||||||
|
'RESULT_ID',
|
||||||
|
{
|
||||||
|
latitude: 55.751999,
|
||||||
|
longitude: 37.617734,
|
||||||
|
title: 'Kremlin',
|
||||||
|
address: 'Red Square'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contact
|
||||||
|
|
||||||
|
You can send a contact as an inline result.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/eb4ac5feaa7717ea7c00fe0dbc562e8f.png"
|
||||||
|
caption="Contact result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.contact(
|
||||||
|
'RESULT_ID',
|
||||||
|
{
|
||||||
|
firstName: 'Alice',
|
||||||
|
phone: '+79001234567',
|
||||||
|
thumb: 'https://avatars.githubusercontent.com/u/86301490'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Games
|
||||||
|
|
||||||
|
Finally, you can send a game as an inline result.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/dfc4b6702db4f586c92ac243b5d28bcb.png"
|
||||||
|
caption="Game result example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.game('RESULT_ID', 'game_short_name'),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Custom message
|
||||||
|
|
||||||
|
By default, mtcute generates a message based on result contents. However,
|
||||||
|
you can override the message that will be sent using `message`
|
||||||
|
|
||||||
|
### Text
|
||||||
|
|
||||||
|
Instead of media or default article message, you may want to send a custom
|
||||||
|
text message:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
message: BotInlineMessage.text(
|
||||||
|
'Ha-ha, just kidding. No Renge for you :p'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Media
|
||||||
|
|
||||||
|
You can customize media message (for photos, videos, voices, documents, etc.)
|
||||||
|
with custom caption, keyboard, etc:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Sadly, Telegram does not allow sending another media instead of the one
|
||||||
|
you provided in the result (however, you *could* use web previews to
|
||||||
|
accomplish similar results)
|
||||||
|
|
||||||
|
### Geolocation
|
||||||
|
|
||||||
|
Instead of sending the default message, you can send geolocation,
|
||||||
|
or even live geolocation:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
// or BotInlineMessage.geoLive
|
||||||
|
message: BotInlineMessage.geo({
|
||||||
|
latitude: 55.751999,
|
||||||
|
longitude: 37.617734,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Venue
|
||||||
|
|
||||||
|
Instead of sending the default message, you can send a venue
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
message: BotInlineMessage.venue({
|
||||||
|
latitude: 55.751999,
|
||||||
|
longitude: 37.617734,
|
||||||
|
title: 'Kremlin',
|
||||||
|
address: 'Red Square'
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contact
|
||||||
|
|
||||||
|
Instead of sending the default message, you can send a contact
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.photo(
|
||||||
|
'RESULT_ID',
|
||||||
|
'https://nyanpa.su/renge.jpg',
|
||||||
|
{
|
||||||
|
message: BotInlineMessage.contact({
|
||||||
|
firstName: 'Alice',
|
||||||
|
phone: '+79001234567',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game
|
||||||
|
|
||||||
|
For inline results containing a game, you can customize keyboard
|
||||||
|
under the message ([learn more](../topics/keyboards.html)):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([
|
||||||
|
BotInline.game('RESULT_ID', 'test', {
|
||||||
|
message: BotInlineMessage.game({
|
||||||
|
replyMarkup: ...
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Switch to PM
|
||||||
|
|
||||||
|
Some bots may benefit from switching to PM with the bot
|
||||||
|
for some action (e.g. logging in with your account).
|
||||||
|
For that, you can use `switchPm` button along with your results:
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://i.gyazo.com/3cbf356545dbd696d128c060389ab0f5.png"
|
||||||
|
caption="Switch to PM example"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onInlineQuery(async (query) => {
|
||||||
|
query.answer([], {
|
||||||
|
switchPm: {
|
||||||
|
text: 'Log in',
|
||||||
|
parameter: 'login_inline'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
|
||||||
|
await msg.answerText('Thanks for logging in!')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chosen inline results
|
||||||
|
|
||||||
|
When a user selects an inline result, and assuming that you have
|
||||||
|
**inline feedback** feature enabled, an update is received,
|
||||||
|
which can be handled:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChosenInlineResult(async (result) => {
|
||||||
|
await result.editMessage({
|
||||||
|
text: `${result.user.displayName}, thanks for using inline!`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `filters.regex` to filter by chosen result ID:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChosenInlineResult(
|
||||||
|
filters.regex(/^CATS_/),
|
||||||
|
async (result) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
These updates are only sent by Telegram when you
|
||||||
|
have enabled **inline feedback** feature. You can enable it
|
||||||
|
in [@BotFather](https://t.me/botfather).
|
||||||
|
|
||||||
|
It is however noted by Telegram that this should only be used
|
||||||
|
for statistical purposes, and even if probability setting is 100%,
|
||||||
|
not all chosen inline results may be reported
|
||||||
|
([source](https://core.telegram.org/api/bots/inline#inline-feedback)).
|
||||||
|
|
||||||
|
|
||||||
|
## Editing inline message
|
||||||
|
|
||||||
|
You can edit an inline message in Chosen inline query update handlers, and
|
||||||
|
in Callback query updates:
|
||||||
|
|
||||||
|
|
||||||
|
::: tip NOTE
|
||||||
|
In the below examples, it is assumed that callback query
|
||||||
|
originates from an inline message.
|
||||||
|
:::
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChosenInlineResult(async (result) => {
|
||||||
|
await result.editMessage({...})
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onCallbackQuery(async (query) => {
|
||||||
|
await query.editMessage({...})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also save inline message ID and edit the message later:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function updateMessageLater(msgId: string) {
|
||||||
|
setTimeout(() => {
|
||||||
|
tg.editInlineMessage(msgId, {...})
|
||||||
|
.catch(console.error)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.onChosenInlineResult(async (result) => {
|
||||||
|
updateMessageLater(result.messageIdStr)
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onCallbackQuery(async (query) => {
|
||||||
|
updateMessageLater(query.inlineMessageIdStr)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`messageIdStr` and `inlineMessageIdStr` contain string representation
|
||||||
|
of the inline message ID, to simplify its storage.
|
||||||
|
|
||||||
|
You can also use the `messageId` and `inlineMessageId` respectively,
|
||||||
|
which contain a TL object, in case you need to use Raw API. In mtcute,
|
||||||
|
they are interchangeable.
|
||||||
|
:::
|
||||||
|
|
||||||
|
You can also edit media inside inline messages, and even upload new media
|
||||||
|
directly to them (unlike Bot API):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onChosenInlineResult(async (result) => {
|
||||||
|
const link = await getDownloadLink(result.id)
|
||||||
|
|
||||||
|
await result.editMessage({
|
||||||
|
media: InputMedia.audio(await fetch(link))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
20
docs/guide/dispatcher/intro.md
Executable file
20
docs/guide/dispatcher/intro.md
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
We've already briefly [touched](../intro/updates.html) on what Dispatcher is,
|
||||||
|
but as a quick reminder: Dispatcher is a class that processes
|
||||||
|
updates from the client and *dispatches* them to the registered handlers.
|
||||||
|
|
||||||
|
It is implemented in `@mtcute/dispatcher` package
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
To use a dispatcher, you need to first create a bound dispatcher
|
||||||
|
using `Dispatcher.for` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Dispatcher } from '@mtcute/dispatcher'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({...})
|
||||||
|
const dp = Dispatcher.for(tg)
|
||||||
|
```
|
||||||
|
|
67
docs/guide/dispatcher/middlewares.md
Executable file
67
docs/guide/dispatcher/middlewares.md
Executable file
|
@ -0,0 +1,67 @@
|
||||||
|
# Middlewares
|
||||||
|
|
||||||
|
Dispatcher provides basic middleware functionality. It is not as extensible
|
||||||
|
as middlewares in frameworks likes [Telegraf](https://github.com/telegraf/telegraf),
|
||||||
|
and that is by design, since we already have filters and propagation using which
|
||||||
|
you can achieve pretty much the same.
|
||||||
|
|
||||||
|
Middlewares are only called for parsed updates.
|
||||||
|
|
||||||
|
## Middleware types
|
||||||
|
|
||||||
|
Dispatcher supports 2 middlewares: pre-update and post-update.
|
||||||
|
|
||||||
|
### Pre-update
|
||||||
|
|
||||||
|
**Pre-update** middleware is called right before an update is going
|
||||||
|
to be dispatched, and can be used to skip that update altogether.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onPreUpdate((upd) => {
|
||||||
|
// randomly skip 10% of updates
|
||||||
|
if (Math.random() < 0.1)
|
||||||
|
return PropagationAction.Stop
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-update
|
||||||
|
|
||||||
|
**Post-update** middleware is called after an update was processed
|
||||||
|
by the dispatcher. Whether the update was handled is also provided here:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onPostUpdate((handled, upd) => {
|
||||||
|
if (handled) {
|
||||||
|
console.log(`handled ${upd.name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
You can add some additional context in the pre-update middleware,
|
||||||
|
and that context will also be available in post-update middleware
|
||||||
|
and error handler:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface TimerContext {
|
||||||
|
start: number
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.onPreUpdate<TimerContext>((upd) => {
|
||||||
|
upd.start = Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onPostUpdate<TimerContext>((handled, upd) => {
|
||||||
|
if (handled) {
|
||||||
|
console.log(`handled ${upd.name} in ${Date.now() - upd.start} ms`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dp.onError<TimerContext>((err, upd) => {
|
||||||
|
console.log(`error for ${upd.name} after ${Date.now() - upd.start} ms`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that *currently* you can't access that context from handlers or filters
|
||||||
|
(see [mtcute#4](https://github.com/mtcute/mtcute/issues/4)).
|
61
docs/guide/dispatcher/rate-limit.md
Executable file
61
docs/guide/dispatcher/rate-limit.md
Executable file
|
@ -0,0 +1,61 @@
|
||||||
|
# Rate limit
|
||||||
|
|
||||||
|
You may want to limit access to certain commands or handlers
|
||||||
|
in your bot, for example to avoid flood limits.
|
||||||
|
|
||||||
|
For that, you can use rate-limiting feature of the Dispatcher.
|
||||||
|
Rate limiting is built into update state object, and all you have to do
|
||||||
|
is to call `.rateLimit` function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.command('some_expensive_command'),
|
||||||
|
async (msg, state) => {
|
||||||
|
try {
|
||||||
|
// 1 request every 15 seconds
|
||||||
|
await state.rateLimit('some_expensive_command', 1, 15)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof RateLimitError) {
|
||||||
|
await msg.replyText('Try again later')
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = doSomeExpensiveComputations()
|
||||||
|
await msg.replyText(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we use `some_expensive_command` as a key for the
|
||||||
|
rate limit. This allows you to have multiple independent rate limits.
|
||||||
|
|
||||||
|
When the rate limit is exceeded, `rateLimit` throws `RateLimitError`, which
|
||||||
|
also contains `.reset` field with the Unix time when the rate limit will be
|
||||||
|
reset.
|
||||||
|
|
||||||
|
## Throttle
|
||||||
|
|
||||||
|
In some cases you might want to throttle a used instead of rate-limiting them,
|
||||||
|
and that is exactly what `.throttle` does. When a rate limit is reached,
|
||||||
|
it waits until the rate limit is replenished and then returns:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.start,
|
||||||
|
async (msg, state) => {
|
||||||
|
// no more than 2 rps per user
|
||||||
|
await state.throttle('start', 2, 1)
|
||||||
|
|
||||||
|
await msg.replyText('Hi!')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`throttle` returns tuple containing number of requests left until
|
||||||
|
the user hits the limit, and when the limit will be reset:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [left, reset] = await state.throttle('some_expensive_command', 1, 15)
|
||||||
|
await msg.replyText(`${left} requests left. Reset at ${new Date(reset).toString()}`)
|
||||||
|
```
|
245
docs/guide/dispatcher/scenes.md
Executable file
245
docs/guide/dispatcher/scenes.md
Executable file
|
@ -0,0 +1,245 @@
|
||||||
|
# Scenes
|
||||||
|
|
||||||
|
Scene is basically a child dispatcher with a name. It is not used by default,
|
||||||
|
unless you explicitly **enter** into it, after which **all** (supported*)
|
||||||
|
updates will be redirected to the scene, and not processed as usual.
|
||||||
|
|
||||||
|
This is particularly useful with FSM, since it allows users to
|
||||||
|
enter independent "dialogues" with the bot.
|
||||||
|
|
||||||
|
<!-- Full example: TODO LINK -->
|
||||||
|
|
||||||
|
<p><small>* Only updates that can be <a href="./state#keying">keyed</a> are supported</small></p>
|
||||||
|
|
||||||
|
## Creating a scene
|
||||||
|
|
||||||
|
A scene is created by using `Dispatcher.scene`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface SceneState { ... }
|
||||||
|
|
||||||
|
const dp = Dispatcher.scene<SceneState>('scene-name')
|
||||||
|
// add handlers to `dp`
|
||||||
|
|
||||||
|
export const SomeScene = dp
|
||||||
|
|
||||||
|
// then in the main file:
|
||||||
|
dp.addScene(SomeScene)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't use state within your scene, just don't pass anything:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const scene = new Dispatcher()
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Scenes should only be added to the root dispatcher.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Scene names can't start with `$` (dollar sign), since it is reserved
|
||||||
|
for internal FSM needs. Other than that, you can use any name.
|
||||||
|
|
||||||
|
## Entering a scene
|
||||||
|
|
||||||
|
To enter a scene or change current scene, use `state.enter` and pass the scene instance:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.enter(SomeScene)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass some initial state to the scene:
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.enter(SomeScene, { with: { foo: 'bar' } })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, new scene will be used starting from the next update,
|
||||||
|
but in some cases you may want it to be used immediately.
|
||||||
|
|
||||||
|
To make the dispatcher immediately dispatch the update to the newly
|
||||||
|
entered scene, use `PropagationAction.ToScene`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.enter(SomeScene)
|
||||||
|
return PropagationAction.ToScene
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exiting a scene
|
||||||
|
|
||||||
|
To exit from the current scene, use `state.exit`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.exit()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To make the dispatcher immediately dispatch the update to the
|
||||||
|
root dispatcher, use `PropagationAction.ToScene`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.exit()
|
||||||
|
return PropagationAction.ToScene
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Entering another scene will also exit the current one.
|
||||||
|
|
||||||
|
## Isolated state
|
||||||
|
|
||||||
|
By default, scenes have their own, fully isolated FSM state,
|
||||||
|
which is (by default) destroyed as soon as the user leaves the scene.
|
||||||
|
This is more clean than using the global state, and also allows
|
||||||
|
scenes to have their own state type.
|
||||||
|
|
||||||
|
However, in some cases, you may want to access global FSM state.
|
||||||
|
This is possible with `getGlobalState`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
const local = await state.get()
|
||||||
|
|
||||||
|
const globalState = await dp.getGlobalState<BotState>(msg)
|
||||||
|
const global = await globalState.get()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can disable isolated storage for FSM altogether and use
|
||||||
|
global state directly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dp = new Dispatcher<BotState>()
|
||||||
|
// add handlers to `dp`
|
||||||
|
|
||||||
|
export const SomeScene = dp
|
||||||
|
|
||||||
|
// in the main file:
|
||||||
|
dp.addScene(SomeScene, /* scoped: */ false)
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, `scene` can't have state type other than `BotState` (i.e. the
|
||||||
|
one used by the parent), and it will not be reset when the user
|
||||||
|
leaves the scene.
|
||||||
|
|
||||||
|
|
||||||
|
## Wizard scenes
|
||||||
|
|
||||||
|
A commonly used pattern for scenes is a step-by-step wizard.
|
||||||
|
|
||||||
|
To simplify their creation, mtcute implements `WizardScene`,
|
||||||
|
which is simply a Dispatcher with an additional method: `addStep`.
|
||||||
|
|
||||||
|
Every step is an `onNewMessage` handler that is filtered by the current
|
||||||
|
step, which is stored in wizard's FSM state. In each step, you can
|
||||||
|
choose either to `WizardAction.Stay` in the same step, proceed to the
|
||||||
|
`WizardAction.Next` step, or `WizardAction.Exit` the wizard altogether.
|
||||||
|
|
||||||
|
You can also return a `number` to jump to some step (ordering starts from 0).
|
||||||
|
|
||||||
|
Additionally, wizard provides `onCurrentStep` filter that filters for updates that
|
||||||
|
happened *after* the last triggered step.
|
||||||
|
|
||||||
|
<!-- A simple example (full example TODO LINK): -->
|
||||||
|
A simple example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface RegForm {
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const wizard = new WizardScene<RegForm>('REGISTRATION')
|
||||||
|
|
||||||
|
wizard.addStep(async (msg) => {
|
||||||
|
await msg.answerText('What is your name?', {
|
||||||
|
replyMarkup: BotKeyboard.inline([[BotKeyboard.callback('Skip', 'SKIP')]]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return WizardSceneAction.Next
|
||||||
|
})
|
||||||
|
|
||||||
|
wizard.onCallbackQuery(filters.and(wizard.onCurrentStep(), filters.equals('SKIP')), async (upd, state) => {
|
||||||
|
await state.merge({ name: 'Anonymous' })
|
||||||
|
await wizard.skip(state)
|
||||||
|
|
||||||
|
await upd.client.sendText(upd.chatId, 'Alright, "Anonymous" then\n\nNow enter your email')
|
||||||
|
})
|
||||||
|
|
||||||
|
wizard.addStep(async (msg, state) => {
|
||||||
|
// simple validation
|
||||||
|
if (msg.text.length < 3) {
|
||||||
|
await msg.replyText('Invalid name!')
|
||||||
|
return WizardSceneAction.Stay
|
||||||
|
}
|
||||||
|
|
||||||
|
await state.set({ name: msg.text.trim() })
|
||||||
|
await msg.answerText('Enter your email')
|
||||||
|
|
||||||
|
return WizardSceneAction.Next
|
||||||
|
})
|
||||||
|
|
||||||
|
wizard.addStep(async (msg, state) => {
|
||||||
|
const { name } = (await state.get())!
|
||||||
|
|
||||||
|
console.log({ name, email: msg.text })
|
||||||
|
|
||||||
|
await msg.answerText('Thanks!')
|
||||||
|
return WizardSceneAction.Exit
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using some custom state, you may want to set the default
|
||||||
|
state for the wizard:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
wizard.setDefaultState({ name: 'Ivan' })
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, `{}` is used as the default state.
|
||||||
|
|
||||||
|
## Transition updates
|
||||||
|
|
||||||
|
Whenever you `.enter()` or `.exit()` a scene, the dispatcher will also emit
|
||||||
|
a transition update, which can be caught by using `onSceneTransition`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
scene.onSceneTransition(async (upd, state) => {
|
||||||
|
console.log(`Transition from ${upd.previousScene} to SomeScene`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These handlers are called **before** any of the scene's handlers are called,
|
||||||
|
even if `PropagationAction.ToScene` is used, and can be used to cancel the transition:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.enter(SomeScene)
|
||||||
|
return PropagationAction.ToScene
|
||||||
|
})
|
||||||
|
|
||||||
|
SomeScene.onSceneTransition(async (upd, state) => {
|
||||||
|
await state.exit()
|
||||||
|
return PropagationAction.Stop
|
||||||
|
})
|
||||||
|
|
||||||
|
SomeScene.onNewMessage(async (msg, state) => {
|
||||||
|
await msg.replyText('This will never be called')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The update which triggered the transition is passed to the handler, and
|
||||||
|
you can use it to decide whether to cancel the transition or not:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
SomeScene.onSceneTransition(async (upd, state) => {
|
||||||
|
if (upd.message.text === 'cancel') {
|
||||||
|
return PropagationAction.Stop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
271
docs/guide/dispatcher/state.md
Executable file
271
docs/guide/dispatcher/state.md
Executable file
|
@ -0,0 +1,271 @@
|
||||||
|
# State
|
||||||
|
|
||||||
|
Finite State Machine (**FSM** for short, or simply **State**) is a commonly used concept
|
||||||
|
when developing bots that allows the bot to "remember" its state,
|
||||||
|
which in turn makes the bot more interactive and user-friendly.
|
||||||
|
|
||||||
|
<!-- Full code example with FSM: TODO LINK -->
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
FSM is even more useful when used with [Scenes](scenes.html)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Dispatcher natively supports FSM. To set it up, simply pass
|
||||||
|
a [storage](#storage) to the constructor:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface BotState { ... }
|
||||||
|
|
||||||
|
const dp = Dispatcher.for<BotState>(tg, {
|
||||||
|
storage: new MemoryStateStorage()
|
||||||
|
})
|
||||||
|
// or, for children
|
||||||
|
const dp = Dispatcher.child<BotState>()
|
||||||
|
```
|
||||||
|
|
||||||
|
You **must** provide some state type in order to use FSM (in the example above,
|
||||||
|
`BotState`). You *can* use `any`, but this is not recommended.
|
||||||
|
|
||||||
|
Then, the update state argument will be available in every handler
|
||||||
|
that supports FSM (that is: `new_message`, `edit_message`, `message_group`, `callback_query`)
|
||||||
|
as well as to their filters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Type parameter for `Dispatcher` (in this case, `BotState`) is only
|
||||||
|
used as a *hint* for the compiler.
|
||||||
|
|
||||||
|
It is not checked at runtime.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Getting current state
|
||||||
|
|
||||||
|
To retrieve the current state, use `state.get`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
const current = await state.get()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, if there's no state stored, `null` is returned.
|
||||||
|
However, you can provide the default fallback state,
|
||||||
|
which will be used instead:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
const current = await state.get({ ... })
|
||||||
|
// or a function
|
||||||
|
const current = await state.get(() => ({ ... }))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating state
|
||||||
|
|
||||||
|
To update the state, use `state.set`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.set({ ... })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set a TTL, after which the newly set state
|
||||||
|
will be considered "stale" and removed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
// ttl = 1 hour
|
||||||
|
await state.set({ ... }, 3600)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also modify the existing state by only
|
||||||
|
providing the modification (under the hood, the library will
|
||||||
|
fetch the current state automatically):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.merge({ ... })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If the state can be empty, make sure to pass the default state,
|
||||||
|
otherwise an error will be thrown:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.merge({ ... }, defaultState)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing state
|
||||||
|
|
||||||
|
To remove currently stored state, use `state.delete`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg, state) => {
|
||||||
|
await state.delete()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related filters
|
||||||
|
|
||||||
|
As mentioned above, state (and its type!) is also available to the filters,
|
||||||
|
so you can make [custom filters](filters.html#custom-filters) that use it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
(msg, state) => state.get().then((res) => res?.action === 'ENTER_PASSWORD'),
|
||||||
|
async (msg, state) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
However, the above isn't very clean, so the library provides `filters.state`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.state((state) => state.action === 'ENTER_PASSWORD'),
|
||||||
|
async (msg, state: UpdateState<ActionEnterPassword>) => {
|
||||||
|
const current = await state.get()
|
||||||
|
// or, if you have strict null checks
|
||||||
|
const current = (await state.get())!
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that here we explicitly pass inner type, because due to TypeScript
|
||||||
|
limitations, we can't automatically derive state type from the predicate.
|
||||||
|
|
||||||
|
`filters.state` *does not* match empty state, instead,
|
||||||
|
use `filters.stateEmpty`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.stateEmpty,
|
||||||
|
async (msg, state) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keying
|
||||||
|
|
||||||
|
FSM may look like magic, but in fact it is not. Under the hood,
|
||||||
|
user's state is stored in the [storage](#storage), and the key is derived
|
||||||
|
from the update object.
|
||||||
|
|
||||||
|
By default, `defaultStateKeyDelegate` is used, which derives
|
||||||
|
the key as follows:
|
||||||
|
- If private chat, `msg.chat.id`
|
||||||
|
- If group chat, `msg.chat.id + '_' + msg.sender.id`
|
||||||
|
- If channel, `msg.chat.id`
|
||||||
|
- If callback query from a non-inline message:
|
||||||
|
- If in private chat (i.e. `upd.chatType === 'user'`), `upd.user.id`
|
||||||
|
- If in group/channel/supergroup (i.e. `upd.chatType !== 'user'`),
|
||||||
|
`upd.chatId + '_' + upd.user.id`
|
||||||
|
|
||||||
|
This is meant to be a pretty opinionated default, but you can use custom
|
||||||
|
keying mechanism too, if you want:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const customKey = (upd) => ...
|
||||||
|
|
||||||
|
const dp = new Dispatcher<BotState>(tg, storage, customKey)
|
||||||
|
// or, locally for a child dispatcher:
|
||||||
|
const dp = new Dispatcher<BotState>(customKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Getting state from outside
|
||||||
|
|
||||||
|
In some cases, you may need to access the state (and maybe even alter it)
|
||||||
|
outside of handlers, in the handler that does not support state, or using a different key.
|
||||||
|
|
||||||
|
You can do so by using `.getState`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const state = await dp.getState(await msg.getReply())
|
||||||
|
// you can also pass User/Chat instances:
|
||||||
|
const state = await dp.getState(msg.sender)
|
||||||
|
// and then, for example
|
||||||
|
await state.delete()
|
||||||
|
```
|
||||||
|
|
||||||
|
When providing an object, dispatcher will use its own keying
|
||||||
|
mechanism(s). You can provide a key manually to avoid that:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const target = msg.getReply()
|
||||||
|
const state = dp.getState(defaultStateKeyDelegate(target))
|
||||||
|
// or even manually
|
||||||
|
const state = await dp.getState(`${target.chat.id}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also provide a totally custom key
|
||||||
|
to store arbitrary data:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// tip: prefix the key with $ and then something unique
|
||||||
|
// to avoid clashing with FSM and Scenes
|
||||||
|
const state = await dp.getState<UserPref>(`$internal-user-pref:${userId}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
`getState` **DOES NOT** guarantee type of the state,
|
||||||
|
because it can not determine the origin of state key.
|
||||||
|
|
||||||
|
By default, it uses dispatcher's state type, but you can also
|
||||||
|
override this with type parameter:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const state = await dp.getState<SomeInternalState>(...)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Storage is the backend used by Dispatcher to store state related information.
|
||||||
|
A storage is a class that implements [`IStateStorageProvider`](https://ref.mtcute.dev/types/_mtcute_dispatcher.IStateStorageProvider.html).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dp = Dispatcher.for<BotState>(tg, { storage: new MemoryStorage() })
|
||||||
|
// or, locally for a child dispatcher:
|
||||||
|
const dp = Dispatcher.child<BotState>({ storage: new MemoryStorage() })
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite storage
|
||||||
|
|
||||||
|
You can re-use your existing SQLite storage for FSM:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SqliteStorage } from '@mtcute/sqlite'
|
||||||
|
import { SqliteStateStorage } from '@mtcute/dispatcher'
|
||||||
|
|
||||||
|
const storage = new SqliteStorage('my-account')
|
||||||
|
const tg = new TelegramClient({ ..., storage })
|
||||||
|
|
||||||
|
const dp = Dispatcher.for<BotState>(tg, {
|
||||||
|
storage: SqliteStateStorage.from(storage)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can create a new SQLite storage specifically for FSM:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SqliteStorageDriver } from '@mtcute/sqlite'
|
||||||
|
import { SqliteStateStorage } from '@mtcute/dispatcher'
|
||||||
|
|
||||||
|
const dp = Dispatcher.for<BotState>(tg, {
|
||||||
|
storage: new SqliteStateStorage(new SqliteStorageDriver('my-state'))
|
||||||
|
})
|
||||||
|
```
|
170
docs/guide/index.md
Executable file
170
docs/guide/index.md
Executable file
|
@ -0,0 +1,170 @@
|
||||||
|
# Quick start
|
||||||
|
|
||||||
|
This is a quick guide on how to get mtcute up and running as fast as possible.
|
||||||
|
|
||||||
|
## Node.js
|
||||||
|
|
||||||
|
For bots in Node.js, there's a special package that scaffolds a project for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm create @mtcute/bot my-awesome-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
Just follow the instructions and you'll get a working bot in no time!
|
||||||
|
|
||||||
|
### Manually
|
||||||
|
|
||||||
|
For existing projects, you'll probably want to add it manually, though.
|
||||||
|
|
||||||
|
> **Note**: mtcute is currently targeting TypeScript 5.0.
|
||||||
|
> If you are using an older version of TypeScript, please consider upgrading.
|
||||||
|
|
||||||
|
1. Get your API ID and Hash at
|
||||||
|
[https://my.telegram.org/apps](https://my.telegram.org/apps).
|
||||||
|
2. Install `@mtcute/node` package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/node
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Import the package and create a client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TelegramClient, html } from '@mtcute/node'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: API_ID,
|
||||||
|
apiHash: 'API_HASH'
|
||||||
|
})
|
||||||
|
|
||||||
|
const self = await tg.start({ ... }
|
||||||
|
console.log(`Logged in as ${self.displayName}`)
|
||||||
|
|
||||||
|
await tg.sendText('self', html`Hello from <b>MTCute</b>!`)
|
||||||
|
```
|
||||||
|
4. That's literally it! Happy hacking 🚀
|
||||||
|
|
||||||
|
### Native crypto addon
|
||||||
|
mtcute also provides `@mtcute/crypto-node` package, that implements
|
||||||
|
a native Node.js addon for crypto functions used in MTProto.
|
||||||
|
|
||||||
|
Using this addon improves overall library performance (especially when uploading/downloading files),
|
||||||
|
so it is advised that you install it as well:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/crypto-node
|
||||||
|
```
|
||||||
|
|
||||||
|
When using `@mtcute/node`, native addon is loaded automatically,
|
||||||
|
no extra steps are required.
|
||||||
|
|
||||||
|
## Bun
|
||||||
|
|
||||||
|
Experimental support for Bun is provided in `@mtcute/bun` package, and
|
||||||
|
Bun is also supported in `@mtcute/create-bot`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun create @mtcute/bot my-awesome-bot
|
||||||
|
# or add to an existing project
|
||||||
|
bun add @mtcute/bun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deno
|
||||||
|
|
||||||
|
Experimental support for Deno is provided in `@mtcute/deno` package, which is published
|
||||||
|
to the [jsr.io](https://jsr.io) registry:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TelegramClient } from 'jsr:@mtcute/deno'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: 123456,
|
||||||
|
apiHash: '0123456789abcdef0123456789abcdef',
|
||||||
|
storage: 'my-account' // will use sqlite-based storage
|
||||||
|
})
|
||||||
|
|
||||||
|
await tg.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno run -A --unstable-ffi your-script.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Deno is also supported in `@mtcute/create-bot`, which is only available in npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno run -A npm:@mtcute/create-bot my-awesome-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
For browsers, it is recommended to use [vite](https://vitejs.dev).
|
||||||
|
Webpack is probably also fine, but you may need to do some extra configuration.
|
||||||
|
|
||||||
|
For usage in browsers, mtcute provides an `@mtcute/web` package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/web
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
For vite, you'll need to deoptimize `@mtcute/wasm` (see [vite#8427](https://github.com/vitejs/vite/issues/8427)):
|
||||||
|
```ts
|
||||||
|
// in vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['@mtcute/wasm']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
Then, you can use it as you wish:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TelegramClient } from '@mtcute/web'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: 123456,
|
||||||
|
apiHash: '0123456789abcdef0123456789abcdef',
|
||||||
|
storage: 'my-account' // will use IndexedDB-based storage
|
||||||
|
})
|
||||||
|
|
||||||
|
tg.call({ _: 'help.getConfig' }).then((res) => console.log(res))
|
||||||
|
```
|
||||||
|
|
||||||
|
See also: [Tree-shaking](/guide/advanced/treeshaking.md)
|
||||||
|
|
||||||
|
## Other runtimes
|
||||||
|
|
||||||
|
mtcute strives to be as runtime-agnostic as possible, so it should work in any environment that supports
|
||||||
|
some basic ES2020 features.
|
||||||
|
|
||||||
|
In case your runtime of choice is not listed above, you can try using `@mtcute/core` directly
|
||||||
|
|
||||||
|
You will need to provide your own implementations of storage, networking and crypto - feel free to take a
|
||||||
|
look at web/node implementations for reference (or even extend them to better fit your needs, e.g. if some runtime
|
||||||
|
only partially supports some Node.js APIs).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TelegramClient } from '@mtcute/core/client.js'
|
||||||
|
import { setPlatform } from '@mtcute/core/platform.js'
|
||||||
|
|
||||||
|
setPlatform(new MyPlatform())
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
...,
|
||||||
|
storage: new MyStorage(),
|
||||||
|
crypto: new MyCrypto()
|
||||||
|
transport: () => new MyTransport(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info
|
||||||
|
You only need to call `setPlatform` once, before creating any clients.
|
||||||
|
Platform is set once globally and cannot be changed afterwards.
|
||||||
|
It is safe to call `setPlatform` multiple times, as long as the constructor is the same - it will be ignored if the platform is already set.
|
||||||
|
|
||||||
|
A good starting point might be to use [WebPlatform](https://ref.mtcute.dev/classes/_mtcute_web.WebPlatform.html),
|
||||||
|
since it implements everything in portable JavaScript.
|
||||||
|
:::
|
148
docs/guide/intro/errors.md
Executable file
148
docs/guide/intro/errors.md
Executable file
|
@ -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`).
|
230
docs/guide/intro/faq.md
Executable file
230
docs/guide/intro/faq.md
Executable file
|
@ -0,0 +1,230 @@
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
> except the only person who has ever asked them was my alter ego
|
||||||
|
|
||||||
|
Miscellaneous questions about the library and Telegram API as a whole
|
||||||
|
that don't really belong to any other topic.
|
||||||
|
|
||||||
|
If you are new to the library or Telegram API, you can skip this page
|
||||||
|
for now and return later.
|
||||||
|
|
||||||
|
## What is mtcute?
|
||||||
|
|
||||||
|
mtcute is a TypeScript library and framework for MTProto clients and bots.
|
||||||
|
|
||||||
|
It was written from scratch, however some logic was borrowed from TDLib and
|
||||||
|
similar projects like [Pyrogram](https://github.com/pyrogram/pyrogram) and
|
||||||
|
[Telethon](https://github.com/LonamiWebs/Telethon)
|
||||||
|
|
||||||
|
## Can I use it with JavaScript?
|
||||||
|
|
||||||
|
While you surely can, this is **not recommended**, since you would lose
|
||||||
|
any benefits TypeScript gives, including type checking and smart suggestions.
|
||||||
|
|
||||||
|
## How old is mtcute?
|
||||||
|
|
||||||
|
mtcute is pretty late to the party. Work on the library started
|
||||||
|
in early 2021, was first published on GitHub in spring, and the first
|
||||||
|
alpha release was published in the end of October 2023.
|
||||||
|
|
||||||
|
## Why mtcute?
|
||||||
|
|
||||||
|
First of all, apart from mtcute, there aren't many libraries for
|
||||||
|
MTProto in TypeScript (and even JS, for that matter).
|
||||||
|
|
||||||
|
There's [@mtproto/core](https://github.com/alik0211/mtproto-core), which is extremely
|
||||||
|
low-level and not TS-friendly; there's [GramJS](https://github.com/gram-js/gramjs),
|
||||||
|
which is a port of Telethon, and pretty much nothing else (at the moment of writing).
|
||||||
|
|
||||||
|
mtcute tries to provide a simple, elegant and type-safe API even for advanced
|
||||||
|
use cases, while also achieving good performance, staying up-to-date
|
||||||
|
with the current schema and providing near-complete documentation.
|
||||||
|
|
||||||
|
## Why are API keys needed for bots?
|
||||||
|
|
||||||
|
Because Telegram requires you to. Bot API internally is just a
|
||||||
|
TDLib instance running with its own API ID and hash for the connection.
|
||||||
|
|
||||||
|
## Webhooks?
|
||||||
|
|
||||||
|
Webhooks are only used by Bot API because there's no persistent connection
|
||||||
|
to the client to send updates.
|
||||||
|
|
||||||
|
mtcute, on the other hand, has a persistent server connection, and receives
|
||||||
|
updates from Telegram. This is **not** polling, because the updates are
|
||||||
|
sent by Telegram, and not requested by the library.
|
||||||
|
|
||||||
|
## What are the IPs of the DCs?
|
||||||
|
|
||||||
|
The primary DC (and primary Test DC) is always available at
|
||||||
|
[my.telegram.org](https://my.telegram.org).
|
||||||
|
|
||||||
|
List of other DCs is fetched by the client on demand. You can use
|
||||||
|
this code (it doesn't even require authorization):
|
||||||
|
|
||||||
|
<!-- TODO link to repl -->
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.call({ _: 'help.getConfig' })
|
||||||
|
.then((res) => console.log(res.dcOptions))
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to migrate an account?
|
||||||
|
|
||||||
|
You can't.
|
||||||
|
|
||||||
|
Even though Telegram docs state that the server might decide to do that,
|
||||||
|
this feature was confirmed to be unimplemented yet by Levin (source unavailable).
|
||||||
|
|
||||||
|
## Why does it work slower sometimes?
|
||||||
|
|
||||||
|
Because of Telegram's infrastructure.
|
||||||
|
|
||||||
|
Firstly, supergroups reside in the same DC as the (original) creator,
|
||||||
|
and if it is not the same as yours, it must first be
|
||||||
|
passed through that DC, which incurs some delay.
|
||||||
|
|
||||||
|
In the worst case, you, creator and other user are all in different DCs,
|
||||||
|
and it takes some time before the client receives an update about that.
|
||||||
|
|
||||||
|
The same goes for text-mentioning a user from another DC. It takes time for
|
||||||
|
the server to check access hash for that user since it is
|
||||||
|
stored in another DC, and thus sending a message takes more time.
|
||||||
|
|
||||||
|
Another reason is that updates in supergroups are sent in order of priority
|
||||||
|
([source](https://docs.pyrogram.org/faq#why-is-my-client-reacting-slowly-in-supergroups), unverified):
|
||||||
|
|
||||||
|
1. Creator
|
||||||
|
2. Administrators
|
||||||
|
3. Bots
|
||||||
|
4. Mentioned users
|
||||||
|
5. Recently online users
|
||||||
|
6. Everyone else
|
||||||
|
|
||||||
|
This is **not** affected by the library, and we can't do anything about it.
|
||||||
|
This can also be reproduced in TDLib and Bot API.
|
||||||
|
|
||||||
|
Your best bet on improving update latency is to:
|
||||||
|
- Use `openChat` method on the client to open chats you are interested in
|
||||||
|
- Periodically call `setOffline(false)` to tell Telegram that you are online
|
||||||
|
- Use an account in the same DC as the peer you are interacting with
|
||||||
|
|
||||||
|
## Why do I get PEER_ID_INVALID/MtPeerNotFoundError?
|
||||||
|
|
||||||
|
First, make sure that the ID you pass is actually correct.
|
||||||
|
|
||||||
|
If it is, then probably the problem is that you haven't met this
|
||||||
|
peer in the current session. As described in [Peers section](../topics/peers.html),
|
||||||
|
you need access hash to interact with the user, which is only sent by the server.
|
||||||
|
|
||||||
|
Think of how you find peers in normal clients - you search for usernames,
|
||||||
|
open them from dialogs, messages, members lists, etc. The same goes for
|
||||||
|
mtcute - you need to encounter the user before you can interact with them.
|
||||||
|
|
||||||
|
Some ideas on how you can fix this:
|
||||||
|
- Use [`findDialogs`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient.html#findDialogs) method
|
||||||
|
to iterate over all dialogs and find the one you need
|
||||||
|
- Use a username/phone number instead of ID
|
||||||
|
- Use [`getMessages`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient.html#getMessages) method
|
||||||
|
and fetch some message by the user (so mtcute caches the access hash)
|
||||||
|
- ...and a lot more ways to "meet" a user without interacting with them
|
||||||
|
|
||||||
|
### Why do I get "Peer ... is not found in local cache"?
|
||||||
|
|
||||||
|
For a similar reason. In some cases, Telegram will send an [incomplete](../topics/peers.html#incomplete-peers)
|
||||||
|
peer object, which is not enough to interact with the user.
|
||||||
|
|
||||||
|
mtcute tries its best to fill in the missing fields on demand
|
||||||
|
(whenever you use `.resolvePeer` or any other method that uses it under the hood),
|
||||||
|
but sometimes it unfortunately fails. There isn't much we can do about it :c
|
||||||
|
|
||||||
|
By the way, there's an [`isPeerAvailable`](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient.html#isPeerAvailable)
|
||||||
|
method that you can use to check if a peer is available, that *never* does any network requests.
|
||||||
|
Do note, however, that it is prone to false negatives, meaning that `resolvePeer` *might* still work
|
||||||
|
if that method returns `false`, but should always work if it returns `true`.
|
||||||
|
|
||||||
|
## Why is my verification code expired?
|
||||||
|
|
||||||
|
That's probably because you have sent it to someone.
|
||||||
|
|
||||||
|
To protect users from potential scammers, Telegram checks if the
|
||||||
|
outgoing message contains the verification code, and if it does,
|
||||||
|
immediately revokes it.
|
||||||
|
|
||||||
|
If you actually want to share it, consider somehow scrambling it,
|
||||||
|
for example: `12345` → `one two three four f1ve`
|
||||||
|
|
||||||
|
## How to avoid flood errors?
|
||||||
|
|
||||||
|
Write code with care, make less requests and do not abuse Telegram.
|
||||||
|
|
||||||
|
Nobody knows the exact reason why flood waits occur, and that is intentional.
|
||||||
|
Publicly available information often contradicts with other,
|
||||||
|
so there is no definitive answer.
|
||||||
|
|
||||||
|
You might find helpful information in
|
||||||
|
[this article](https://telegra.ph/So-your-bot-is-rate-limited-01-26).
|
||||||
|
|
||||||
|
This might also be an issue with the library. Currently mtcute doesn't do a lot of caching,
|
||||||
|
though it is on the roadmap. Particularly, issues may arise if your bot is high-load.
|
||||||
|
|
||||||
|
If you receive transport error -429, however,
|
||||||
|
please [let us know](https://t.me/mt_cute), so we can investigate further.
|
||||||
|
|
||||||
|
## How to not get banned?
|
||||||
|
|
||||||
|
Do not abuse Telegram. If you use the API for spamming, flooding,
|
||||||
|
faking counters or similar, you *will* be banned.
|
||||||
|
|
||||||
|
Accounts created using unofficial API clients are automatically
|
||||||
|
put under observation to prevent violation of the ToS
|
||||||
|
([source](https://core.telegram.org/api/obtaining_api_id#using-the-api-id)).
|
||||||
|
|
||||||
|
In some cases, even logging in with an unofficial client to an
|
||||||
|
account created using an official one may trigger the system
|
||||||
|
|
||||||
|
If you are planning to implement an active userbot, be extra careful,
|
||||||
|
avoid using VOIP numbers and try to minimize server load generated
|
||||||
|
(for example, implement local caching and rate limiting).
|
||||||
|
|
||||||
|
Anya from [@theyforcedme](https://t.me/theyforcedme) has shared her thoughts on this
|
||||||
|
in Pyrogram chat:
|
||||||
|
|
||||||
|
<EmbedPost link="PyrogramChat/69424" height="331px" />
|
||||||
|
|
||||||
|
## I was banned, help!
|
||||||
|
|
||||||
|
The library only does the things you told it to do. If you abuse Telegram,
|
||||||
|
then the ban is justified.
|
||||||
|
|
||||||
|
If you still have access to the phone number used when registering
|
||||||
|
the account, you can try contacting the Telegram support and ask them
|
||||||
|
to recover your account:
|
||||||
|
[recover@telegram.org](mailto:recover@telegram.org).
|
||||||
|
|
||||||
|
If you don't, then welp. Create a new account and be *even more* careful.
|
||||||
|
|
||||||
|
|
||||||
|
## How to know when I'm banned
|
||||||
|
|
||||||
|
When an account is banned, any network request will fail with `USER_DEACTIVATED_BAN`,
|
||||||
|
even simple ones like `getMe`.
|
||||||
|
|
||||||
|
You can also use a [network middleware](../advanced/net-middlewares.md#errors-in-middlewares)
|
||||||
|
to set up a client-wide listener for this error:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
...,
|
||||||
|
network: {
|
||||||
|
middlewares: [
|
||||||
|
networkMiddlewares.onRpcError((ctx, error) => {
|
||||||
|
if (error.errorMessage === 'USER_DEACTIVATED_BAN') {
|
||||||
|
console.log('account was banned :c')
|
||||||
|
tg.close()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
networkMiddlewares.basic()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
74
docs/guide/intro/mtproto-vs-bot-api.md
Executable file
74
docs/guide/intro/mtproto-vs-bot-api.md
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
# MTProto vs. Bot API
|
||||||
|
|
||||||
|
Unlike many existing libraries and frameworks for Telegram in
|
||||||
|
TypeScript/JavaScript, mtcute uses MTProto, and not Bot API.
|
||||||
|
|
||||||
|
This allows mtcute to be much more flexible and support
|
||||||
|
many features that Bot API does not.
|
||||||
|
|
||||||
|
## What is Bot API?
|
||||||
|
|
||||||
|
[**Telegram Bot API**](https://core.telegram.org/bots/api), or simply
|
||||||
|
**Bot API**, is an HTTP(s) interface provided and hosted by Telegram
|
||||||
|
that allows developers to build bots.
|
||||||
|
|
||||||
|
Under the hood, Bot API uses TDLib (which in turn uses MTProto API),
|
||||||
|
and simply provides HTTP methods that closely correlate with TDLib methods.
|
||||||
|
|
||||||
|
## What is MTProto API?
|
||||||
|
|
||||||
|
[MTProto](https://core.telegram.org/mtproto) is the custom protocol
|
||||||
|
invented by Nikolai Durov and his team, consisting of six ACM champions,
|
||||||
|
half of them Ph.Ds in math. It took them about two years
|
||||||
|
to roll out the current version of MTProto
|
||||||
|
([source](https://news.ycombinator.com/item?id=6916860)).
|
||||||
|
It is used to communicate between clients and Telegram servers.
|
||||||
|
|
||||||
|
On the surface, MTProto API is basically an RPC,
|
||||||
|
where MTProto and TL are used behind the scenes
|
||||||
|
to serialize, encrypt and process the requests.
|
||||||
|
~~*Sounds like VK API with extra steps, right?*~~
|
||||||
|
|
||||||
|
## Why MTProto?
|
||||||
|
|
||||||
|
MTProto clients (like mtcute) connect directly to Telegram, removing
|
||||||
|
the need to use additional transport layers like HTTP, polling or webhooks.
|
||||||
|
This means **less overhead**, since the data is sent directly to you,
|
||||||
|
and not passed through the Bot API server and then sent to you via HTTP:
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="/assets/mtproto_vs_botapi.svg"
|
||||||
|
adaptive="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Apart from smaller overhead, using MTProto has many other advantages, including:
|
||||||
|
|
||||||
|
| | Bot API | MTProto |
|
||||||
|
|---|---|---|
|
||||||
|
| Userbots | Only bots | Both bots and users.
|
||||||
|
| Files | 20 MB download, 50 MB upload. | No limits (except global limit of 2000 MB)
|
||||||
|
| Objects | Often brief and non-exhaustive | Exposes anything you can think of
|
||||||
|
| Updates | Only a limited subset | Updates about virtually anything that had happened
|
||||||
|
| Errors | Often non-informative (e.g. slow mode is the same as flood wait) and not machine-readable | Informative and simple to use
|
||||||
|
| Version | Receives updates slower | Updates with the TL layer
|
||||||
|
| Compatibility | Updates randomly, you have to prepare for the upcoming breaking changes.<br/>Attempts to stay backwards-compatible, leading to weird hacks | You can stay on older version for as long as you need
|
||||||
|
|
||||||
|
> **Note**: the above table assumes official Bot API instance, hosted at `api.telegram.org`.
|
||||||
|
> Self-hosted and/or custom Bot API instances bypass some of these limitations.
|
||||||
|
|
||||||
|
## Why not MTProto?
|
||||||
|
|
||||||
|
Everything has its drawbacks though.
|
||||||
|
|
||||||
|
Using mtcute instead of Bot API (or TDLib) for high-load bots might currently
|
||||||
|
not be the best idea, since TDLib caches basically everything, while mtcute doesn't.
|
||||||
|
This is our primary focus for the upcoming releases, though.
|
||||||
|
|
||||||
|
If your bot is high-load, and you receive errors like 500 and 429, this
|
||||||
|
definitely means a problem on mtcute side. Please
|
||||||
|
[let us know](https://t.me/mt_cute), so we can
|
||||||
|
investigate further.
|
||||||
|
|
||||||
|
Another drawback is that due to mtcute being an enthusiast project,
|
||||||
|
it does not offer the same level of robustness and support as Bot API,
|
||||||
|
and is missing some features that Bot API has.
|
222
docs/guide/intro/sign-in.md
Executable file
222
docs/guide/intro/sign-in.md
Executable file
|
@ -0,0 +1,222 @@
|
||||||
|
# Signing in
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Before we start, **please do not** use mtcute to abuse Telegram services
|
||||||
|
or harm other users by any means (including spamming, scraping,
|
||||||
|
scamming, etc.)
|
||||||
|
|
||||||
|
Bots are meant to help people, not hurt them, so
|
||||||
|
**please** make sure you don't :)
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## API Keys
|
||||||
|
|
||||||
|
Before you can use this library (or any other MTProto library, for that matter),
|
||||||
|
you need to obtain API ID and API Hash from Telegram:
|
||||||
|
|
||||||
|
1. Go to [https://my.telegram.org/apps](https://my.telegram.org/apps)
|
||||||
|
and log in with your Telegram account
|
||||||
|
2. Fill out the form to create a new application
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
You can leave URL field empty.
|
||||||
|
App name and short name can (currently) be changed later.
|
||||||
|
|
||||||
|
Note that you **will not** be able to create
|
||||||
|
another application using the same account.
|
||||||
|
:::
|
||||||
|
|
||||||
|
3. Press *Create Application*, and you'll see your `api_id` and `api_hash`.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
Be careful with your API hash. It can not be revoked.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Signing in
|
||||||
|
|
||||||
|
Now that we have got our API keys, we can sign in into our account:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Replace with your own values
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: API_ID,
|
||||||
|
apiHash: 'API_HASH'
|
||||||
|
})
|
||||||
|
|
||||||
|
const self = await tg.start({
|
||||||
|
phone: () => tg.input('Phone > '),
|
||||||
|
code: () => tg.input('Code > '),
|
||||||
|
password: () => tg.input('Password > ')
|
||||||
|
})
|
||||||
|
console.log(`Logged in as ${self.displayName}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`tg.input` is a tiny wrapper over `readline` module in Node.js,
|
||||||
|
that will ask you for input in the console.
|
||||||
|
|
||||||
|
It's not available in `@mtcute/core`, since it is platform-agnostic
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## Signing in as a bot
|
||||||
|
|
||||||
|
You can also use mtcute for bots (created via [@BotFather](https://t.me/BotFather)).
|
||||||
|
You will still need API ID and Hash, though:
|
||||||
|
|
||||||
|
```ts{10}
|
||||||
|
// Replace with your own values
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: API_ID,
|
||||||
|
apiHash: 'API_HASH'
|
||||||
|
})
|
||||||
|
|
||||||
|
const self = await tg.start({
|
||||||
|
botToken: '12345678:0123456789abcdef0123456789abcdef'
|
||||||
|
})
|
||||||
|
console.log(`Logged in as ${self.displayName}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storing your API ID and Hash
|
||||||
|
|
||||||
|
In the examples above, we hard-coded the API keys. It works
|
||||||
|
fine, but it is better to not keep that kind of stuff in the code,
|
||||||
|
let alone publish them to public repositories.
|
||||||
|
|
||||||
|
Instead, it is a good practice to use environment variables
|
||||||
|
and a `.env` that will contain them.
|
||||||
|
You can load it then using [dotenv-cli](https://npmjs.org/package/dotenv-cli):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
API_ID=123456
|
||||||
|
API_HASH=0123456789abcdef0123456789abcdef
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: process.env.API_ID,
|
||||||
|
apiHash: process.env.API_HASH
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotenv ts-node your-file.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a proxy
|
||||||
|
|
||||||
|
When using Node.js, you can also connect to Telegram through proxy.
|
||||||
|
This is particularly useful in countries like Iran or Russia, where
|
||||||
|
Telegram might be limited.
|
||||||
|
|
||||||
|
To learn how to set up a connection through proxy,
|
||||||
|
refer to [Transport](../topics/transport.html#http-s-proxy-transport) documentation
|
||||||
|
|
||||||
|
## Manual sign in
|
||||||
|
|
||||||
|
So far we've only discussed the `.start` helper method.
|
||||||
|
|
||||||
|
While they do provide some flexibility and convenience, they are not always
|
||||||
|
suitable for every use case. For example, when building a web application,
|
||||||
|
you might want to use a different way to ask for user input.
|
||||||
|
|
||||||
|
In that case, you can use the underlying sign-in methods directly
|
||||||
|
|
||||||
|
<!-- TODO link to full example -->
|
||||||
|
|
||||||
|
First, check if you are already signed in:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function checkSignedIn() {
|
||||||
|
try {
|
||||||
|
// Try calling any method that requires authorization
|
||||||
|
// (getMe is the simplest one and likely the most useful,
|
||||||
|
// but you can use any other)
|
||||||
|
return await tg.getMe()
|
||||||
|
} catch (e) {
|
||||||
|
if (tl.RpcError.is(e, 'AUTH_KEY_UNREGISTERED')) {
|
||||||
|
// Not signed in, continue
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
// Some other error, rethrow
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are not signed in, you should first use `.sendCode` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// phone can be in pretty much any format,
|
||||||
|
// mtcute will automatically normalize it
|
||||||
|
const phone = '+7 999 123 4567'
|
||||||
|
const code = await tg.sendCode({ phone })
|
||||||
|
```
|
||||||
|
|
||||||
|
The `code` object will contain [information about the sent code](https://ref.mtcute.dev/classes/_mtcute_core.index.SentCode.html),
|
||||||
|
including the `phoneCodeHash` that you will need to use later.
|
||||||
|
|
||||||
|
Now, you need to ask the user for the code and call `.signIn` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const code = '12345' // code from user input
|
||||||
|
|
||||||
|
const user = await tg.signIn({
|
||||||
|
phone,
|
||||||
|
phoneCodeHash: code.phoneCodeHash,
|
||||||
|
phoneCode: code
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This method may either return right away, or throw one of:
|
||||||
|
- `SESSION_PASSWORD_NEEDED` if the account has 2FA enabled, and you should [enter the password](#handling-2fa)
|
||||||
|
- `PHONE_CODE_INVALID` if the code entered was invalid
|
||||||
|
- `PHONE_CODE_EXPIRED` if the code has expired, and you should [resend it](#resending-code)
|
||||||
|
|
||||||
|
### Handling 2FA
|
||||||
|
|
||||||
|
If the account has 2FA enabled, you will need to ask the user for the password, and then call `.checkPassword`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const password = 'hunter2' // password from user input
|
||||||
|
|
||||||
|
const user = await tg.checkPassword(password)
|
||||||
|
```
|
||||||
|
|
||||||
|
In case of invalid password, this method will throw `PASSWORD_HASH_INVALID`.
|
||||||
|
|
||||||
|
### Resending code
|
||||||
|
|
||||||
|
In some cases, the code may expire before the user enters it, or it may never arrive,
|
||||||
|
so you may want to resend it. To do that, you can use `.resendCode` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
code = await tg.resendCode({
|
||||||
|
phone,
|
||||||
|
phoneCodeHash: code.phoneCodeHash
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
You can know beforehand the type of the next code that will be sent
|
||||||
|
using the [`code.nextType`](https://ref.mtcute.dev/classes/_mtcute_core.index.SentCode.html#nextType) field
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
If you want the updates to be processed, you should manually use `startUpdatesLoop` once you are signed in:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
if (await checkSignedIn()) {
|
||||||
|
tg.startUpdatesLoop()
|
||||||
|
} else {
|
||||||
|
// some sign in logic
|
||||||
|
|
||||||
|
tg.startUpdatesLoop()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`tg.start` method does this automatically for you.
|
171
docs/guide/intro/updates.md
Executable file
171
docs/guide/intro/updates.md
Executable file
|
@ -0,0 +1,171 @@
|
||||||
|
# Getting updates
|
||||||
|
|
||||||
|
## What is an Update?
|
||||||
|
|
||||||
|
Updates are events that happen in Telegram and sent
|
||||||
|
to the clients. This includes events about a new message, joined chat member,
|
||||||
|
inline keyboard callbacks, etc.
|
||||||
|
|
||||||
|
In a bot environment, a [Dispatcher](/guide/dispatcher/intro.html) is normally used to handle the updates.
|
||||||
|
|
||||||
|
### Don't need them?
|
||||||
|
|
||||||
|
Sometimes, you might not really need any updates.
|
||||||
|
Then, just disable them in `TelegramClient` parameters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
disableUpdates: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
The parameters themselves will be explained a bit below, for now let's just focus on how they are passed.
|
||||||
|
|
||||||
|
`TelegramClient` has updates handling turned on by default, and you can configure it using `updates` parameter
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
updates: {
|
||||||
|
messageGroupingInterval: 250,
|
||||||
|
catchUp: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The updates themselves are dispatched on the client as events (see [reference](https://ref.mtcute.dev/classes/_mtcute_core.index.TelegramClient.html#on)):
|
||||||
|
```ts
|
||||||
|
tg.on('new_message', (msg) => {
|
||||||
|
console.log(msg.text)
|
||||||
|
})
|
||||||
|
|
||||||
|
// You can also handle any supported update at once:
|
||||||
|
tg.on('update', (upd) => {
|
||||||
|
if (upd.name === 'new_message') {
|
||||||
|
console.log(upd.data.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// As well as raw MTProto updates:
|
||||||
|
tg.on('raw_update', (upd, users, chats) => {
|
||||||
|
console.log(upd._)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Client events are based on EventEmitter. It expects handlers to be synchronous,
|
||||||
|
so if you want to do something async, make sure to also handle the errors:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.on('new_message', async (msg) => {
|
||||||
|
try {
|
||||||
|
await msg.answerText('test')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Missed updates
|
||||||
|
|
||||||
|
When your client is offline, updates are still stored by Telegram,
|
||||||
|
and can be fetched later (client "catches up" with the updates).
|
||||||
|
|
||||||
|
When back online, mtcute may "catch up", fetch any missed updates and
|
||||||
|
process them. To do that, pass `catchUp: true` parameter as shown above:
|
||||||
|
|
||||||
|
### Message grouping
|
||||||
|
|
||||||
|
As you may already know, albums handling in Telegram is not very trivial, as they are sent by the server
|
||||||
|
as separate messages. To make it easier to handle them, you may opt into grouping them automatically.
|
||||||
|
|
||||||
|
To do that, pass `messageGroupingInterval` as shown above. It is a number of milliseconds to wait
|
||||||
|
for the next message in the album. If the next message is not received in that time, the album is
|
||||||
|
considered complete and dispatched as a single `message_group` object.
|
||||||
|
|
||||||
|
The recommended value is `250` ms.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
This **will** introduce delays of up to `messageGroupingInterval` ms for every message with media groups,
|
||||||
|
and may sometimes break ordering. Use with caution.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Opening chats
|
||||||
|
|
||||||
|
For Telegram to properly send updates for channels (e.g. for channels that you are not a member of,
|
||||||
|
and for more consistent updates for channels that you are a member of), you need to open them first.
|
||||||
|
|
||||||
|
This is done by calling `openChat` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.openChat('durov')
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you're done, you can close the chat by calling `closeChat`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.closeChat('durov')
|
||||||
|
```
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
Opening a chat with `openChat` method will make the library make additional requests every so often.
|
||||||
|
|
||||||
|
Which means that you should **avoid opening more than 5-10 chats at once**, as it will probably trigger
|
||||||
|
server-side limits and you might start getting transport errors or even get banned.
|
||||||
|
|
||||||
|
If missing *some* updates from *some* channels (or having them arrive a bit late) is acceptable for you,
|
||||||
|
you might want to consider not opening them at all. You will still receive updates for channels
|
||||||
|
you are a member of, but they might be delayed.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Dispatcher
|
||||||
|
|
||||||
|
Dispatcher is a class that dispatches client events to registered handlers,
|
||||||
|
while also filtering and propagating them as needed.
|
||||||
|
|
||||||
|
You can think of it as some sort of framework that allows you to do everything
|
||||||
|
more declaratively and handles most of the boilerplate related to state.
|
||||||
|
|
||||||
|
Dispatcher is provided by `@mtcute/dispatcher` package.
|
||||||
|
|
||||||
|
### Registering
|
||||||
|
|
||||||
|
Dispatcher is quite a powerful thing, and we will explore it in-depth in
|
||||||
|
a separate section. For now, let's just register a dispatcher and add a simple handler:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient(...)
|
||||||
|
const dp = new Dispatcher(tg)
|
||||||
|
|
||||||
|
dp.onNewMessage(async (msg) => {
|
||||||
|
await msg.forwardTo('me')
|
||||||
|
})
|
||||||
|
|
||||||
|
await tg.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
Pretty simple, right? We have registered a "new message" handler and made
|
||||||
|
it forward any new messages to "Saved Messages".
|
||||||
|
|
||||||
|
### Filters
|
||||||
|
|
||||||
|
Example above is pretty useless in real world, though. Most of the
|
||||||
|
time you will want to *filter* the events, and only react to some of them.
|
||||||
|
|
||||||
|
Let's make our code only handle messages containing media:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.media,
|
||||||
|
async (msg) => {
|
||||||
|
await msg.forwardTo('me')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Filters can do a lot more than that, and
|
||||||
|
we will cover them further [later](../dispatcher/filters.html).
|
126
docs/guide/topics/conversation.md
Executable file
126
docs/guide/topics/conversation.md
Executable file
|
@ -0,0 +1,126 @@
|
||||||
|
# Conversation
|
||||||
|
|
||||||
|
A conversation is an object that represents some chat and all messages
|
||||||
|
inside it since the start of the conversation until it is stopped.
|
||||||
|
|
||||||
|
<!-- This is particularly useful when programmatically interacting with bots,
|
||||||
|
as you can see in this example TODO LINK -->
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
**DO NOT** use conversations to interact with users. Conversations are
|
||||||
|
designed to be used in a one-shot fashion, and will not work properly when used with users,
|
||||||
|
as they don't remember any state.
|
||||||
|
|
||||||
|
This may change in the future, but for now you should use [scenes](/guide/dispatcher/scenes) instead.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Create a `Conversation` object and pass `TelegramClient` and the peer you want
|
||||||
|
there:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const conv = new Conversation(tg, 'stickers')
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, use `.with()`, and inside it you can interact with the other party:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.with(async () => {
|
||||||
|
await conv.sendText('Hello!')
|
||||||
|
await conv.waitForResponse()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`.with()` is a simple wrapper that automatically calls
|
||||||
|
`.start()` and `.stop()` for you, essentially this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.start()
|
||||||
|
try {
|
||||||
|
// ... code ...
|
||||||
|
} catch (e) {}
|
||||||
|
conv.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling `.stop()` is vitally important, failing to do so *will* lead
|
||||||
|
to memory leaks, so use `.with()` whenever possible.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Sending messages
|
||||||
|
|
||||||
|
To send messages, use `conv.sendText`, `conv.sendMedia` and `conv.sendMediaGroup`
|
||||||
|
methods:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.sendText('Hello!')
|
||||||
|
await conv.sendMedia('BQACAgEAAx...Z2mGB8E')
|
||||||
|
await conv.sendMediaGroup(['BQACAgEAAx...Z2mGB8E', 'BQACAgEAAx...Z2mGB8E'])
|
||||||
|
```
|
||||||
|
|
||||||
|
**DO NOT** use client methods like `tg.sendText`, because conversation state
|
||||||
|
won't properly be updated.
|
||||||
|
|
||||||
|
## Waiting for events
|
||||||
|
|
||||||
|
Currently, `Conversation` supports waiting for new messages, edits and read
|
||||||
|
acknowledgments:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.sendText('Hello!')
|
||||||
|
await conv.waitForNewMessage()
|
||||||
|
|
||||||
|
await conv.sendText('Hello!')
|
||||||
|
await conv.waitForRead()
|
||||||
|
|
||||||
|
await conv.sendText('Hello!')
|
||||||
|
await conv.waitForNewMessage()
|
||||||
|
await conv.sendText('Now edit that')
|
||||||
|
await conv.waitForEdit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smarter waiting
|
||||||
|
|
||||||
|
Instead of `waitForNewMessage`, you can use `waitForResponse`
|
||||||
|
or `waitForReply`.
|
||||||
|
|
||||||
|
`waitForResponse` will wait for a message which was sent strictly
|
||||||
|
after the given message (by default, the last one)
|
||||||
|
|
||||||
|
`waitForReply` will wait for a message which is a reply to the
|
||||||
|
given message (by default, the last one)
|
||||||
|
|
||||||
|
### Timeouts
|
||||||
|
|
||||||
|
By default, for every `waitFor*` method a timeout of 15 seconds is applied.
|
||||||
|
If the event does not occur within those, `MtTimeoutError` is thrown.
|
||||||
|
|
||||||
|
This is a pretty generous default when interacting with bots.
|
||||||
|
However, if you are interacting with other people (which you shouldn't, use
|
||||||
|
[scenes](/guide/dispatcher/scenes) instead), you may want to raise the timeout or
|
||||||
|
disable it altogether.
|
||||||
|
|
||||||
|
To do that, you can pass `timeout` parameter to the functions:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.waitForNewMessage(filters.any, 60_000) // 60 sec timeout
|
||||||
|
await conv.waitForResponse(filters.any, { timeout: null }) // disable timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filters
|
||||||
|
|
||||||
|
You can apply dispatcher filters to the `waitFor*` methods:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.waitForNewMessage(filters.regex(/welcome/i))
|
||||||
|
```
|
||||||
|
|
||||||
|
Since dispatcher filters are simple functions, you can also use custom filters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await conv.waitForResponse((msg) => msg.id > 42)
|
||||||
|
```
|
||||||
|
|
||||||
|
If a newly received message (or other update) does not match the filter,
|
||||||
|
it will be ignored.
|
261
docs/guide/topics/files.md
Executable file
261
docs/guide/topics/files.md
Executable file
|
@ -0,0 +1,261 @@
|
||||||
|
# Files
|
||||||
|
|
||||||
|
Working with files and media is important in almost any bot,
|
||||||
|
and mtcute makes it very simple.
|
||||||
|
|
||||||
|
## Downloading files
|
||||||
|
|
||||||
|
To download a file, just use `downloadIterable`, `downloadStream`,
|
||||||
|
`downloadBuffer`or `downloadToFile` method on the object that represents a file, for example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.on('new_message', async (msg) => {
|
||||||
|
if (msg.media?.type === 'photo') {
|
||||||
|
await tg.downloadToFile('download.jpg', msg.media)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in case you don't have the object, you can download a file
|
||||||
|
by its [File ID](#file-ids):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.downloadToFile('download.jpg', 'AgACAgEAAxkBA...33gAgABHwQ')
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`downloadToFile` is only available for Node.js.
|
||||||
|
Other methods are environment agnostic.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Uploading files
|
||||||
|
|
||||||
|
In Telegram, files are primarily used to back a media
|
||||||
|
(a photo, a chat avatar, a document, etc.)
|
||||||
|
|
||||||
|
Client methods (like `setProfilePhoto`) accept `InputFileLike`, which is
|
||||||
|
a type containing information on how to upload a certain file, or
|
||||||
|
to re-use an existing file (see [File IDs](#file-ids)).
|
||||||
|
|
||||||
|
`InputFileLike` can be one of:
|
||||||
|
- `Buffer`, in this case contents of the buffer will be uploaded as file
|
||||||
|
- Readable stream (both.js and Web are supported)
|
||||||
|
- `File` from the Web API
|
||||||
|
- `Response` from `window.fetch` or `node-fetch`
|
||||||
|
- `UploadedFile`, see [Uploading files manually](#uploading-files-manually)
|
||||||
|
- `string` with URL: `https://example.com/file.jpg` (only supported sometimes)
|
||||||
|
- `string` with file path (only Node.js): `file:path/to/file.jpg` (note the `file:` prefix)
|
||||||
|
- `string` with [File ID](#file-ids)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.setProfilePhoto('photo', Buffer.from(...))
|
||||||
|
await tg.setProfilePhoto('photo', fs.createReadableStream('assets/renge.jpg'))
|
||||||
|
await tg.setProfilePhoto('photo', await fetch('https://nyanpa.su/renge.jpg'))
|
||||||
|
await tg.setProfilePhoto('photo', await tg.uploadFile(...))
|
||||||
|
await tg.setProfilePhoto('photo', 'file:assets/renge.jpg')
|
||||||
|
await tg.setProfilePhoto('photo', 'BQACAgEAAx...Z2mGB8E')
|
||||||
|
|
||||||
|
// setProfilePhoto and some other methods don't support URLs (TG limitation)
|
||||||
|
// await tg.setProfilePhoto('photo', 'https://nyanpa.su/renge.jpg')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sending media
|
||||||
|
|
||||||
|
As mentioned earlier, most of the time file is used as a media in a Message.
|
||||||
|
Sending media is incredibly easy with mtcute - you simply
|
||||||
|
call `sendMedia` and provide `InputMediaLike`.
|
||||||
|
|
||||||
|
`InputMediaLike` can be constructed manually, or using one of the
|
||||||
|
builder functions exported in [`InputMedia` namespace](https://ref.mtcute.dev/modules/_mtcute_core.index.InputMedia.html):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendMedia('me', InputMedia.photo('file:assets/welcome.jpg'))
|
||||||
|
|
||||||
|
await tg.sendMedia('me', InputMedia.auto('BQACAgEAAx...Z2mGB8E', {
|
||||||
|
caption: 'Backup of the project'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// when using InputMedia.auto with file IDs and not using
|
||||||
|
// additonal parameters like `caption`, you can simply pass it as a string
|
||||||
|
await tg.sendMedia('me', 'CAADAgADLgZZCKNgg2JpAg')
|
||||||
|
```
|
||||||
|
|
||||||
|
First argument is `InputFileLike`, so you can use
|
||||||
|
[any supported type](#files) when using it.
|
||||||
|
|
||||||
|
Using `InputMedia` instead of separate methods (like in Bot API) allows easily
|
||||||
|
switching to `sendMediaGroup`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendMediaGroup('me', [
|
||||||
|
InputMedia.photo('file:assets/welcome.jpg'),
|
||||||
|
InputMedia.auto('AgACAgEAAxkBA...6D2AgABHwQ'),
|
||||||
|
'AgACAgEAAxkBA...33gAgABHwQ',
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Even though you *technically can* pass media groups
|
||||||
|
of different types, do not mix them up (you can mix photos and videos),
|
||||||
|
since that would result in a server-sent error.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## File IDs
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
File IDs are implemented in `@mtcute/file-id` package, which
|
||||||
|
can be easily used outside of mtcute.
|
||||||
|
:::
|
||||||
|
|
||||||
|
If you ever worked with the Bot API, you probably already know what File ID
|
||||||
|
is. If you don't, it is a unique string identifier that is used to represent
|
||||||
|
files already stored on Telegram servers.
|
||||||
|
|
||||||
|
It comes in very handy when dealing with files programmatically, since
|
||||||
|
storing a simple string is much easier than storing a lot of different
|
||||||
|
TL objects, parsing them, normalizing, converting, etc.
|
||||||
|
|
||||||
|
mtcute File IDs are a port of TDLib's File IDs (which are also used in Bot API),
|
||||||
|
which means they are **100% interoperable** with TDLib and Bot API.
|
||||||
|
|
||||||
|
They do have some limitations though:
|
||||||
|
- You can't get a File ID until you upload a file and use it somewhere, e.g.
|
||||||
|
as a message media (or you can use [uploadMedia](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient.html#uploadMedia)).
|
||||||
|
- When sending by File ID, you can't change type of the file
|
||||||
|
(i.e. if this is a video, you can't send it as a document) or any meta
|
||||||
|
information like duration.
|
||||||
|
- File ID cannot be used for thumbnails.
|
||||||
|
- When sending a photo by File ID of one of its sizes, all sizes will be re-used.
|
||||||
|
- File ID is unique per-user and can't be used on another account
|
||||||
|
(however, some developers seem to bypass this with userbots).
|
||||||
|
- For user accounts, File ID expire (after ~24-48 hours).
|
||||||
|
- The same file may have different *valid* File IDs.
|
||||||
|
|
||||||
|
::: details Why is this?
|
||||||
|
All of the above points can be explained fairly easily, but to understand
|
||||||
|
them you'll need to have some understanding of how files in MTProto work.
|
||||||
|
|
||||||
|
- File ID contains document/photo ID and file reference, which are not
|
||||||
|
available until that file is uploaded and used somehow, or
|
||||||
|
`messages.uploadMedia` method is used.
|
||||||
|
- Photos and documents are completely different types in MTProto.
|
||||||
|
As for documents, when using `inputDocument`, Telegram [does not allow](https://corefork.telegram.org/constructor/inputDocument)
|
||||||
|
setting new document attributes, and re-uses the attributes from the
|
||||||
|
original document. They contain file type (video/audio/voice/etc),
|
||||||
|
file name, duration (for video/audio), and so on.
|
||||||
|
- For thumbnails, Telegram [requires](https://corefork.telegram.org/constructor/inputMediaUploadedDocument)
|
||||||
|
clients to use `InputFile`, which can only contain newly uploaded files.
|
||||||
|
- When sending photos, File ID is transformed to [inputPhoto](https://corefork.telegram.org/constructor/inputPhoto),
|
||||||
|
which does not contain information about sizes. When downloading, however,
|
||||||
|
it is transformed to [inputPhotoFileLocation](https://corefork.telegram.org/constructor/inputPhotoFileLocation),
|
||||||
|
which does contain `thumbSize` parameter
|
||||||
|
- File IDs contain what is known as a File reference, which is unique per-user.
|
||||||
|
It seems that bots can sometimes use any file reference for files that
|
||||||
|
they have recently encountered, but this is pretty unreliable and should not be used.
|
||||||
|
- File reference [may expire](https://core.telegram.org/api/file_reference),
|
||||||
|
making the File ID unusable. Telegram claims that this does not happen to bots though
|
||||||
|
([source](https://core.telegram.org/bots/faq#can-i-count-on-file-ids-to-be-persistent))
|
||||||
|
- File reference or File ID format might change over time.
|
||||||
|
:::
|
||||||
|
|
||||||
|
File ID is available in `.fileId` field:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.on('new_message', async (msg) => {
|
||||||
|
if (msg.media?.type === 'photo') {
|
||||||
|
console.log(msg.media.fileId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unique File ID
|
||||||
|
|
||||||
|
This is also a concept ported from TDLib. It is similar to
|
||||||
|
File ID, but built in such a way that it uniquely defines
|
||||||
|
some file, i.e. the same file always has the same Unique File ID,
|
||||||
|
and different files have different Unique File IDs.
|
||||||
|
|
||||||
|
It can't be used to download a file, but it is the same
|
||||||
|
for different users/bots.
|
||||||
|
|
||||||
|
Unique File ID is available in `.uniqueFileId` field:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.on('new_message', async (msg) => {
|
||||||
|
if (msg.media?.type === 'photo') {
|
||||||
|
console.log(msg.media.uniqueFileId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
## Uploading files manually
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
This method is rarely needed outside mtcute, simply
|
||||||
|
because most of the methods already handle uploading
|
||||||
|
automatically, and for Raw API you can use
|
||||||
|
`normalizeInputFile` and `normalizeInputMedia`
|
||||||
|
:::
|
||||||
|
|
||||||
|
To upload files, Client provides a simple method `uploadFile`.
|
||||||
|
It has a bunch of options, but the only required one is `file`.
|
||||||
|
|
||||||
|
### Uploading a local file
|
||||||
|
|
||||||
|
To upload a local file from Node.js, you can either provide file path,
|
||||||
|
or a readable stream (note that here you don't need `file:` prefix):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.uploadFile({ file: 'assets/renge.jpg' })
|
||||||
|
// or
|
||||||
|
await tg.uploadFile({ file: fs.createReadStream('assets/renge.jpg') })
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Uploading from `Buffer`
|
||||||
|
|
||||||
|
To upload a `Buffer` as a file, simply pass it as `file`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const data = Buffer.from(...)
|
||||||
|
await tg.uploadFile({ file: data })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uploading from stream
|
||||||
|
|
||||||
|
To upload from a stream, pass it as `file`, and provide file size
|
||||||
|
whenever possible:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.uploadFile({
|
||||||
|
file: stream,
|
||||||
|
fileSize: streamExpectedLength
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uploading from the Internet
|
||||||
|
|
||||||
|
To upload a file from the Internet, you can use `window.fetch`
|
||||||
|
and simply pass the response object:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.uploadFile({ file: await fetch('https://nyanpa.su/renge.jpg') })
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using some other library for HTTP(S), it probably also supports
|
||||||
|
returning streams, but you'll have to extract meta from the response object
|
||||||
|
manually. Rough example for [axios](https://npmjs.com/package/axios):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function uploadFileAxios(tg: TelegramClient, config: AxiosRequestConfig) {
|
||||||
|
const response = await axios({ ...config, responseType: 'stream' })
|
||||||
|
return tg.uploadFile({
|
||||||
|
file: response.data,
|
||||||
|
fileSize: parseInt(response.headers['content-length'] || 0),
|
||||||
|
fileMime: response.headers['content-type'],
|
||||||
|
// fileName: ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await uploadFileAxios(tg, { url: 'https://nyanpa.su/renge.jpg' })
|
||||||
|
await tg.sendMedia('me', InputMedia.photo(file))
|
||||||
|
```
|
55
docs/guide/topics/inline-mode.md
Executable file
55
docs/guide/topics/inline-mode.md
Executable file
|
@ -0,0 +1,55 @@
|
||||||
|
# Inline mode
|
||||||
|
|
||||||
|
Users can interact with bots using inline queries, by starting
|
||||||
|
a message with bot's username and then typing their query.
|
||||||
|
|
||||||
|
## Implementing inline mode
|
||||||
|
|
||||||
|
First, you'll need to enable inline mode in [@BotFather](https://t.me/botfather),
|
||||||
|
either in `/mybots` or with `/setinline`
|
||||||
|
|
||||||
|
Then, you can use Dispatcher to [implement inline mode](../dispatcher/inline-mode.html)
|
||||||
|
for your bot.
|
||||||
|
|
||||||
|
Instead of Dispatcher, you can also use client events (however you will miss
|
||||||
|
features that Dispatcher provides):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.on('inline_query', async (query) => {
|
||||||
|
await query.answer([])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using inline mode
|
||||||
|
|
||||||
|
As a user, you can use inline mode just like with a normal client.
|
||||||
|
|
||||||
|
It is currently not implemented as a Client method, but you can use
|
||||||
|
[Raw API](raw-api.html):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const chat = await tg.resolvePeer('me')
|
||||||
|
|
||||||
|
const results = await tg.call({
|
||||||
|
_: 'messages.getInlineBotResults',
|
||||||
|
bot: toInputUser(await tg.resolvePeer('music'))!,
|
||||||
|
peer: chat,
|
||||||
|
query: 'vivaldi',
|
||||||
|
offset: ''
|
||||||
|
}, { throw503: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, for example, to send the first result:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const first = results.results[0]
|
||||||
|
|
||||||
|
const res = await tg.call({
|
||||||
|
_: 'messages.sendInlineBotResult',
|
||||||
|
peer: chat,
|
||||||
|
randomId: randomLong(),
|
||||||
|
queryId: results.queryId,
|
||||||
|
id: first.id
|
||||||
|
})
|
||||||
|
tg.handleClientUpdate(res, true)
|
||||||
|
```
|
301
docs/guide/topics/keyboards.md
Executable file
301
docs/guide/topics/keyboards.md
Executable file
|
@ -0,0 +1,301 @@
|
||||||
|
# Keyboards
|
||||||
|
|
||||||
|
You probably already know what a keyboard is, and if you
|
||||||
|
don't, check the [Bots documentation](https://core.telegram.org/bots#keyboards)
|
||||||
|
by Telegram.
|
||||||
|
|
||||||
|
## Sending a keyboard
|
||||||
|
|
||||||
|
When developing bots, a common feature that many developers use
|
||||||
|
is sending custom keyboards to their users - be it inline or reply.
|
||||||
|
|
||||||
|
In both cases, this is done by providing `replyMarkup` parameter
|
||||||
|
when using `sendText` or similar methods. It accepts plain
|
||||||
|
JavaScript object, but you can also use builder functions
|
||||||
|
from `BotKeyboard` namespace.
|
||||||
|
|
||||||
|
In mtcute, buttons are represented as a two-dimensional array.
|
||||||
|
|
||||||
|
## Reply keyboards
|
||||||
|
|
||||||
|
Reply keyboard is a keyboard that is shown under user's writebar.
|
||||||
|
When user taps on some button, a message is sent containing this
|
||||||
|
button's text.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://core.telegram.org/file/811140184/1/5YJxx-rostA/ad3f74094485fb97bd"
|
||||||
|
width="280"
|
||||||
|
caption="Example of a reply keyboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendText('username', 'Awesome keyboard!', {
|
||||||
|
replyMarkup: BotKeyboard.reply([
|
||||||
|
[BotKeyboard.text('First button')],
|
||||||
|
[BotKeyboard.text('Second button')],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can only use the following button types with reply keyboards:
|
||||||
|
|
||||||
|
| Name | Type | Notes |
|
||||||
|
| ------------------- | ---------------------------- | ----------------------- |
|
||||||
|
| Text-only | `BotKeyboard.text` | |
|
||||||
|
| Request contact | `BotKeyboard.requestContact` | only for private chats. |
|
||||||
|
| Request geolocation | `BotKeyboard.requestGeo` | only for private chats. |
|
||||||
|
| Request poll | `BotKeyboard.requestPoll` | only for private chats. |
|
||||||
|
|
||||||
|
Using any other will result in an error by Telegram.
|
||||||
|
|
||||||
|
You can also instruct the client to hide a previously
|
||||||
|
sent reply keyboard:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendText('username', 'No more keyboard :p', {
|
||||||
|
replyMarkup: BotKeyboard.hideReply()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, ask the user to reply to this message with custom text:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendText('username', 'What is your name?', {
|
||||||
|
replyMarkup: BotKeyboard.forceReply()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Inline keyboards
|
||||||
|
|
||||||
|
Inline keyboard is a keyboard that is shown under the message.
|
||||||
|
When user taps on some button, a client does some action that particular
|
||||||
|
button instructs it to do.
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
src="https://core.telegram.org/file/811140217/1/NkRCCLeQZVc/17a804837802700ea4"
|
||||||
|
width="280"
|
||||||
|
caption="Example of an inline keyboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendText('username', 'Awesome keyboard!', {
|
||||||
|
replyMarkup: BotKeyboard.inline([
|
||||||
|
[BotKeyboard.callback('First button', 'btn:1')],
|
||||||
|
[BotKeyboard.callback('Second button', 'btn:2')],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can only use the following button types with inline keyboards:
|
||||||
|
|
||||||
|
| Name | Type | Notes |
|
||||||
|
| -------------- | -------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| Callback | `BotKeyboard.callback` | When clicked a callback query will be sent to the bot. |
|
||||||
|
| URL | `BotKeyboard.url` | When clicked the client will open the given URL. |
|
||||||
|
| Switch inline | `BotKeyboard.switchInline` | When clicked the client will open an inline query to this bot with the given query. |
|
||||||
|
| "Play game" | `BotKeyboard.game` | Must be the first one, must be used with `InputMedia.game` as the media. |
|
||||||
|
| "Pay" | `BotKeyboard.pay` | Must be the first one, must be used with `InputMedia.invoice` as the media. |
|
||||||
|
| Seamless login | `BotKeyboard.urlAuth` | [Learn more](https://corefork.telegram.org/constructor/inputKeyboardButtonUrlAuth) |
|
||||||
|
| WebView | `BotKeyboard.webView` | [Learn more](https://corefork.telegram.org/api/bots/webapps) |
|
||||||
|
| Open user | `BotKeyboard.userProfile` | When clicked the client will open the given user's profile |
|
||||||
|
| Request peer | `BotKeyboard.requestPeer` | When clicked the client will ask the user to choose a peer and will send a message with [`ActionPeerChosen`](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ActionPeerChosen.html) |
|
||||||
|
|
||||||
|
Using any other will result in an error by Telegram.
|
||||||
|
|
||||||
|
## Keyboard builder
|
||||||
|
|
||||||
|
Sometimes 2D array is a bit too low-level, and thus mtcute provides an
|
||||||
|
easy-to-use builder for the keyboards.
|
||||||
|
|
||||||
|
Once created using `BotKeyboard.builder()`, you can `push` buttons there,
|
||||||
|
and then get it either `asInline` or `asReply`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const markup = BotKeyboard.builder()
|
||||||
|
.push(BotKeyboard.text('Button 1'))
|
||||||
|
.push(BotKeyboard.text('Button 2'))
|
||||||
|
.asReply()
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// [ Button 1 ]
|
||||||
|
// [ Button 2 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also push a button conditionally, or even use a function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const markup = BotKeyboard.builder()
|
||||||
|
.push(BotKeyboard.text('Button 1'))
|
||||||
|
.push(isAdmin && BotKeyboard.text('Button 2'))
|
||||||
|
.push(() => BotKeyboard.text('Button 3'))
|
||||||
|
.asReply()
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// [ Button 1 ]
|
||||||
|
// [ Button 2 ] (only if admin)
|
||||||
|
// [ Button 3 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
When `push`-ing multiple buttons at once, they will be wrapped after a certain
|
||||||
|
number of buttons added (default: 3):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const markup = BotKeyboard.builder()
|
||||||
|
.push(
|
||||||
|
BotKeyboard.text('Button 1'),
|
||||||
|
BotKeyboard.text('Button 2'),
|
||||||
|
BotKeyboard.text('Button 3'),
|
||||||
|
BotKeyboard.text('Button 4'),
|
||||||
|
)
|
||||||
|
.asReply()
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// [ Button 1 ] [ Button 2 ] [ Button 3 ]
|
||||||
|
// [ Button 4 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can add entire rows at once without them getting wrapped
|
||||||
|
(and even populate them from a function!):
|
||||||
|
```ts
|
||||||
|
const markup = BotKeyboard.builder()
|
||||||
|
.row(
|
||||||
|
BotKeyboard.text('1'),
|
||||||
|
BotKeyboard.text('2'),
|
||||||
|
BotKeyboard.text('3'),
|
||||||
|
BotKeyboard.text('4'),
|
||||||
|
)
|
||||||
|
.row((row) => {
|
||||||
|
for (let i = 5; i <= 8; i++ ) {
|
||||||
|
row.push(BotKeyboard.text(`${i}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.asReply()
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// [ 1 ] [ 2 ] [ 3 ] [ 4 ]
|
||||||
|
// [ 5 ] [ 6 ] [ 7 ] [ 8 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback data builders
|
||||||
|
|
||||||
|
Writing, parsing and checking callback data manually gets tiring
|
||||||
|
quite fast. Luckily, mtcute provides a tool that does the heavy stuff for you,
|
||||||
|
called Callback data builder.
|
||||||
|
|
||||||
|
<!-- Full example TODO LINK -->
|
||||||
|
|
||||||
|
### Creating a builder
|
||||||
|
|
||||||
|
Consider a simple bot that has some posts to display to user,
|
||||||
|
and the user can switch between them using inline buttons.
|
||||||
|
|
||||||
|
First, let's declare a builder for the button:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const PostButton = new CallbackDataBuilder('post', 'id', 'action')
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `post` is the *prefix*, which will be prepended to all callback
|
||||||
|
data strings generated by this builder to disambiguate. Make sure to use
|
||||||
|
something unique!
|
||||||
|
|
||||||
|
`id` and `action` are *fields* which will be parsed/serialized to the callback
|
||||||
|
data string in that particular order. Only include important stuff there, since
|
||||||
|
callback data is limited to 64 characters!
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Callback data builders are meant to be static, so it is best
|
||||||
|
to declare them in a separate file and import from other files.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Creating buttons
|
||||||
|
|
||||||
|
Now that we have the builder, we can use `.build` method to add buttons
|
||||||
|
to the messages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await msg.answerText('...', {
|
||||||
|
replyMarkup: BotKeyboard.inline([
|
||||||
|
[
|
||||||
|
BotKeyboard.callback(
|
||||||
|
'Post title',
|
||||||
|
PostButton.build({ id: 1, action: 'view' })
|
||||||
|
)
|
||||||
|
]
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code will produce the following callback data in that button:
|
||||||
|
|
||||||
|
```
|
||||||
|
post:1:view
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling clicks
|
||||||
|
|
||||||
|
Our button is currently rather useless, since we haven't registered
|
||||||
|
a handler for it just yet. We can use `.filter` method of our builder
|
||||||
|
to create a filter to suit our needs:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onCallbackQuery(PostButton.filter({ action: 'view' }), async (upd) => {
|
||||||
|
const post = await getPostById(upd.match.id)
|
||||||
|
if (!post) {
|
||||||
|
await upd.answer({ text: 'Not found!' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await upd.editMessage({
|
||||||
|
text: post.text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`.filter` not only handles parsing and checking, but also provides
|
||||||
|
`.match` extension field that contains the parsed data, and you can use it
|
||||||
|
inside your code.
|
||||||
|
|
||||||
|
## Using a keyboard
|
||||||
|
|
||||||
|
When using mtcute as a client, you may want to use some
|
||||||
|
keyboard that was attached to some message.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg: Message) => {
|
||||||
|
const markup = msg.markup
|
||||||
|
|
||||||
|
switch (markup.type) {
|
||||||
|
// see below
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If type is `hide_reply`, there is (obviously) nothing to do except
|
||||||
|
to hide the current reply keyboard from the UI (if applicable).
|
||||||
|
|
||||||
|
If type is `force_reply`, just send a message in reply to this message:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await msg.replyText('Some text')
|
||||||
|
```
|
||||||
|
|
||||||
|
If type is `reply` or `inline`, then there are some buttons available.
|
||||||
|
You can find the one you need, and then act accordingly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const buttons = markup.buttons
|
||||||
|
const buttonINeed = BotKeyboard.findButton(buttons, 'Button text')
|
||||||
|
|
||||||
|
switch (buttonINeed._) {
|
||||||
|
// see below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`buttonINeed` will be a plain TL object of type
|
||||||
|
[KeyboardButton](https://corefork.telegram.org/type/KeyboardButton).
|
||||||
|
|
||||||
|
### Emulating a click
|
||||||
|
|
||||||
|
See [Telegram docs](https://core.telegram.org/api/bots/buttons#pressing-buttons) on this topic.
|
107
docs/guide/topics/parse-modes.md
Executable file
107
docs/guide/topics/parse-modes.md
Executable file
|
@ -0,0 +1,107 @@
|
||||||
|
# Parse modes
|
||||||
|
|
||||||
|
You may be familiar with parse modes from the Bot API. Indeed,
|
||||||
|
the idea is pretty much the same - parse mode defines the syntax to use
|
||||||
|
for formatting entities in messages.
|
||||||
|
|
||||||
|
However, there's a major difference – in mtcute, the client doesn't know anything about
|
||||||
|
how the parse modes are implemented. Instead, it just accepts an object containing
|
||||||
|
the `text` and `entities` fields, and sends it to the server:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.sendText('self', {
|
||||||
|
text: 'Hi, User!',
|
||||||
|
entities: [
|
||||||
|
{ _: 'messageEntityBold', offset: 4, length: 4 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, passing this object manually is not very convenient,
|
||||||
|
so mtcute provides a set of *parsers* that can be used to convert
|
||||||
|
a string with entities to this structure.
|
||||||
|
|
||||||
|
For convenience, mtcute itself provides two parsers – for Markdown and HTML.
|
||||||
|
They are both implemented as separate packages, and they themselves are tagged template literals,
|
||||||
|
which makes it very easy to interpolate variables into the message.
|
||||||
|
|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
Markdown parser is implemented in `@mtcute/markdown-parser` package:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { md } from '@mtcute/markdown-parser'
|
||||||
|
|
||||||
|
dp.onNewMessage(async (msg) => {
|
||||||
|
await msg.answerText(md`Hello, **${msg.sender.username}**`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: the syntax used by this parser is **not** compatible
|
||||||
|
with Bot API's Markdown or MarkdownV2 syntax.
|
||||||
|
See [documentation](https://ref.mtcute.dev/modules/_mtcute_markdown_parser.html)
|
||||||
|
to learn about the syntax.
|
||||||
|
|
||||||
|
## HTML
|
||||||
|
|
||||||
|
HTML parser is implemented in `@mtcute/html-parser` package:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { html } from '@mtcute/html-parser'
|
||||||
|
|
||||||
|
dp.onNewMessage(async (msg) => {
|
||||||
|
await msg.answerText(html`Hello, <b>${msg.sender.username}</b>`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: the syntax used by this parser is **not**
|
||||||
|
compatible with Bot API's HTML syntax.
|
||||||
|
See [documentation](https://ref.mtcute.dev/modules/_mtcute_html_parser.html)
|
||||||
|
to learn about the syntax.
|
||||||
|
|
||||||
|
## Interpolation
|
||||||
|
|
||||||
|
Both parsers support interpolation of variables into the message,
|
||||||
|
as can be seen in the examples above.
|
||||||
|
|
||||||
|
Both parsers support the following types of interpolation:
|
||||||
|
- `string` - **will not** be parsed, and appended to plain text as-is
|
||||||
|
- `number` - will be converted to string and appended to plain text as-is
|
||||||
|
- `TextWithEntities` or `MessageEntity` - will add the text and its entities to the output.
|
||||||
|
This is the type returned by `md` and `html` themselves, so you can even mix and match them:
|
||||||
|
```ts
|
||||||
|
const greeting = (user) => html`<i>${user.displayName}</i>`
|
||||||
|
const text = md`**Hello**, ${user}!`
|
||||||
|
```
|
||||||
|
- falsy value (i.e. `null`, `undefined`, `false`) - will be ignored
|
||||||
|
|
||||||
|
### Unsafe interpolation
|
||||||
|
|
||||||
|
In some cases, you may already have a string with entities, and want to parse it to entities.
|
||||||
|
|
||||||
|
In this case, you can use the method as a function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const text = 'Hello, **User**!'
|
||||||
|
|
||||||
|
await tg.sendText('self', md(text))
|
||||||
|
// or even
|
||||||
|
await tg.sendText('self', md`${md(text)} What's new?`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Un-parsing
|
||||||
|
|
||||||
|
Both HTML and Markdown parsers also provide an `unparse` method,
|
||||||
|
which can be used to convert the message back to the original text:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { html } from '@mtcute/html-parser'
|
||||||
|
|
||||||
|
const msg = await tg.sendText('Hi, <b>User</b>!', { parseMode: 'html' })
|
||||||
|
|
||||||
|
console.log(msg.text)
|
||||||
|
// Hi, User!
|
||||||
|
console.log(html.unparse())
|
||||||
|
// Hi, <b>User</b>!
|
||||||
|
```
|
258
docs/guide/topics/peers.md
Executable file
258
docs/guide/topics/peers.md
Executable file
|
@ -0,0 +1,258 @@
|
||||||
|
# Peers
|
||||||
|
|
||||||
|
One of the most important concepts in MTProto is the "peer".
|
||||||
|
Peer is an object that defines a user, a chat or a channel, and
|
||||||
|
is widely used within the APIs.
|
||||||
|
|
||||||
|
## What is a Peer?
|
||||||
|
|
||||||
|
In MTProto, there are 2 types representing a peer:
|
||||||
|
[Peer](https://core.telegram.org/type/Peer) and [InputPeer](https://core.telegram.org/type/InputPeer)
|
||||||
|
|
||||||
|
[Peer](https://core.telegram.org/type/Peer)
|
||||||
|
defines the peer type (user/chat/channel) and its ID, and is usually
|
||||||
|
returned by the server inside some other object (like Message).
|
||||||
|
|
||||||
|
[InputPeer](https://core.telegram.org/type/InputPeer)
|
||||||
|
defines the peer by providing its type, ID and access hash.
|
||||||
|
Access hashes are a mechanism designed to prevent users from accessing
|
||||||
|
peers that they never met. These objects are mostly used when sending
|
||||||
|
RPC queries to Telegram.
|
||||||
|
|
||||||
|
> There are also `InputUser` and `InputChannel` that are used
|
||||||
|
> to prevent clients from passing incorrect peers
|
||||||
|
> (e.g. restricting a user in a legacy group chat).
|
||||||
|
>
|
||||||
|
> They are basically the same, and you should only care about
|
||||||
|
> them when using Raw APIs, so we'll skip them for now.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
In MTProto, you'll often see `InputSomething` and `Something` types.
|
||||||
|
|
||||||
|
This simply means that the former should be used when making requests,
|
||||||
|
and the latter is sent by the server back.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Chats and Channels
|
||||||
|
|
||||||
|
As you may have noticed, in MTProto there are only three types of peers:
|
||||||
|
users, chats and channels. However, things are not as simple as you may imagine,
|
||||||
|
so let's dive a bit deeper.
|
||||||
|
|
||||||
|
**[Chat](https://core.telegram.org/class/chat)** is a legacy group. The one which is
|
||||||
|
created by default when you use "Create group" button in official clients.
|
||||||
|
Official clients refer to them as "Groups"
|
||||||
|
|
||||||
|
**[Channel](https://core.telegram.org/class/channel)** is anything that is not a user,
|
||||||
|
nor a legacy group. Supergroups, actual broadcast channels and broadcast groups
|
||||||
|
are all represented in MTProto as a **Channel** with a different set of flags:
|
||||||
|
- A **broadcast channel** is a [Channel](https://core.telegram.org/class/channel) where `.broadcast === true`
|
||||||
|
- A **supergroup** (also referred to as megagroup) is a
|
||||||
|
[Channel](https://core.telegram.org/class/channel) where `.megagroup === true`
|
||||||
|
- A **forum** is a supergroup where `.forum === true`
|
||||||
|
- A **broadcast group** (also referred to as gigagroup) is a
|
||||||
|
[Channel](https://core.telegram.org/class/channel) where `.gigagroup === true`.
|
||||||
|
They are basically a **supergroup** where default permissions disallow
|
||||||
|
sending messages and cannot be changed ([src](https://t.me/tdlibchat/15164)).
|
||||||
|
|
||||||
|
Official clients only use "Channel" when referring to *broadcast channels*.
|
||||||
|
|
||||||
|
Chats are still used *(probably?)* because they are enough for most people's
|
||||||
|
needs, and are also lighter on server resources. However, chats are missing
|
||||||
|
many important features for public communities, like: usernames,
|
||||||
|
custom admin rights, per-user restrictions, event log and more.
|
||||||
|
|
||||||
|
Official clients silently "migrate" legacy groups to supergroups
|
||||||
|
(actually channels) whenever the user wants to use a feature not supported
|
||||||
|
by the Chat, like setting a username. Channel cannot be migrated back to Chat.
|
||||||
|
|
||||||
|
### Chat in mtcute
|
||||||
|
|
||||||
|
In addition to the mess described above, mtcute also has a [Chat](https://ref.mtcute.dev/classes/_mtcute_core.index.Chat.html) type.
|
||||||
|
It is used to represent anything where there can be messages,
|
||||||
|
including users, legacy groups, supergroups, channels, etc.
|
||||||
|
|
||||||
|
It is mostly inspired by [Bot API's Chat](https://core.telegram.org/bots/api#chat)
|
||||||
|
type, and works in a very similar way.
|
||||||
|
|
||||||
|
## Fetching peers
|
||||||
|
|
||||||
|
Often, you'll need to interact with a peer later, when the respective
|
||||||
|
`User` or `Chat` object is no longer available.
|
||||||
|
|
||||||
|
Of course, manually storing peers along with their access hash is very tedious.
|
||||||
|
That is why mtcute handles it for you! Peers and their access hashes are
|
||||||
|
automatically stored inside the storage you provided, and can be accessed
|
||||||
|
at any time later.
|
||||||
|
|
||||||
|
This way, to get an InputPeer, you simply need
|
||||||
|
to call `resolvePeer` method and provide a Peer
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const peer = await tg.resolvePeer({ _: 'peerUser', userId: 777000 })
|
||||||
|
```
|
||||||
|
|
||||||
|
But still, this is very tedious, so you can pass
|
||||||
|
many more than just a `Peer` object to `resolvePeer`:
|
||||||
|
- Peer's [marked ID](#marked-ids)
|
||||||
|
- Peer's username
|
||||||
|
- Peer's phone number (will only work with contacts)
|
||||||
|
- `"me"` or `"self"` to refer to yourself (current user)
|
||||||
|
- `Peer`, `InputPeer`, `InputUser` and `InputChannel` objects
|
||||||
|
- `Chat`, `User` or generally anything where `.inputPeer: InputPeer` is available
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const peer = await tg.resolvePeer(-1001234567890)
|
||||||
|
const peer = await tg.resolvePeer("durovschat")
|
||||||
|
const peer = await tg.resolvePeer("+79001234567")
|
||||||
|
const peer = await tg.resolvePeer("me")
|
||||||
|
```
|
||||||
|
|
||||||
|
## `InputPeerLike`
|
||||||
|
|
||||||
|
However, you will only really need to use `resolvePeer` method
|
||||||
|
when you are working with the Raw APIs. High-level methods
|
||||||
|
use `InputPeerLike`, a special type used to represent an input peer.
|
||||||
|
|
||||||
|
In fact, we have already covered it - it is the very type that `resolvePeer`
|
||||||
|
takes as its argument:
|
||||||
|
```ts
|
||||||
|
async function resolvePeer(peer: InputPeerLike): Promise<tl.TypeInputPeer>
|
||||||
|
```
|
||||||
|
|
||||||
|
Client methods implicitly call `resolvePeer` to convert `InputPeerLike`
|
||||||
|
to input peer and use it for the MTProto API call.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Whenever possible, pass `Chat`, `InputPeer` or `"me"/"self"` as `InputPeerLike`, since
|
||||||
|
this avoids redundant storage and/or API calls.
|
||||||
|
|
||||||
|
When not possible, use their marked IDs, since most of the time
|
||||||
|
it is the cheapest way to fetch an `InputPeer`.
|
||||||
|
|
||||||
|
For smaller-scale scripts, you *can* use usernames and phone numbers.
|
||||||
|
They are also cached in the storage, but might require additional API
|
||||||
|
call if they are not. Also, not every user has a username, and only
|
||||||
|
your contacts can be fetched by the phone number.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ BAD
|
||||||
|
await tg.sendText(msg.sender.username!, ...)
|
||||||
|
await tg.sendText(msg.sender.phone!, ...)
|
||||||
|
|
||||||
|
// 🧐 BETTER
|
||||||
|
await tg.sendText(msg.sender.id, ...)
|
||||||
|
|
||||||
|
// ✅ GOOD
|
||||||
|
await tg.sendText(msg.sender.inputPeer, ...)
|
||||||
|
await tg.sendText(msg.sender, ...)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Marked IDs
|
||||||
|
|
||||||
|
As you may have noticed, both `Peer` and `InputPeer` contain
|
||||||
|
peer type, but when using client methods and Bot API,
|
||||||
|
you don't specify it manually.
|
||||||
|
|
||||||
|
`Peer` and `InputPeer` contain what is called as a "bare" ID, the ID
|
||||||
|
inside that particular peer type. Bare IDs between different peer types
|
||||||
|
may (and do!) collide, and that is why Marked IDs are used.
|
||||||
|
|
||||||
|
Marked ID is a slightly transformed variant of the bare ID to
|
||||||
|
unambiguously define both peer type and peer ID with a single integer
|
||||||
|
(currently, it fits into JS number, but later we may be forced to move
|
||||||
|
to 64-bit integers).
|
||||||
|
|
||||||
|
This was first introduced in TDLib, and was since adopted by many
|
||||||
|
third-party libraries, including mtcute.
|
||||||
|
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
The concept described below is implemented and exported in utils,
|
||||||
|
see [getBasicPeerType](https://ref.mtcute.dev/functions/_mtcute_core.index.getBasicPeerType.html),
|
||||||
|
[getMarkedPeerId](https://ref.mtcute.dev/functions/_mtcute_core.index.getMarkedPeerId.html),
|
||||||
|
:::
|
||||||
|
|
||||||
|
It works as follows:
|
||||||
|
- `User` IDs are kept as-is (`123 -> 123`)
|
||||||
|
- `Chat` IDs are negated (`123 -> -123`)
|
||||||
|
- `Channel` IDs are subtracted from `-1000000000000` (`1234567890 -> -1001234567890`)
|
||||||
|
|
||||||
|
Some sources may say that it's simply prepending `-100` to the ID,
|
||||||
|
but that's not entirely true, since channel ID may be not 10 digits long.
|
||||||
|
|
||||||
|
This way, you can easily determine peer type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const MIN_CHANNEL_ID = -1002147483647
|
||||||
|
const MAX_CHANNEL_ID = -1000000000000
|
||||||
|
const MIN_CHAT_ID = -2147483647
|
||||||
|
const MAX_USER_ID = 2147483647
|
||||||
|
|
||||||
|
if (peer < 0) {
|
||||||
|
if (MIN_CHAT_ID <= peer) return 'chat'
|
||||||
|
if (MIN_CHANNEL_ID <= peer && peer < MAX_CHANNEL_ID) return 'channel'
|
||||||
|
} else if (0 < peer && peer <= MAX_USER_ID) {
|
||||||
|
return 'user'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then, to convert it back to bare ID, use the exact same operation
|
||||||
|
(it works in two directions).
|
||||||
|
|
||||||
|
## Incomplete peers
|
||||||
|
|
||||||
|
In some cases, mtcute may not be able to immediately provide you with complete information
|
||||||
|
about a user/chat (see [min constructors](https://core.telegram.org/api/min) in MTProto docs).
|
||||||
|
|
||||||
|
This currently only seems to happen for `msg.sender` and `msg.chat` fields for non-bot accounts in large chats,
|
||||||
|
so if you're only ever going to work with bots, you can safely ignore this section (for now?).
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Complete peers ≠ [full peers](https://ref.mtcute.dev/classes/_mtcute_core.highlevel_client.TelegramClient.html#getFullChat)!
|
||||||
|
|
||||||
|
- Incomplete are seen in updates in rare cases, and are missing some fields (e.g. username)
|
||||||
|
- Complete peers are pretty much all the other peer objects you get in updates
|
||||||
|
- Full peers are objects with additional information (e.g. bio) which you should request explicitly
|
||||||
|
:::
|
||||||
|
|
||||||
|
For such chats, the server may send "min" constructors, which contain incomplete information about the user/chat.
|
||||||
|
To avoid blocking the updates loop, and since the missing information is not critical, mtcute will return an incomplete
|
||||||
|
peer object, which is *good enough* for most cases.
|
||||||
|
|
||||||
|
For example, such `User` objects may have the following data missing or have incorrect values:
|
||||||
|
- `.username` may be missing
|
||||||
|
- `.photo` may be missing with some privacy settings
|
||||||
|
- online status may be incorrect
|
||||||
|
- and probably more (for more info please consult the official docs for [user](https://core.telegram.org/constructor/user)
|
||||||
|
and [channel](https://core.telegram.org/constructor/channel))
|
||||||
|
|
||||||
|
The user itself is still usable, though. If you need to get the missing information, you can call
|
||||||
|
`getUsers`/`getChat` method, which will return a complete `User`/`Chat` object.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const user = ... // incomplete user from somewhere
|
||||||
|
const [completeUser] = await tg.getUsers(user)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using [Dispatcher](/guide/dispatcher/intro.md), you can use `.getCompleteSender()` or `.getCompleteChat()` methods instead:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(async (msg) => {
|
||||||
|
const sender = await msg.getCompleteSender()
|
||||||
|
const chat = await msg.getCompleteChat()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can use `withCompleteSender` and `withCompleteChat` middleware-like filters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
dp.onNewMessage(
|
||||||
|
filters.withCompleteSender(filters.sender('user'))
|
||||||
|
async (msg) => {
|
||||||
|
const user = msg.sender
|
||||||
|
// user is guaranteed to be complete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
198
docs/guide/topics/raw-api.md
Executable file
198
docs/guide/topics/raw-api.md
Executable file
|
@ -0,0 +1,198 @@
|
||||||
|
# Raw API
|
||||||
|
|
||||||
|
mtcute implements a lot of methods to simplify using the
|
||||||
|
Telegram APIs. However, it does not cover the entirety of the API,
|
||||||
|
and in that case, you can resort to using the MTProto APIs directly.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
When using MTProto API directly, you will have to manually
|
||||||
|
implement any checks, arguments normalization, parsing, etc.
|
||||||
|
|
||||||
|
Whenever possible, use client methods instead!
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Calling MTProto API
|
||||||
|
|
||||||
|
Before you can call some method, you need to know *what* to call and *how* to
|
||||||
|
call it. To do that, please refer to [TL Reference](https://core.telegram.org/methods).
|
||||||
|
|
||||||
|
Then, simply pass method name and arguments to `.call()` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const result = await tg.call({
|
||||||
|
_: 'account.checkUsername',
|
||||||
|
username: 'finally_water'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Thanks to TypeScript, the request object is strictly typed,
|
||||||
|
and the return value also has the correct type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const result = await tg.call({
|
||||||
|
_: 'account.checkUsername',
|
||||||
|
username: 42 // error: must be a string
|
||||||
|
})
|
||||||
|
|
||||||
|
result.ok // error: boolean does not have `ok` property
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common parameters
|
||||||
|
|
||||||
|
In MTProto APIs, there are some parameters that are
|
||||||
|
often encountered in different methods, and are
|
||||||
|
briefly described below:
|
||||||
|
|
||||||
|
| Name | Description | Safe default value |
|
||||||
|
|---|---|---|
|
||||||
|
| `hash` | Hash of the previously stored content, used to avoid re-fetching the content that is not modified. Methods that use this parameter have `*NotModified` class as one of the possible return types. It is not returned if `hash=0`. | `0`
|
||||||
|
| `offset` | Offset for pagination | `0`
|
||||||
|
| `limit` | Maximum number of items for pagination, max. limit is different for every method. | `0`, this will usually default to ~20
|
||||||
|
| `randomId` | Random message ID to avoid sending the same message. | `randomLong()` (exported by `@mtcute/core/utils.js`)
|
||||||
|
|
||||||
|
Learn more about pagination in [Telegram docs](https://core.telegram.org/api/offsets)
|
||||||
|
|
||||||
|
## Resolving peers
|
||||||
|
|
||||||
|
To fetch a value for fields that require `InputPeer`, use `resolvePeer` method.
|
||||||
|
If you need `InputUser` or `InputChannel`, you can use `to*` functions
|
||||||
|
respectively:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const result = await tg.call({
|
||||||
|
_: 'channels.reportSpam',
|
||||||
|
channel: toInputChannel(await tg.resolvePeer(...)),
|
||||||
|
userId: toInputUser(await tg.resolvePeer(...)),
|
||||||
|
id: [1, 2, 3]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These functions will throw in case the peer is of wrong type
|
||||||
|
|
||||||
|
## Handling Updates
|
||||||
|
|
||||||
|
Some RPC methods return `Updates` type. For these methods,
|
||||||
|
it is important that they are properly handled by Updates manager.
|
||||||
|
This is done by calling `tg.handleClientUpdate` method:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const res = await tg.call({ _: 'contacts.addContact', ... })
|
||||||
|
tg.handleClientUpdate(res)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Calling `tg.handleClientUpdate` will not dispatch all the updates contained
|
||||||
|
in that object. This is sometimes undesirable, and can be avoided
|
||||||
|
by passing `false` as the second argument:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.handleClientUpdate(res, false)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: details Why is this important?
|
||||||
|
When RPC method returns `Updates`, it might have newer PTS and SEQ values.
|
||||||
|
If it does, passing it to `handleClientUpdate` makes the library aware of those
|
||||||
|
new updates. Otherwise, library would have to re-fetch them the next
|
||||||
|
time an update is encountered.
|
||||||
|
|
||||||
|
Also, in case PTS/SEQ values are bigger than the next expected value,
|
||||||
|
an *update gap* is detected and missing updates will be fetched.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Dummy updates
|
||||||
|
|
||||||
|
Some methods return not updates, but a class like
|
||||||
|
[messages.affectedHistory](https://corefork.telegram.org/constructor/messages.affectedHistory).
|
||||||
|
|
||||||
|
They also contain PTS values, and should also be handled.
|
||||||
|
But since this is not an update, it can't be passed directly
|
||||||
|
to `handleClientUpdate`, and instead a "dummy" update is created:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const res = await this.call({
|
||||||
|
_: 'messages.deleteMessages',
|
||||||
|
id: [1, 2, 3],
|
||||||
|
})
|
||||||
|
const upd = createDummyUpdate(res.pts, res.ptsCount)
|
||||||
|
tg.handleClientUpdate(upd)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, in case this PTS is related to a channel:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const channel = toInputChannel(peer)
|
||||||
|
const res = await this.call({
|
||||||
|
_: 'channels.deleteMessages',
|
||||||
|
channel,
|
||||||
|
id: [1, 2, 3],
|
||||||
|
})
|
||||||
|
const upd = createDummyUpdate(res.pts, res.ptsCount, channel.channelId)
|
||||||
|
tg.handleClientUpdate(upd)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files and media
|
||||||
|
|
||||||
|
To get an `InputFile` from `InputFileLike`, use `_normalizeInputFile`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const file = 'file:theme.txt'
|
||||||
|
|
||||||
|
const res = await tg.call({
|
||||||
|
_: 'account.uploadTheme',
|
||||||
|
file: await tg._normalizeInputFile(file),
|
||||||
|
...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To get an `InputMedia` from `InputMediaLike`, use `_normalizeInputMedia`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const file = InputMedia.auto('BQACAgEAAx...Z2mGB8E')
|
||||||
|
|
||||||
|
const res = await tg.call({
|
||||||
|
_: 'messages.uploadMedia',
|
||||||
|
media: await tg._normalizeInputMedia(file),
|
||||||
|
...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message entities
|
||||||
|
|
||||||
|
To simplify processing message entities, client has a special method:
|
||||||
|
```ts
|
||||||
|
_parseEntities(
|
||||||
|
text?: string | FormattedString,
|
||||||
|
mode?: string | null,
|
||||||
|
entities?: tl.TypeMessageEntity[]
|
||||||
|
): Promise<[string, tl.TypeMessageEntity[] | undefined]>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `text` is user-provided text which may have formatted entities,
|
||||||
|
`mode` is the chosen parse mode (or `null` to disable), and `entities`
|
||||||
|
is the override message entities provided by the user.
|
||||||
|
|
||||||
|
If `FormattedString` is passed (e.g. <code>md\`\*\*Hello!**\`</code>),
|
||||||
|
`mode` parameter is ignored.
|
||||||
|
|
||||||
|
It returns a tuple containing text without any entities, and the entities
|
||||||
|
themselves (if applicable). If `text` was not provided, empty string
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
## Fully custom requests
|
||||||
|
|
||||||
|
mtcute also allows you to send fully custom requests to the server.
|
||||||
|
This is useful if you want to use some undocumented or yet-unreleased APIs
|
||||||
|
and don't want to patch the library or use the TL schema override mechanism.
|
||||||
|
|
||||||
|
You can use the `mtcute.customRequest` pseudo-method for that:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const res = await tg.call({
|
||||||
|
_: 'mtcute.customRequest',
|
||||||
|
bytes: Buffer.from('11223344', 'hex'),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`bytes` will be send as-is to the server, and the response will be returned as a `Uint8Array`
|
||||||
|
for you to handle on your own (it might be useful to look into [`@mtcute/tl-runtime` package](https://ref.mtcute.dev/modules/_mtcute_tl_runtime.html))
|
174
docs/guide/topics/storage.md
Executable file
174
docs/guide/topics/storage.md
Executable file
|
@ -0,0 +1,174 @@
|
||||||
|
# Storage
|
||||||
|
|
||||||
|
Storage is a very important aspect of the library,
|
||||||
|
which should not be overlooked. It is primarily used to
|
||||||
|
handle caching and authorization (you wouldn't want to
|
||||||
|
log in every time, right?).
|
||||||
|
|
||||||
|
## In-memory storage
|
||||||
|
|
||||||
|
The simplest way to store data is to store it in-memory
|
||||||
|
and never persist it anywhere, and this is exactly
|
||||||
|
what `MemoryStorage` does.
|
||||||
|
|
||||||
|
```ts{4}
|
||||||
|
import { MemoryStorage } from '@mtcute/core'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
storage: new MemoryStorage()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
It is highly advised that you use some kind of persisted storage!
|
||||||
|
|
||||||
|
With in-memory storage, you will need to re-authorize every time
|
||||||
|
(assuming you don't use [session strings](#session-strings)),
|
||||||
|
and also caching won't work past a single run.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## SQLite storage
|
||||||
|
|
||||||
|
The preferred storage for a Node.js application is the one using SQLite,
|
||||||
|
because it does not require loading the entire thing into memory, and
|
||||||
|
is also faster than simply reading/writing a file.
|
||||||
|
|
||||||
|
mtcute implements it in a separate package, `@mtcute/sqlite`, and internally
|
||||||
|
uses [better-sqlite3](https://www.npmjs.com/package/better-sqlite3)
|
||||||
|
|
||||||
|
```ts{4}
|
||||||
|
import { SqliteStorage } from '@mtcute/sqlite'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
storage: new SqliteStorage('my-account.session')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
If you are using `@mtcute/node`, SQLite storage is the default,
|
||||||
|
and you can simply pass a string with file name instead
|
||||||
|
of instantiating `SqliteStorage` manually:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
storage: 'my-account.session'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
To improve performance, `@mtcute/sqlite` by default uses
|
||||||
|
WAL mode ([Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md)).
|
||||||
|
|
||||||
|
When using WAL, along with your SQLite file there may also
|
||||||
|
be `-shm` and `-wal` files. If you don't like seeing those files,
|
||||||
|
instead of disabling WAL altogether, consider putting your storage in a folder
|
||||||
|
(i.e. `new SqliteStorage('storage/my-account')`).
|
||||||
|
|
||||||
|
If you are in fact having problems with WAL mode, you can disable it
|
||||||
|
with `disableWal` parameter.
|
||||||
|
|
||||||
|
|
||||||
|
## IndexedDB storage
|
||||||
|
|
||||||
|
The preferred storage for a Web application is the one using IndexedDB,
|
||||||
|
which is basically a browser's version of SQLite.
|
||||||
|
|
||||||
|
```ts{4}
|
||||||
|
import { IdbStorage } from '@mtcute/web'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
storage: new IdbStorage('my-account')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
In the browser, IndexedDB storage is the default,
|
||||||
|
and you can simply pass a string with file name instead
|
||||||
|
of instantiating `IdbStorage` manually:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
storage: 'my-account'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## Session strings
|
||||||
|
|
||||||
|
Sometimes it might be useful to export storage data to a string, and
|
||||||
|
import it later to another storage. For example, when deploying userbot
|
||||||
|
applications to a server, where you'll be using another storage.
|
||||||
|
|
||||||
|
To generate a session string, simply call `exportSession`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tg.start()
|
||||||
|
console.log(await tg.exportSession())
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output a fairly long string (about 400 chars) to your console,
|
||||||
|
which can then be imported:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const tg = new TelegramClient({...})
|
||||||
|
|
||||||
|
await tg.importSession(SESSION_STRING)
|
||||||
|
// or
|
||||||
|
await tg.start({ session: SESSION_STRING })
|
||||||
|
```
|
||||||
|
|
||||||
|
You can import session into any storage, including in-memory storage.
|
||||||
|
This may be useful when deploying to services like [Heroku](https://www.heroku.com),
|
||||||
|
where their ephemeral file system makes it impossible to use file-based storage.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Anyone with this string will be able to authorize as you and do anything.
|
||||||
|
Treat this as your password, and **never give it away**!
|
||||||
|
|
||||||
|
In case you have accidentally leaked this string, make sure to revoke
|
||||||
|
this session in account settings: "Privacy & Security" > "Active sessions" >
|
||||||
|
find the one containing "mtcute" > Revoke, or, in case this is a bot,
|
||||||
|
revoke bot token with [@BotFather](https://t.me/botfather)
|
||||||
|
|
||||||
|
Also note that you can't log in with the same session
|
||||||
|
string from multiple IPs at once, and that would immediately
|
||||||
|
revoke that session.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: details What is included?
|
||||||
|
You might be curious about the information that the session
|
||||||
|
string includes, and why is it so long.
|
||||||
|
|
||||||
|
Most of the string is occupied by 256 bytes long
|
||||||
|
MTProto authorization key, which, when Base64 encoded,
|
||||||
|
results in **344** characters. Additionally, information
|
||||||
|
about user (their ID and whether the user is a bot) and their DC
|
||||||
|
is included, which results in an average of **407** characters
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Implementing custom storage
|
||||||
|
|
||||||
|
The easiest way to implement a custom storage would be to make a subclass of `MemoryStorage`,
|
||||||
|
or check the [source code of SqliteStorage](https://github.com/mtcute/mtcute/blob/master/packages/sqlite/src/index.ts)
|
||||||
|
and implement something similar with your DB of choice.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
A storage provider in mtcute is composed of:
|
||||||
|
- **Driver**: the core of the storage, which handles reading and writing data to the storage and implements
|
||||||
|
lifecycle methods like `load` and `save`. Driver also manages migrations for the storage, however the migrations
|
||||||
|
themselves are not part of the driver, but are registered separately by repositories
|
||||||
|
- **Repository**: a set of methods to read and write data of a specific entity to the storage, allowing for
|
||||||
|
more efficient and organized access to the data. Repositories are registered in the driver and are used to
|
||||||
|
access the data in the storage
|
||||||
|
|
||||||
|
Such composable architecture allows for custom storages to implement a specific set of repositories,
|
||||||
|
and to reuse the same driver for different providers.
|
||||||
|
|
||||||
|
In mtcute, these sets of repositories are defined:
|
||||||
|
- [IMtStorageProvider](https://ref.mtcute.dev/types/_mtcute_core.index.IMtStorageProvider.html), used by `BaseTelegramClient` for low-level
|
||||||
|
MTProto data storage
|
||||||
|
- [ITelegramStorageProvider](https://ref.mtcute.dev/interfaces/_mtcute_core.index.ITelegramStorageProvider.html), used by `TelegramClient` for basic caching
|
||||||
|
and update handling operations required for the client to work
|
||||||
|
- [IStateStorageProvider](https://ref.mtcute.dev/types/_mtcute_dispatcher.IStateStorageProvider.html), used by `Dispatcher` for FSM and Scenes storage
|
137
docs/guide/topics/transport.md
Executable file
137
docs/guide/topics/transport.md
Executable file
|
@ -0,0 +1,137 @@
|
||||||
|
# Transport
|
||||||
|
|
||||||
|
Transport is a way for mtcute to communicate with Telegram servers.
|
||||||
|
|
||||||
|
mtcute comes bundled with TCP and WebSocket transport, and also
|
||||||
|
supports proxies via additional packages.
|
||||||
|
|
||||||
|
## TCP transport
|
||||||
|
|
||||||
|
TCP transport is the default transport for Node.js, and is implemented
|
||||||
|
using `net.Socket` in `@mtcute/node`:
|
||||||
|
|
||||||
|
```ts{5}
|
||||||
|
import { TcpTransport } from '@mtcute/node'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
transport: () => new TcpTransport()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
In Node.js it is used automatically, you don't need to pass this explicitly
|
||||||
|
:::
|
||||||
|
|
||||||
|
## WebSocket transport
|
||||||
|
|
||||||
|
WebSocket transport is mostly used for the browser,
|
||||||
|
but can also be used in Node.js.
|
||||||
|
|
||||||
|
It is implemented in `@mtcute/web`:
|
||||||
|
|
||||||
|
```ts{5}
|
||||||
|
import { WebSocketTransport } from '@mtcute/web'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
transport: () => new WebSocketTransport()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
In browser, it is used automatically, you don't need to pass this explicitly
|
||||||
|
:::
|
||||||
|
|
||||||
|
## HTTP(s) Proxy transport
|
||||||
|
|
||||||
|
To access Telegram via HTTP(s) proxy, you can use
|
||||||
|
`HttpProxyTcpTransport`, which is provided
|
||||||
|
by `@mtcute/http-proxy` (Node.js only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/http-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts{5-8}
|
||||||
|
import { HttpProxyTcpTransport } from '@mtcute/http-proxy'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
transport: () => new HttpProxyTcpTransport({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8080
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## SOCKS4/5 Proxy transport
|
||||||
|
|
||||||
|
To access Telegram via SOCKS4/5 proxy, you can use
|
||||||
|
`SocksTcpTransport`, which is provided
|
||||||
|
by `@mtcute/socks-proxy` (Node.js only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/socks-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts{5-8}
|
||||||
|
import { SocksTcpTransport } from '@mtcute/socks-proxy'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
transport: () => new SocksTcpTransport({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8080
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## MTProxy transport
|
||||||
|
|
||||||
|
To access Telegram via MTProxy (MTProto proxy), you can use
|
||||||
|
`MtProxyTcpTransport`, which is provided by `@mtcute/mtproxy` (Node.js only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @mtcute/mtproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts{5-8}
|
||||||
|
import { MtProxyTcpTransport } from '@mtcute/mtproxy'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
// ...
|
||||||
|
transport: () => new MtProxyTcpTransport({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8080,
|
||||||
|
secret: '0123456789abcdef0123456789abcdef'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
mtcute supports all kinds of MTProxies, including the newer ones
|
||||||
|
with Fake TLS ⚡️
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Changing transport at runtime
|
||||||
|
|
||||||
|
It is possible to change transport at runtime. For example, this
|
||||||
|
could be used to change proxy used to connect to Telegram.
|
||||||
|
|
||||||
|
To change the transport, simply call `changeTransport`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
tg.changeTransport(() => new MtProxyTcpTransport({...}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing custom transport
|
||||||
|
|
||||||
|
When targeting an environment which is not supported already,
|
||||||
|
you can implement a custom transport on your own. In fact, it is
|
||||||
|
much simpler than it sounds!
|
||||||
|
|
||||||
|
You can check out source code for the bundled transports
|
||||||
|
to get the basic idea
|
||||||
|
[here](https://github.com/mtcute/mtcute/tree/master/packages/core/src/network/transports),
|
||||||
|
and re-use any packet codecs that are included.
|
41
docs/index.md
Normal file
41
docs/index.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "mtcute"
|
||||||
|
tagline: Modern TypeScript library for MTProto
|
||||||
|
image:
|
||||||
|
src: /mtcute-logo.png
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Quick Start →
|
||||||
|
link: /guide/
|
||||||
|
- theme: alt
|
||||||
|
text: API Reference
|
||||||
|
link: //ref.mtcute.dev
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: 🍰
|
||||||
|
title: Simple
|
||||||
|
details: mtcute hides all the complexity and provides a clean and modern API
|
||||||
|
- icon: ✨
|
||||||
|
title: Compatible
|
||||||
|
details: mtcute supports almost everything Bot API does, and even more!
|
||||||
|
- icon: 🍡
|
||||||
|
title: Lightweight
|
||||||
|
details: Running instance uses less than 50 MB of RAM*.
|
||||||
|
- icon: 🛡️
|
||||||
|
title: Type-safe
|
||||||
|
details: Most of the APIs (including MTProto) are strictly typed to help your workflow
|
||||||
|
- icon: ⚙️
|
||||||
|
title: Hackable
|
||||||
|
details: Almost every aspect of the library is customizable, including networking and storage
|
||||||
|
- icon: 🕙
|
||||||
|
title: Up-to-date
|
||||||
|
details: mtcute uses the latest TL schema to provide the newest features as soon as possible
|
||||||
|
---
|
||||||
|
|
||||||
|
<small class="index-footnote">
|
||||||
|
* Tested on a personal account and a few small-load bots
|
||||||
|
</small>
|
20
docs/package.json
Executable file
20
docs/package.json
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@mtcute/docs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "mtcute documentation",
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "1.0.0-rc.24",
|
||||||
|
"vue": "^3.3.7"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"build": "vitepress build",
|
||||||
|
"preview": "vitepress preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"markdown-it-footnote": "^3.0.3",
|
||||||
|
"medium-zoom": "^1.0.8",
|
||||||
|
"vitepress-plugin-back-to-top": "^1.0.1"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.5.0"
|
||||||
|
}
|
995
docs/pnpm-lock.yaml
Normal file
995
docs/pnpm-lock.yaml
Normal file
|
@ -0,0 +1,995 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
markdown-it-footnote:
|
||||||
|
specifier: ^3.0.3
|
||||||
|
version: 3.0.3
|
||||||
|
medium-zoom:
|
||||||
|
specifier: ^1.0.8
|
||||||
|
version: 1.0.8
|
||||||
|
vitepress-plugin-back-to-top:
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
|
devDependencies:
|
||||||
|
vitepress:
|
||||||
|
specifier: 1.0.0-rc.24
|
||||||
|
version: 1.0.0-rc.24(@algolia/client-search@4.20.0)(search-insights@2.9.0)
|
||||||
|
vue:
|
||||||
|
specifier: ^3.3.7
|
||||||
|
version: 3.3.7
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@algolia/autocomplete-core@1.9.3':
|
||||||
|
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
|
||||||
|
|
||||||
|
'@algolia/autocomplete-plugin-algolia-insights@1.9.3':
|
||||||
|
resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==}
|
||||||
|
peerDependencies:
|
||||||
|
search-insights: '>= 1 < 3'
|
||||||
|
|
||||||
|
'@algolia/autocomplete-preset-algolia@1.9.3':
|
||||||
|
resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@algolia/client-search': '>= 4.9.1 < 6'
|
||||||
|
algoliasearch: '>= 4.9.1 < 6'
|
||||||
|
|
||||||
|
'@algolia/autocomplete-shared@1.9.3':
|
||||||
|
resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@algolia/client-search': '>= 4.9.1 < 6'
|
||||||
|
algoliasearch: '>= 4.9.1 < 6'
|
||||||
|
|
||||||
|
'@algolia/cache-browser-local-storage@4.20.0':
|
||||||
|
resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==}
|
||||||
|
|
||||||
|
'@algolia/cache-common@4.20.0':
|
||||||
|
resolution: {integrity: sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ==}
|
||||||
|
|
||||||
|
'@algolia/cache-in-memory@4.20.0':
|
||||||
|
resolution: {integrity: sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg==}
|
||||||
|
|
||||||
|
'@algolia/client-account@4.20.0':
|
||||||
|
resolution: {integrity: sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q==}
|
||||||
|
|
||||||
|
'@algolia/client-analytics@4.20.0':
|
||||||
|
resolution: {integrity: sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug==}
|
||||||
|
|
||||||
|
'@algolia/client-common@4.20.0':
|
||||||
|
resolution: {integrity: sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ==}
|
||||||
|
|
||||||
|
'@algolia/client-personalization@4.20.0':
|
||||||
|
resolution: {integrity: sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ==}
|
||||||
|
|
||||||
|
'@algolia/client-search@4.20.0':
|
||||||
|
resolution: {integrity: sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg==}
|
||||||
|
|
||||||
|
'@algolia/logger-common@4.20.0':
|
||||||
|
resolution: {integrity: sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ==}
|
||||||
|
|
||||||
|
'@algolia/logger-console@4.20.0':
|
||||||
|
resolution: {integrity: sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA==}
|
||||||
|
|
||||||
|
'@algolia/requester-browser-xhr@4.20.0':
|
||||||
|
resolution: {integrity: sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw==}
|
||||||
|
|
||||||
|
'@algolia/requester-common@4.20.0':
|
||||||
|
resolution: {integrity: sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng==}
|
||||||
|
|
||||||
|
'@algolia/requester-node-http@4.20.0':
|
||||||
|
resolution: {integrity: sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng==}
|
||||||
|
|
||||||
|
'@algolia/transporter@4.20.0':
|
||||||
|
resolution: {integrity: sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg==}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.22.5':
|
||||||
|
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.22.20':
|
||||||
|
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/parser@7.23.0':
|
||||||
|
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/types@7.23.0':
|
||||||
|
resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@docsearch/css@3.5.2':
|
||||||
|
resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==}
|
||||||
|
|
||||||
|
'@docsearch/js@3.5.2':
|
||||||
|
resolution: {integrity: sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==}
|
||||||
|
|
||||||
|
'@docsearch/react@3.5.2':
|
||||||
|
resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>= 16.8.0 < 19.0.0'
|
||||||
|
react: '>= 16.8.0 < 19.0.0'
|
||||||
|
react-dom: '>= 16.8.0 < 19.0.0'
|
||||||
|
search-insights: '>= 1 < 3'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
search-insights:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.18.20':
|
||||||
|
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.18.20':
|
||||||
|
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.18.20':
|
||||||
|
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.18.20':
|
||||||
|
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.18.20':
|
||||||
|
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.18.20':
|
||||||
|
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.18.20':
|
||||||
|
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.4.15':
|
||||||
|
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||||
|
|
||||||
|
'@types/linkify-it@3.0.4':
|
||||||
|
resolution: {integrity: sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ==}
|
||||||
|
|
||||||
|
'@types/markdown-it@13.0.5':
|
||||||
|
resolution: {integrity: sha512-QhJP7hkq3FCrFNx0szMNCT/79CXfcEgUIA3jc5GBfeXqoKsk3R8JZm2wRXJ2DiyjbPE4VMFOSDemLFcUTZmHEQ==}
|
||||||
|
|
||||||
|
'@types/mdurl@1.0.4':
|
||||||
|
resolution: {integrity: sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.18':
|
||||||
|
resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==}
|
||||||
|
|
||||||
|
'@vitejs/plugin-vue@4.3.1':
|
||||||
|
resolution: {integrity: sha512-tUBEtWcF7wFtII7ayNiLNDTCE1X1afySEo+XNVMNkFXaThENyCowIEX095QqbJZGTgoOcSVDJGlnde2NG4jtbQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^4.0.0
|
||||||
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.3.7':
|
||||||
|
resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.3.7':
|
||||||
|
resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.3.7':
|
||||||
|
resolution: {integrity: sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==}
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.3.7':
|
||||||
|
resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@6.5.1':
|
||||||
|
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
|
||||||
|
|
||||||
|
'@vue/reactivity-transform@3.3.7':
|
||||||
|
resolution: {integrity: sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==}
|
||||||
|
|
||||||
|
'@vue/reactivity@3.3.7':
|
||||||
|
resolution: {integrity: sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==}
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.3.7':
|
||||||
|
resolution: {integrity: sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==}
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.3.7':
|
||||||
|
resolution: {integrity: sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==}
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.3.7':
|
||||||
|
resolution: {integrity: sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: 3.3.7
|
||||||
|
|
||||||
|
'@vue/shared@3.3.7':
|
||||||
|
resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
|
||||||
|
|
||||||
|
'@vueuse/core@10.5.0':
|
||||||
|
resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==}
|
||||||
|
|
||||||
|
'@vueuse/integrations@10.5.0':
|
||||||
|
resolution: {integrity: sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==}
|
||||||
|
peerDependencies:
|
||||||
|
async-validator: '*'
|
||||||
|
axios: '*'
|
||||||
|
change-case: '*'
|
||||||
|
drauu: '*'
|
||||||
|
focus-trap: '*'
|
||||||
|
fuse.js: '*'
|
||||||
|
idb-keyval: '*'
|
||||||
|
jwt-decode: '*'
|
||||||
|
nprogress: '*'
|
||||||
|
qrcode: '*'
|
||||||
|
sortablejs: '*'
|
||||||
|
universal-cookie: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
async-validator:
|
||||||
|
optional: true
|
||||||
|
axios:
|
||||||
|
optional: true
|
||||||
|
change-case:
|
||||||
|
optional: true
|
||||||
|
drauu:
|
||||||
|
optional: true
|
||||||
|
focus-trap:
|
||||||
|
optional: true
|
||||||
|
fuse.js:
|
||||||
|
optional: true
|
||||||
|
idb-keyval:
|
||||||
|
optional: true
|
||||||
|
jwt-decode:
|
||||||
|
optional: true
|
||||||
|
nprogress:
|
||||||
|
optional: true
|
||||||
|
qrcode:
|
||||||
|
optional: true
|
||||||
|
sortablejs:
|
||||||
|
optional: true
|
||||||
|
universal-cookie:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vueuse/metadata@10.5.0':
|
||||||
|
resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==}
|
||||||
|
|
||||||
|
'@vueuse/shared@10.5.0':
|
||||||
|
resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==}
|
||||||
|
|
||||||
|
algoliasearch@4.20.0:
|
||||||
|
resolution: {integrity: sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g==}
|
||||||
|
|
||||||
|
ansi-sequence-parser@1.1.1:
|
||||||
|
resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
|
||||||
|
|
||||||
|
csstype@3.1.2:
|
||||||
|
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||||
|
|
||||||
|
esbuild@0.18.20:
|
||||||
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
estree-walker@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
focus-trap@7.5.4:
|
||||||
|
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
jsonc-parser@3.2.0:
|
||||||
|
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||||
|
|
||||||
|
magic-string@0.30.5:
|
||||||
|
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
mark.js@8.11.1:
|
||||||
|
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||||
|
|
||||||
|
markdown-it-footnote@3.0.3:
|
||||||
|
resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==}
|
||||||
|
|
||||||
|
medium-zoom@1.0.8:
|
||||||
|
resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==}
|
||||||
|
|
||||||
|
minisearch@6.2.0:
|
||||||
|
resolution: {integrity: sha512-BECkorDF1TY2rGKt9XHdSeP9TP29yUbrAaCh/C03wpyf1vx3uYcP/+8XlMcpTkgoU0rBVnHMAOaP83Rc9Tm+TQ==}
|
||||||
|
|
||||||
|
nanoid@3.3.6:
|
||||||
|
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
picocolors@1.0.0:
|
||||||
|
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||||
|
|
||||||
|
postcss@8.4.31:
|
||||||
|
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
preact@10.18.1:
|
||||||
|
resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==}
|
||||||
|
|
||||||
|
rollup@3.29.4:
|
||||||
|
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
||||||
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
search-insights@2.9.0:
|
||||||
|
resolution: {integrity: sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==}
|
||||||
|
|
||||||
|
shiki@0.14.5:
|
||||||
|
resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==}
|
||||||
|
|
||||||
|
source-map-js@1.0.2:
|
||||||
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tabbable@6.2.0:
|
||||||
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
|
|
||||||
|
to-fast-properties@2.0.0:
|
||||||
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
vite@4.5.0:
|
||||||
|
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '>= 14'
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
vitepress-plugin-back-to-top@1.0.1:
|
||||||
|
resolution: {integrity: sha512-fQUzhkV6SGEmYcO56fv6Jbz5ifseAZuoi+lPDGg0c8gpMVHgqs/D1GbfX4tbvUAtuFNv4kz88wuQRK024iZ2sQ==}
|
||||||
|
|
||||||
|
vitepress@1.0.0-rc.24:
|
||||||
|
resolution: {integrity: sha512-RpnL8cnOGwiRlBbrYQUm9sYkJbtyOt/wYXk2diTcokY4yvks/5lq9LuSt+MURWB6ZqwpSNHvTmxgaSfLoG0/OA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
markdown-it-mathjax3: ^4.3.2
|
||||||
|
postcss: ^8.4.31
|
||||||
|
peerDependenciesMeta:
|
||||||
|
markdown-it-mathjax3:
|
||||||
|
optional: true
|
||||||
|
postcss:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
vscode-oniguruma@1.7.0:
|
||||||
|
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
|
||||||
|
|
||||||
|
vscode-textmate@8.0.0:
|
||||||
|
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
|
||||||
|
|
||||||
|
vue-demi@0.14.6:
|
||||||
|
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.0.0-rc.1
|
||||||
|
vue: ^3.0.0-0 || ^2.6.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
vue@3.3.7:
|
||||||
|
resolution: {integrity: sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)
|
||||||
|
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@algolia/client-search'
|
||||||
|
- algoliasearch
|
||||||
|
- search-insights
|
||||||
|
|
||||||
|
'@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
|
||||||
|
search-insights: 2.9.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@algolia/client-search'
|
||||||
|
- algoliasearch
|
||||||
|
|
||||||
|
'@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
|
||||||
|
'@algolia/client-search': 4.20.0
|
||||||
|
algoliasearch: 4.20.0
|
||||||
|
|
||||||
|
'@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/client-search': 4.20.0
|
||||||
|
algoliasearch: 4.20.0
|
||||||
|
|
||||||
|
'@algolia/cache-browser-local-storage@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/cache-common': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/cache-common@4.20.0': {}
|
||||||
|
|
||||||
|
'@algolia/cache-in-memory@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/cache-common': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/client-account@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/client-common': 4.20.0
|
||||||
|
'@algolia/client-search': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/client-analytics@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/client-common': 4.20.0
|
||||||
|
'@algolia/client-search': 4.20.0
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/client-common@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/client-personalization@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/client-common': 4.20.0
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/client-search@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/client-common': 4.20.0
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/logger-common@4.20.0': {}
|
||||||
|
|
||||||
|
'@algolia/logger-console@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/logger-common': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/requester-browser-xhr@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/requester-common@4.20.0': {}
|
||||||
|
|
||||||
|
'@algolia/requester-node-http@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
|
||||||
|
'@algolia/transporter@4.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/cache-common': 4.20.0
|
||||||
|
'@algolia/logger-common': 4.20.0
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.22.5': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.22.20': {}
|
||||||
|
|
||||||
|
'@babel/parser@7.23.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 7.23.0
|
||||||
|
|
||||||
|
'@babel/types@7.23.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 7.22.5
|
||||||
|
'@babel/helper-validator-identifier': 7.22.20
|
||||||
|
to-fast-properties: 2.0.0
|
||||||
|
|
||||||
|
'@docsearch/css@3.5.2': {}
|
||||||
|
|
||||||
|
'@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)
|
||||||
|
preact: 10.18.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@algolia/client-search'
|
||||||
|
- '@types/react'
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
|
- search-insights
|
||||||
|
|
||||||
|
'@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0)
|
||||||
|
'@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
|
||||||
|
'@docsearch/css': 3.5.2
|
||||||
|
algoliasearch: 4.20.0
|
||||||
|
search-insights: 2.9.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@algolia/client-search'
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.18.20':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.4.15': {}
|
||||||
|
|
||||||
|
'@types/linkify-it@3.0.4': {}
|
||||||
|
|
||||||
|
'@types/markdown-it@13.0.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/linkify-it': 3.0.4
|
||||||
|
'@types/mdurl': 1.0.4
|
||||||
|
|
||||||
|
'@types/mdurl@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.18': {}
|
||||||
|
|
||||||
|
'@vitejs/plugin-vue@4.3.1(vite@4.5.0)(vue@3.3.7)':
|
||||||
|
dependencies:
|
||||||
|
vite: 4.5.0
|
||||||
|
vue: 3.3.7
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.23.0
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
source-map-js: 1.0.2
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-core': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.23.0
|
||||||
|
'@vue/compiler-core': 3.3.7
|
||||||
|
'@vue/compiler-dom': 3.3.7
|
||||||
|
'@vue/compiler-ssr': 3.3.7
|
||||||
|
'@vue/reactivity-transform': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
magic-string: 0.30.5
|
||||||
|
postcss: 8.4.31
|
||||||
|
source-map-js: 1.0.2
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
|
||||||
|
'@vue/devtools-api@6.5.1': {}
|
||||||
|
|
||||||
|
'@vue/reactivity-transform@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.23.0
|
||||||
|
'@vue/compiler-core': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
magic-string: 0.30.5
|
||||||
|
|
||||||
|
'@vue/reactivity@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/reactivity': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/runtime-core': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
csstype: 3.1.2
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.3.7(vue@3.3.7)':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-ssr': 3.3.7
|
||||||
|
'@vue/shared': 3.3.7
|
||||||
|
vue: 3.3.7
|
||||||
|
|
||||||
|
'@vue/shared@3.3.7': {}
|
||||||
|
|
||||||
|
'@vueuse/core@10.5.0(vue@3.3.7)':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.18
|
||||||
|
'@vueuse/metadata': 10.5.0
|
||||||
|
'@vueuse/shared': 10.5.0(vue@3.3.7)
|
||||||
|
vue-demi: 0.14.6(vue@3.3.7)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/integrations@10.5.0(focus-trap@7.5.4)(vue@3.3.7)':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 10.5.0(vue@3.3.7)
|
||||||
|
'@vueuse/shared': 10.5.0(vue@3.3.7)
|
||||||
|
focus-trap: 7.5.4
|
||||||
|
vue-demi: 0.14.6(vue@3.3.7)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/metadata@10.5.0': {}
|
||||||
|
|
||||||
|
'@vueuse/shared@10.5.0(vue@3.3.7)':
|
||||||
|
dependencies:
|
||||||
|
vue-demi: 0.14.6(vue@3.3.7)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
|
algoliasearch@4.20.0:
|
||||||
|
dependencies:
|
||||||
|
'@algolia/cache-browser-local-storage': 4.20.0
|
||||||
|
'@algolia/cache-common': 4.20.0
|
||||||
|
'@algolia/cache-in-memory': 4.20.0
|
||||||
|
'@algolia/client-account': 4.20.0
|
||||||
|
'@algolia/client-analytics': 4.20.0
|
||||||
|
'@algolia/client-common': 4.20.0
|
||||||
|
'@algolia/client-personalization': 4.20.0
|
||||||
|
'@algolia/client-search': 4.20.0
|
||||||
|
'@algolia/logger-common': 4.20.0
|
||||||
|
'@algolia/logger-console': 4.20.0
|
||||||
|
'@algolia/requester-browser-xhr': 4.20.0
|
||||||
|
'@algolia/requester-common': 4.20.0
|
||||||
|
'@algolia/requester-node-http': 4.20.0
|
||||||
|
'@algolia/transporter': 4.20.0
|
||||||
|
|
||||||
|
ansi-sequence-parser@1.1.1: {}
|
||||||
|
|
||||||
|
csstype@3.1.2: {}
|
||||||
|
|
||||||
|
esbuild@0.18.20:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.18.20
|
||||||
|
'@esbuild/android-arm64': 0.18.20
|
||||||
|
'@esbuild/android-x64': 0.18.20
|
||||||
|
'@esbuild/darwin-arm64': 0.18.20
|
||||||
|
'@esbuild/darwin-x64': 0.18.20
|
||||||
|
'@esbuild/freebsd-arm64': 0.18.20
|
||||||
|
'@esbuild/freebsd-x64': 0.18.20
|
||||||
|
'@esbuild/linux-arm': 0.18.20
|
||||||
|
'@esbuild/linux-arm64': 0.18.20
|
||||||
|
'@esbuild/linux-ia32': 0.18.20
|
||||||
|
'@esbuild/linux-loong64': 0.18.20
|
||||||
|
'@esbuild/linux-mips64el': 0.18.20
|
||||||
|
'@esbuild/linux-ppc64': 0.18.20
|
||||||
|
'@esbuild/linux-riscv64': 0.18.20
|
||||||
|
'@esbuild/linux-s390x': 0.18.20
|
||||||
|
'@esbuild/linux-x64': 0.18.20
|
||||||
|
'@esbuild/netbsd-x64': 0.18.20
|
||||||
|
'@esbuild/openbsd-x64': 0.18.20
|
||||||
|
'@esbuild/sunos-x64': 0.18.20
|
||||||
|
'@esbuild/win32-arm64': 0.18.20
|
||||||
|
'@esbuild/win32-ia32': 0.18.20
|
||||||
|
'@esbuild/win32-x64': 0.18.20
|
||||||
|
|
||||||
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
focus-trap@7.5.4:
|
||||||
|
dependencies:
|
||||||
|
tabbable: 6.2.0
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
jsonc-parser@3.2.0: {}
|
||||||
|
|
||||||
|
magic-string@0.30.5:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
|
mark.js@8.11.1: {}
|
||||||
|
|
||||||
|
markdown-it-footnote@3.0.3: {}
|
||||||
|
|
||||||
|
medium-zoom@1.0.8: {}
|
||||||
|
|
||||||
|
minisearch@6.2.0: {}
|
||||||
|
|
||||||
|
nanoid@3.3.6: {}
|
||||||
|
|
||||||
|
picocolors@1.0.0: {}
|
||||||
|
|
||||||
|
postcss@8.4.31:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.6
|
||||||
|
picocolors: 1.0.0
|
||||||
|
source-map-js: 1.0.2
|
||||||
|
|
||||||
|
preact@10.18.1: {}
|
||||||
|
|
||||||
|
rollup@3.29.4:
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
search-insights@2.9.0: {}
|
||||||
|
|
||||||
|
shiki@0.14.5:
|
||||||
|
dependencies:
|
||||||
|
ansi-sequence-parser: 1.1.1
|
||||||
|
jsonc-parser: 3.2.0
|
||||||
|
vscode-oniguruma: 1.7.0
|
||||||
|
vscode-textmate: 8.0.0
|
||||||
|
|
||||||
|
source-map-js@1.0.2: {}
|
||||||
|
|
||||||
|
tabbable@6.2.0: {}
|
||||||
|
|
||||||
|
to-fast-properties@2.0.0: {}
|
||||||
|
|
||||||
|
vite@4.5.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.18.20
|
||||||
|
postcss: 8.4.31
|
||||||
|
rollup: 3.29.4
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
vitepress-plugin-back-to-top@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
vue: 3.3.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
vitepress@1.0.0-rc.24(@algolia/client-search@4.20.0)(search-insights@2.9.0):
|
||||||
|
dependencies:
|
||||||
|
'@docsearch/css': 3.5.2
|
||||||
|
'@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0)
|
||||||
|
'@types/markdown-it': 13.0.5
|
||||||
|
'@vitejs/plugin-vue': 4.3.1(vite@4.5.0)(vue@3.3.7)
|
||||||
|
'@vue/devtools-api': 6.5.1
|
||||||
|
'@vueuse/core': 10.5.0(vue@3.3.7)
|
||||||
|
'@vueuse/integrations': 10.5.0(focus-trap@7.5.4)(vue@3.3.7)
|
||||||
|
focus-trap: 7.5.4
|
||||||
|
mark.js: 8.11.1
|
||||||
|
minisearch: 6.2.0
|
||||||
|
shiki: 0.14.5
|
||||||
|
vite: 4.5.0
|
||||||
|
vue: 3.3.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@algolia/client-search'
|
||||||
|
- '@types/node'
|
||||||
|
- '@types/react'
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- async-validator
|
||||||
|
- axios
|
||||||
|
- change-case
|
||||||
|
- drauu
|
||||||
|
- fuse.js
|
||||||
|
- idb-keyval
|
||||||
|
- jwt-decode
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- nprogress
|
||||||
|
- qrcode
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
|
- sass
|
||||||
|
- search-insights
|
||||||
|
- sortablejs
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- terser
|
||||||
|
- typescript
|
||||||
|
- universal-cookie
|
||||||
|
|
||||||
|
vscode-oniguruma@1.7.0: {}
|
||||||
|
|
||||||
|
vscode-textmate@8.0.0: {}
|
||||||
|
|
||||||
|
vue-demi@0.14.6(vue@3.3.7):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.3.7
|
||||||
|
|
||||||
|
vue@3.3.7:
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.3.7
|
||||||
|
'@vue/compiler-sfc': 3.3.7
|
||||||
|
'@vue/runtime-dom': 3.3.7
|
||||||
|
'@vue/server-renderer': 3.3.7(vue@3.3.7)
|
||||||
|
'@vue/shared': 3.3.7
|
3
docs/pnpm-workspace.yaml
Normal file
3
docs/pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# empty workspace to prevent pnpm from installing docs deps unnecessarily
|
||||||
|
packages:
|
||||||
|
- .
|
3
docs/public/assets/mtproto_vs_botapi.svg
Normal file
3
docs/public/assets/mtproto_vs_botapi.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
BIN
docs/public/mtcute-logo.png
Normal file
BIN
docs/public/mtcute-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
5
docs/public/mtcute-logo.svg
Normal file
5
docs/public/mtcute-logo.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="363" viewBox="0 0 400 363">
|
||||||
|
<defs/>
|
||||||
|
<path fill="#DE6FBE" d="M299.961,8.049 C328.659,24.617 338.492,61.312 321.923,90.01 L304.003,121.049 L340,121.049 C373.137,121.049 400,147.911 400,181.048 C400,214.186 373.137,241.048 340,241.048 L303.766,241.048 L321.923,272.497 C338.492,301.195 328.659,337.89 299.961,354.459 C271.264,371.027 234.568,361.195 218,332.497 L78,90.01 C61.431,61.312 71.264,24.617 99.961,8.049 C128.659,-8.52 165.354,1.312 181.923,30.01 L199.961,61.254 L218,30.01 C234.568,1.312 271.264,-8.52 299.961,8.049 Z"/>
|
||||||
|
<path fill="#F69DDC" d="M252.093,210.96 C268.661,182.262 258.829,145.567 230.131,128.998 C219.617,122.928 208.028,120.401 196.75,121.048 L60,121.048 C26.863,121.048 0,147.911 0,181.049 C0,214.186 26.863,241.049 60,241.049 L96.157,241.049 L78.169,272.204 C61.601,300.901 71.433,337.596 100.131,354.165 C128.829,370.734 165.524,360.901 182.093,332.204 L252.093,210.96 Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 973 B |
Loading…
Reference in a new issue