├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── biome.json
├── bun.lock
├── deno.json
├── package.json
├── scripts
├── generate-changelog.ts
└── release-jsr.ts
├── src
├── bot.ts
├── composer.ts
├── errors.ts
├── filters.ts
├── index.ts
├── plugin.ts
├── queue.ts
├── types.ts
├── updates.ts
├── utils.internal.ts
├── utils.ts
└── webhook
│ ├── adapters.ts
│ └── index.ts
├── tests
├── types
│ ├── index.ts
│ └── triggers.ts
├── updates.test.ts
└── utils.test.ts
└── tsconfig.json
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish package
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | publish_to_npm:
7 | description: "Publish package to NPM"
8 | required: false
9 | type: boolean
10 | default: true
11 | publish_to_jsr:
12 | description: "Publish package to JSR"
13 | required: false
14 | type: boolean
15 | default: true
16 |
17 | permissions:
18 | contents: write
19 | id-token: write
20 |
21 | jobs:
22 | publish_package:
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 | with:
29 | fetch-depth: 0
30 |
31 | - name: Install Bun
32 | uses: oven-sh/setup-bun@v2
33 | with:
34 | bun-version: 1.1.34
35 |
36 | - id: changelog
37 | name: Generate changelog
38 | run: bun scripts/generate-changelog.ts
39 |
40 | - name: Install modules
41 | run: bun install
42 |
43 | - name: Run tests
44 | run: bun test
45 |
46 | - name: Prepare to JSR publish
47 | run: bun jsr
48 |
49 | - name: Type-check
50 | run: tsc --noEmit
51 |
52 | - name: Setup Deno
53 | uses: denoland/setup-deno@v1
54 |
55 | # - name: Publish package to JSR
56 | # if: ${{ github.event.inputs.publish_to_jsr }}
57 | # run: deno publish --allow-dirty --unstable-sloppy-imports --allow-slow-types
58 |
59 | - name: Publish package to NPM
60 | if: ${{ github.event.inputs.publish_to_npm }}
61 | run: bun publish --access public
62 | env:
63 | NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
64 |
65 | - name: GitHub Release
66 | uses: ncipollo/release-action@v1
67 | with:
68 | token: ${{ secrets.GITHUB_TOKEN }}
69 | tag: v${{ steps.changelog.outputs.version }}
70 | name: v${{ steps.changelog.outputs.version }}
71 | body: ${{ steps.changelog.outputs.changelog }}
72 | draft: false
73 | prerelease: false
74 | generateReleaseNotes: true
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | tsconfig.tsbuildinfo
4 | test.ts
5 | .env
6 | *.local.ts
7 | test.html
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Kravets
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GramIO
2 |
3 |
4 |
5 | [](https://core.telegram.org/bots/api)
6 | [](https://www.npmjs.org/package/gramio)
7 | [](https://www.npmjs.org/package/gramio)
8 | [](https://jsr.io/@gramio/core)
9 | [](https://jsr.io/@gramio/core)
10 |
11 |
12 |
13 | TypeScript/JavaScript Telegram Bot API Framework for create your bots with convenience!
14 |
15 | ✨ **Extensible** - Our [plugin](https://gramio.dev/plugins/) and [hook](https://gramio.dev/hooks/overview) system is awesome
16 |
17 | 🛡️ **Type-safe** - Written in TypeScript with love ❤️
18 |
19 | 🌐 **Multi-runtime** - Works on [Node.js](https://nodejs.org/), [Bun](https://bun.sh/) and [Deno](https://deno.com/)
20 |
21 | ⚙️ **Code-generated** - Many parts are code-generated (for example, [code-generated and auto-published Telegram Bot API types](https://github.com/gramiojs/types))
22 |
23 | ## [Get started](https://gramio.dev/get-started)
24 |
25 | To create your new bot, you just need to write it to the console:
26 |
27 | ```bash [npm]
28 | npm create gramio@latest ./bot
29 | ```
30 |
31 | and GramIO customize your project the way you want it!
32 |
33 | ### Example
34 |
35 | ```typescript
36 | import { Bot } from "gramio";
37 |
38 | const bot = new Bot(process.env.TOKEN as string)
39 | .command("start", (context) => context.send("Hello!"))
40 | .onStart(({ info }) => console.log(`✨ Bot ${info.username} was started!`));
41 |
42 | bot.start();
43 | ```
44 |
45 | For more, please see [documentation](https://gramio.dev).
46 |
47 | ### GramIO in action
48 |
49 | Example which uses some interesting features.
50 |
51 | ```ts
52 | import { Bot, format, bold, code } from "gramio";
53 | import { findOrRegisterUser } from "./utils";
54 |
55 | const bot = new Bot(process.env.BOT_TOKEN as string)
56 | .derive("message", async () => {
57 | const user = await findOrRegisterUser();
58 |
59 | return {
60 | user,
61 | };
62 | })
63 | .on("message", (context) => {
64 | context.user; // typed
65 |
66 | return context.send(format`
67 | Hi, ${bold(context.user.name)}!
68 | You balance: ${code(context.user.balance)}`);
69 | });
70 | ```
71 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "complexity": {
10 | "noBannedTypes": "off"
11 | },
12 | "suspicious": {
13 | "noExplicitAny": "off"
14 | },
15 | "recommended": true
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/bun.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1,
3 | "workspaces": {
4 | "": {
5 | "name": "gramio",
6 | "dependencies": {
7 | "@gramio/callback-data": "^0.0.3",
8 | "@gramio/contexts": "^0.2.5",
9 | "@gramio/files": "^0.3.0",
10 | "@gramio/format": "^0.2.1",
11 | "@gramio/keyboards": "^1.2.1",
12 | "@gramio/types": "^9.0.2",
13 | "debug": "^4.4.1",
14 | "middleware-io": "^2.8.1",
15 | },
16 | "devDependencies": {
17 | "@biomejs/biome": "1.9.4",
18 | "@types/bun": "^1.2.14",
19 | "@types/debug": "^4.1.12",
20 | "expect-type": "^1.2.1",
21 | "pkgroll": "^2.12.2",
22 | "typescript": "^5.8.3",
23 | },
24 | },
25 | },
26 | "packages": {
27 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
28 |
29 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
30 |
31 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
32 |
33 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
34 |
35 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
36 |
37 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
38 |
39 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
40 |
41 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
42 |
43 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
44 |
45 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="],
46 |
47 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="],
48 |
49 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="],
50 |
51 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="],
52 |
53 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="],
54 |
55 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="],
56 |
57 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="],
58 |
59 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="],
60 |
61 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="],
62 |
63 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="],
64 |
65 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="],
66 |
67 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="],
68 |
69 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="],
70 |
71 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="],
72 |
73 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="],
74 |
75 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="],
76 |
77 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="],
78 |
79 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="],
80 |
81 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="],
82 |
83 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="],
84 |
85 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="],
86 |
87 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="],
88 |
89 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="],
90 |
91 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="],
92 |
93 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="],
94 |
95 | "@gramio/callback-data": ["@gramio/callback-data@0.0.3", "", {}, "sha512-fL+fo3c58dIE6cl2yEoG9E4Y+mT+zvpw7PXImcATFVdRcK4mnuDzzxyOOibopzjKNszlHQZXYJer515kBr81RQ=="],
96 |
97 | "@gramio/contexts": ["@gramio/contexts@0.2.5", "", { "peerDependencies": { "@gramio/types": "^9.0.0", "inspectable": "^3.0.1" } }, "sha512-Pl7jvcyM1nhMo8Ud5aSM6Sl8E1xXxj2n7NI43FI3yK0N375c49fzw2E5fQ8bCLwbVGOzMwFsh985gv79AgLADg=="],
98 |
99 | "@gramio/files": ["@gramio/files@0.3.0", "", { "dependencies": { "@gramio/types": "^9.0.0" } }, "sha512-ZdrwRm57xxWzQ+KxGB5V2aC6raZ9B0yZavkPhd0JzKFUkKS1jbeG3W35qQxdlZZLtD9aeZbjwRAHk4VrRDbpqA=="],
100 |
101 | "@gramio/format": ["@gramio/format@0.2.1", "", { "dependencies": { "@gramio/types": "^9.0.1" } }, "sha512-amkEu4pIPETTp28+M63qFfenNwFJ++0dzXBlX1FjHROXqqsIO03YR16/ACdsh9qlBxooA/80DiSH+bcNzDPhww=="],
102 |
103 | "@gramio/keyboards": ["@gramio/keyboards@1.2.1", "", { "dependencies": { "@gramio/types": "^9.0.1" } }, "sha512-HqCJl9HmLTvckA2fMRc+mC258vBr2zKfMqvFEGEgaeauIIP4n8TTT9GU4YCqVFTGTzGA5I6gnfk87dGZuSP6vA=="],
104 |
105 | "@gramio/types": ["@gramio/types@9.0.2", "", {}, "sha512-6YQ9wO1gXgScLd1yfNZLYhi4+GxF4buyQfIKmVdef/ooZEaiQ01lV9rTy0SxC7IyA4mvLr5H21qPyp2Zlr8BSw=="],
106 |
107 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
108 |
109 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
110 |
111 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
112 |
113 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
114 |
115 | "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="],
116 |
117 | "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw=="],
118 |
119 | "@rollup/plugin-dynamic-import-vars": ["@rollup/plugin-dynamic-import-vars@2.1.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "astring": "^1.8.5", "estree-walker": "^2.0.2", "fast-glob": "^3.2.12", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-Mymi24fd9hlRifdZV/jYIFj1dn99F34imiYu3KzlAcgBcRi3i9SucgW/VRo5SQ9K4NuQ7dCep6pFWgNyhRdFHQ=="],
120 |
121 | "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="],
122 |
123 | "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
124 |
125 | "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg=="],
126 |
127 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
128 |
129 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.36.0", "", { "os": "android", "cpu": "arm" }, "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w=="],
130 |
131 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.36.0", "", { "os": "android", "cpu": "arm64" }, "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg=="],
132 |
133 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.36.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw=="],
134 |
135 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.36.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA=="],
136 |
137 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.36.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg=="],
138 |
139 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.36.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ=="],
140 |
141 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.36.0", "", { "os": "linux", "cpu": "arm" }, "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg=="],
142 |
143 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.36.0", "", { "os": "linux", "cpu": "arm" }, "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg=="],
144 |
145 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A=="],
146 |
147 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw=="],
148 |
149 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.36.0", "", { "os": "linux", "cpu": "none" }, "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg=="],
150 |
151 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.36.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg=="],
152 |
153 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.36.0", "", { "os": "linux", "cpu": "none" }, "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA=="],
154 |
155 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.36.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag=="],
156 |
157 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ=="],
158 |
159 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ=="],
160 |
161 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.36.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A=="],
162 |
163 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.36.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ=="],
164 |
165 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="],
166 |
167 | "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
168 |
169 | "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
170 |
171 | "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
172 |
173 | "@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="],
174 |
175 | "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
176 |
177 | "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
178 |
179 | "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
180 |
181 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
182 |
183 | "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
184 |
185 | "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
186 |
187 | "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
188 |
189 | "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
190 |
191 | "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="],
192 |
193 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
194 |
195 | "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
196 |
197 | "fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
198 |
199 | "fastq": ["fastq@1.18.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw=="],
200 |
201 | "fdir": ["fdir@6.4.2", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ=="],
202 |
203 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
204 |
205 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
206 |
207 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
208 |
209 | "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
210 |
211 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
212 |
213 | "inspectable": ["inspectable@3.0.2", "", {}, "sha512-XHygPjNXXe1TWQLlVdCYLejUklsGGo+XWS3Cn/RlkvnhkbZqcQxoXPrSh7Via5aATYJUMWVnrD5CbW2c+BtKMA=="],
214 |
215 | "is-core-module": ["is-core-module@2.15.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ=="],
216 |
217 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
218 |
219 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
220 |
221 | "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
222 |
223 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
224 |
225 | "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
226 |
227 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
228 |
229 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
230 |
231 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
232 |
233 | "middleware-io": ["middleware-io@2.8.1", "", {}, "sha512-H0XftkexHKxxQsoCsItMzM7WU3S/rIFzL3T4guU8tWLKr7e5cVkdaZ+JQeeL+TB3OaHpqFi/ozYqQl69z2X6bg=="],
234 |
235 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
236 |
237 | "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
238 |
239 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
240 |
241 | "pkgroll": ["pkgroll@2.12.2", "", { "dependencies": { "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-dynamic-import-vars": "^2.1.5", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/pluginutils": "^5.1.4", "esbuild": "^0.25.1", "magic-string": "^0.30.17", "rollup": "^4.34.6", "rollup-pluginutils": "^2.8.2" }, "peerDependencies": { "typescript": "^4.1 || ^5.0" }, "optionalPeers": ["typescript"], "bin": { "pkgroll": "dist/cli.mjs" } }, "sha512-Vl1hJ6jQj6YY9xvhuH8qNVf4qEZng9qC5jfqUKIR4+k5HaGdt6TCXj3lI5uNs+Z3ljtdlwMigi5re1YyDDhVxA=="],
242 |
243 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
244 |
245 | "resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="],
246 |
247 | "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
248 |
249 | "rollup": ["rollup@4.36.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.36.0", "@rollup/rollup-android-arm64": "4.36.0", "@rollup/rollup-darwin-arm64": "4.36.0", "@rollup/rollup-darwin-x64": "4.36.0", "@rollup/rollup-freebsd-arm64": "4.36.0", "@rollup/rollup-freebsd-x64": "4.36.0", "@rollup/rollup-linux-arm-gnueabihf": "4.36.0", "@rollup/rollup-linux-arm-musleabihf": "4.36.0", "@rollup/rollup-linux-arm64-gnu": "4.36.0", "@rollup/rollup-linux-arm64-musl": "4.36.0", "@rollup/rollup-linux-loongarch64-gnu": "4.36.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0", "@rollup/rollup-linux-riscv64-gnu": "4.36.0", "@rollup/rollup-linux-s390x-gnu": "4.36.0", "@rollup/rollup-linux-x64-gnu": "4.36.0", "@rollup/rollup-linux-x64-musl": "4.36.0", "@rollup/rollup-win32-arm64-msvc": "4.36.0", "@rollup/rollup-win32-ia32-msvc": "4.36.0", "@rollup/rollup-win32-x64-msvc": "4.36.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q=="],
250 |
251 | "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
252 |
253 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
254 |
255 | "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
256 |
257 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
258 |
259 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
260 |
261 | "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
262 |
263 | "@gramio/files/@gramio/types": ["@gramio/types@9.0.1", "", {}, "sha512-vg+GNdkHUjElMQhFxrhYJxOjQMy+PXBq1wEGh8EYRn0yQR7l3jSBVdf8z4AlRnWydIS2BO+x2PdHPNwZGIBx1w=="],
264 |
265 | "@gramio/keyboards/@gramio/types": ["@gramio/types@9.0.1", "", {}, "sha512-vg+GNdkHUjElMQhFxrhYJxOjQMy+PXBq1wEGh8EYRn0yQR7l3jSBVdf8z4AlRnWydIS2BO+x2PdHPNwZGIBx1w=="],
266 |
267 | "@rollup/plugin-inject/@rollup/pluginutils": ["@rollup/pluginutils@5.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A=="],
268 |
269 | "@rollup/plugin-inject/magic-string": ["magic-string@0.30.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw=="],
270 |
271 | "@rollup/plugin-json/@rollup/pluginutils": ["@rollup/pluginutils@5.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A=="],
272 |
273 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
274 |
275 | "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@gramio/core",
3 | "version": "0.0.48",
4 | "exports": "./src/index.ts",
5 | "publish": {
6 | "include": ["src", "README.md", "LICENSE"]
7 | },
8 | "imports": {
9 | "@gramio/types": "jsr:@gramio/types@^8.1.0",
10 | "@gramio/callback-data": "jsr:@gramio/callback-data@^0.0.3",
11 | "@gramio/contexts": "jsr:@gramio/contexts@^0.1.0",
12 | "@gramio/files": "jsr:@gramio/files@^0.1.2",
13 | "@gramio/format": "jsr:@gramio/format@^0.1.5",
14 | "@gramio/keyboards": "jsr:@gramio/keyboards@^1.0.2",
15 | "inspectable": "npm:inspectable@^3.0.2",
16 | "middleware-io": "npm:middleware-io@^2.8.1",
17 | "debug": "npm:debug@^4.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gramio",
3 | "type": "module",
4 | "version": "0.4.7",
5 | "description": "Powerful, extensible and really type-safe Telegram Bot API framework",
6 | "main": "dist/index.cjs",
7 | "module": "dist/index.js",
8 | "types": "dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": {
12 | "types": "./dist/index.d.ts",
13 | "default": "./dist/index.js"
14 | },
15 | "require": {
16 | "types": "./dist/index.d.cts",
17 | "default": "./dist/index.cjs"
18 | }
19 | },
20 | "./utils": {
21 | "import": {
22 | "types": "./dist/utils.d.ts",
23 | "default": "./dist/utils.js"
24 | },
25 | "require": {
26 | "types": "./dist/utils.d.cts",
27 | "default": "./dist/utils.cjs"
28 | }
29 | }
30 | },
31 | "keywords": [
32 | "telegram",
33 | "telegram-bot",
34 | "telegram-bot-api",
35 | "bot",
36 | "framework",
37 | "types",
38 | "client",
39 | "webhook",
40 | "long-polling",
41 | "files",
42 | "plugins",
43 | "format",
44 | "contexts",
45 | "files"
46 | ],
47 | "scripts": {
48 | "type": "tsc --noEmit",
49 | "lint": "bunx @biomejs/biome check ./src",
50 | "lint:fix": "bun lint --apply",
51 | "prepublishOnly": "bunx pkgroll",
52 | "jsr": "bun scripts/release-jsr.ts",
53 | "try-deno": "deno publish --unstable-sloppy-imports --dry-run --allow-slow-types --allow-dirty"
54 | },
55 | "author": "kravets",
56 | "license": "MIT",
57 | "devDependencies": {
58 | "@biomejs/biome": "1.9.4",
59 | "@types/bun": "^1.2.14",
60 | "@types/debug": "^4.1.12",
61 | "expect-type": "^1.2.1",
62 | "pkgroll": "^2.12.2",
63 | "typescript": "^5.8.3"
64 | },
65 | "dependencies": {
66 | "@gramio/callback-data": "^0.0.3",
67 | "@gramio/contexts": "^0.2.5",
68 | "@gramio/files": "^0.3.0",
69 | "@gramio/format": "^0.2.1",
70 | "@gramio/keyboards": "^1.2.1",
71 | "@gramio/types": "^9.0.2",
72 | "debug": "^4.4.1",
73 | "middleware-io": "^2.8.1"
74 | },
75 | "files": ["dist"]
76 | }
77 |
--------------------------------------------------------------------------------
/scripts/generate-changelog.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import { randomUUID } from "node:crypto";
3 | import { appendFileSync } from "node:fs";
4 | import { EOL } from "node:os";
5 |
6 | function getLatestTag() {
7 | try {
8 | return execSync("git describe --abbrev=0 --tags").toString().trim();
9 | } catch (e) {
10 | console.warn(e);
11 | return execSync("git rev-list --max-parents=0 HEAD").toString().trim();
12 | }
13 | }
14 |
15 | const commits = execSync(
16 | `git log ${getLatestTag()}..HEAD --pretty="format:%s%b"`,
17 | )
18 | .toString()
19 | .trim()
20 | .split("\n")
21 | .reverse();
22 |
23 | console.log(getLatestTag(), commits);
24 |
25 | const version = execSync("npm pkg get version").toString().replace(/"/gi, "");
26 |
27 | const delimiter = `---${randomUUID()}---${EOL}`;
28 |
29 | if (process.env.GITHUB_OUTPUT)
30 | appendFileSync(
31 | process.env.GITHUB_OUTPUT,
32 | `changelog<<${delimiter}${commits.join(
33 | EOL.repeat(2),
34 | )}${EOL}${delimiter}version=${version}${EOL}`,
35 | );
36 | else console.log("Not github actions");
37 |
--------------------------------------------------------------------------------
/scripts/release-jsr.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import fs from "node:fs";
3 |
4 | const version = execSync("npm pkg get version")
5 | .toString()
6 | .replace(/"|\n/gi, "");
7 |
8 | const jsrConfig = JSON.parse(String(fs.readFileSync("deno.json")));
9 |
10 | jsrConfig.version = version;
11 |
12 | fs.writeFileSync("deno.json", JSON.stringify(jsrConfig, null, 4));
13 |
14 | try {
15 | execSync("bun x @teidesu/slow-types-compiler@latest fix --entry deno.json");
16 | } catch (error) {
17 | console.error(error);
18 | }
19 |
20 | console.log("Prepared to release on JSR!");
21 |
--------------------------------------------------------------------------------
/src/bot.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import { Readable } from "node:stream";
3 | import { CallbackData } from "@gramio/callback-data";
4 | import {
5 | type Attachment,
6 | type Context,
7 | type ContextType,
8 | PhotoAttachment,
9 | type UpdateName,
10 | contextsMappings,
11 | } from "@gramio/contexts";
12 | import {
13 | convertJsonToFormData,
14 | extractFilesToFormData,
15 | isMediaUpload,
16 | } from "@gramio/files";
17 | import { FormattableMap } from "@gramio/format";
18 | import type {
19 | APIMethodParams,
20 | APIMethods,
21 | SetMyCommandsParams,
22 | TelegramAPIResponse,
23 | TelegramBotCommand,
24 | TelegramReactionType,
25 | TelegramReactionTypeEmojiEmoji,
26 | TelegramUser,
27 | } from "@gramio/types";
28 | import debug from "debug";
29 | import { ErrorKind, TelegramError } from "./errors.js";
30 | // import type { Filters } from "./filters";
31 | import { Plugin } from "./plugin.js";
32 | import type {
33 | AnyBot,
34 | AnyPlugin,
35 | BotOptions,
36 | BotStartOptions,
37 | CallbackQueryShorthandContext,
38 | DeriveDefinitions,
39 | ErrorDefinitions,
40 | FilterDefinitions,
41 | Handler,
42 | Hooks,
43 | MaybePromise,
44 | MaybeSuppressedParams,
45 | SuppressedAPIMethods,
46 | } from "./types.js";
47 | import { Updates } from "./updates.js";
48 | import { IS_BUN, type MaybeArray, simplifyObject } from "./utils.internal.ts";
49 | import { withRetries } from "./utils.ts";
50 |
51 | /** Bot instance
52 | *
53 | * @example
54 | * ```ts
55 | * import { Bot } from "gramio";
56 | *
57 | * const bot = new Bot("") // put you token here
58 | * .command("start", (context) => context.send("Hi!"))
59 | * .onStart(console.log);
60 | *
61 | * bot.start();
62 | * ```
63 | */
64 | export class Bot<
65 | Errors extends ErrorDefinitions = {},
66 | Derives extends DeriveDefinitions = DeriveDefinitions,
67 | // FiltersT extends FilterDefinitions = Filters,
68 | > {
69 | /** @deprecated use `~` instead*/
70 | _ = {
71 | /** @deprecated @internal. Remap generic */
72 | derives: {} as Derives,
73 | };
74 | /** @deprecated use `~.derives` instead @internal. Remap generic */
75 | __Derives!: Derives;
76 |
77 | "~" = this._;
78 |
79 | private filters: FilterDefinitions = {
80 | context: (name: string) => (context: Context) => context.is(name),
81 | };
82 |
83 | /** Options provided to instance */
84 | readonly options: BotOptions;
85 | /** Bot data (filled in when calling bot.init/bot.start) */
86 | info: TelegramUser | undefined;
87 | /**
88 | * Send API Request to Telegram Bot API
89 | *
90 | * @example
91 | * ```ts
92 | * const response = await bot.api.sendMessage({
93 | * chat_id: "@gramio_forum",
94 | * text: "some text",
95 | * });
96 | * ```
97 | *
98 | * [Documentation](https://gramio.dev/bot-api.html)
99 | */
100 | readonly api = new Proxy({} as SuppressedAPIMethods, {
101 | get: (
102 | _target: SuppressedAPIMethods,
103 | method: T,
104 | ) =>
105 | // @ts-expect-error
106 | // biome-ignore lint/suspicious/noAssignInExpressions:
107 | (_target[method] ??= (args: APIMethodParams) =>
108 | this._callApi(method, args)),
109 | });
110 | private lazyloadPlugins: Promise[] = [];
111 | private dependencies: string[] = [];
112 | private errorsDefinitions: Record<
113 | string,
114 | { new (...args: any): any; prototype: Error }
115 | > = {
116 | TELEGRAM: TelegramError,
117 | };
118 |
119 | private errorHandler(context: Context, error: Error) {
120 | if (!this.hooks.onError.length)
121 | return console.error("[Default Error Handler]", context, error);
122 |
123 | return this.runImmutableHooks("onError", {
124 | context,
125 | //@ts-expect-error ErrorKind exists if user register error-class with .error("kind", SomeError);
126 | kind: error.constructor[ErrorKind] ?? "UNKNOWN",
127 | error: error,
128 | });
129 | }
130 |
131 | /** This instance handle updates */
132 | updates = new Updates(this, this.errorHandler.bind(this));
133 |
134 | private hooks: Hooks.Store = {
135 | preRequest: [],
136 | onResponse: [],
137 | onResponseError: [],
138 | onError: [],
139 | onStart: [],
140 | onStop: [],
141 | };
142 |
143 | constructor(
144 | token: string,
145 | options?: Omit & {
146 | api?: Partial;
147 | },
148 | );
149 | constructor(
150 | options: Omit & { api?: Partial },
151 | );
152 | constructor(
153 | tokenOrOptions:
154 | | string
155 | | (Omit & { api?: Partial }),
156 | optionsRaw?: Omit & {
157 | api?: Partial;
158 | },
159 | ) {
160 | const token =
161 | typeof tokenOrOptions === "string"
162 | ? tokenOrOptions
163 | : tokenOrOptions?.token;
164 | const options =
165 | typeof tokenOrOptions === "object" ? tokenOrOptions : optionsRaw;
166 |
167 | if (!token) throw new Error("Token is required!");
168 |
169 | if (typeof token !== "string")
170 | throw new Error(`Token is ${typeof token} but it should be a string!`);
171 |
172 | this.options = {
173 | ...options,
174 | token,
175 | api: {
176 | baseURL: "https://api.telegram.org/bot",
177 | retryGetUpdatesWait: 1000,
178 | ...options?.api,
179 | },
180 | };
181 | if (options?.info) this.info = options.info;
182 |
183 | if (
184 | !(
185 | optionsRaw?.plugins &&
186 | "format" in optionsRaw.plugins &&
187 | !optionsRaw.plugins.format
188 | )
189 | )
190 | this.extend(
191 | new Plugin("@gramio/format").preRequest((context) => {
192 | if (!context.params) return context;
193 |
194 | // @ts-ignore
195 | const formattable = FormattableMap[context.method];
196 | // @ts-ignore add AnyTelegramMethod to @gramio/format
197 | if (formattable) context.params = formattable(context.params);
198 |
199 | return context;
200 | }),
201 | );
202 | }
203 |
204 | private async runHooks<
205 | T extends Exclude<
206 | keyof Hooks.Store,
207 | "onError" | "onStart" | "onStop" | "onResponseError" | "onResponse"
208 | >,
209 | >(type: T, context: Parameters[T][0]>[0]) {
210 | let data = context;
211 |
212 | for await (const hook of this.hooks[type]) {
213 | data = await hook(data);
214 | }
215 |
216 | return data;
217 | }
218 |
219 | private async runImmutableHooks<
220 | T extends Extract<
221 | keyof Hooks.Store,
222 | "onError" | "onStart" | "onStop" | "onResponseError" | "onResponse"
223 | >,
224 | >(type: T, ...context: Parameters[T][0]>) {
225 | for await (const hook of this.hooks[type]) {
226 | //TODO: solve that later
227 | //@ts-expect-error
228 | await hook(...context);
229 | }
230 | }
231 |
232 | private async _callApi(
233 | method: T,
234 | params: MaybeSuppressedParams = {},
235 | ) {
236 | const debug$api$method = debug(`gramio:api:${method}`);
237 | let url = `${this.options.api.baseURL}${this.options.token}/${this.options.api.useTest ? "test/" : ""}${method}`;
238 |
239 | // Omit<
240 | // NonNullable[1]>,
241 | // "headers"
242 | // > & {
243 | // headers: Headers;
244 | // }
245 | // idk why it cause https://github.com/gramiojs/gramio/actions/runs/10388006206/job/28762703484
246 | // also in logs types differs
247 | const reqOptions: any = {
248 | method: "POST",
249 | ...this.options.api.fetchOptions,
250 | // @ts-ignore types node/bun and global missmatch
251 | headers: new Headers(this.options.api.fetchOptions?.headers),
252 | };
253 |
254 | const context = await this.runHooks(
255 | "preRequest",
256 | // TODO: fix type error
257 | // @ts-expect-error
258 | {
259 | method,
260 | params,
261 | },
262 | );
263 |
264 | // biome-ignore lint/style/noParameterAssign: mutate params
265 | params = context.params;
266 |
267 | // @ts-ignore
268 | if (params && isMediaUpload(method, params)) {
269 | if (IS_BUN) {
270 | const formData = await convertJsonToFormData(method, params);
271 | reqOptions.body = formData;
272 | } else {
273 | const [formData, paramsWithoutFiles] = await extractFilesToFormData(
274 | method,
275 | params,
276 | );
277 | reqOptions.body = formData;
278 |
279 | const simplifiedParams = simplifyObject(paramsWithoutFiles);
280 | url += `?${new URLSearchParams(simplifiedParams).toString()}`;
281 | }
282 | } else {
283 | reqOptions.headers.set("Content-Type", "application/json");
284 |
285 | reqOptions.body = JSON.stringify(params);
286 | }
287 | debug$api$method("options: %j", reqOptions);
288 |
289 | const response = await fetch(url, reqOptions);
290 |
291 | const data = (await response.json()) as TelegramAPIResponse;
292 | debug$api$method("response: %j", data);
293 |
294 | if (!data.ok) {
295 | const err = new TelegramError(data, method, params);
296 |
297 | // @ts-expect-error
298 | this.runImmutableHooks("onResponseError", err, this.api);
299 |
300 | if (!params?.suppress) throw err;
301 |
302 | return err;
303 | }
304 |
305 | this.runImmutableHooks(
306 | "onResponse",
307 | // TODO: fix type error
308 | // @ts-expect-error
309 | {
310 | method,
311 | params,
312 | response: data.result as any,
313 | },
314 | );
315 |
316 | return data.result;
317 | }
318 |
319 | /**
320 | * Download file
321 | *
322 | * @example
323 | * ```ts
324 | * bot.on("message", async (context) => {
325 | * if (!context.document) return;
326 | * // download to ./file-name
327 | * await context.download(context.document.fileName || "file-name");
328 | * // get ArrayBuffer
329 | * const buffer = await context.download();
330 | *
331 | * return context.send("Thank you!");
332 | * });
333 | * ```
334 | * [Documentation](https://gramio.dev/files/download.html)
335 | */
336 | async downloadFile(
337 | attachment: Attachment | { file_id: string } | string,
338 | ): Promise;
339 | async downloadFile(
340 | attachment: Attachment | { file_id: string } | string,
341 | path: string,
342 | ): Promise;
343 |
344 | async downloadFile(
345 | attachment: Attachment | { file_id: string } | string,
346 | path?: string,
347 | ): Promise {
348 | function getFileId(attachment: Attachment | { file_id: string }) {
349 | if (attachment instanceof PhotoAttachment) {
350 | return attachment.bigSize.fileId;
351 | }
352 | if ("fileId" in attachment && typeof attachment.fileId === "string")
353 | return attachment.fileId;
354 | if ("file_id" in attachment) return attachment.file_id;
355 |
356 | throw new Error("Invalid attachment");
357 | }
358 |
359 | const fileId =
360 | typeof attachment === "string" ? attachment : getFileId(attachment);
361 |
362 | const file = await this.api.getFile({ file_id: fileId });
363 |
364 | const url = `${this.options.api.baseURL.replace("/bot", "/file/bot")}${
365 | this.options.token
366 | }/${file.file_path}`;
367 |
368 | const res = await fetch(url);
369 |
370 | if (path) {
371 | if (!res.body)
372 | throw new Error("Response without body (should be never throw)");
373 |
374 | // @ts-ignore denoo
375 | await fs.writeFile(path, Readable.fromWeb(res.body));
376 |
377 | return path;
378 | }
379 |
380 | const buffer = await res.arrayBuffer();
381 |
382 | return buffer;
383 | }
384 |
385 | /**
386 | * Register custom class-error for type-safe catch in `onError` hook
387 | *
388 | * @example
389 | * ```ts
390 | * export class NoRights extends Error {
391 | * needRole: "admin" | "moderator";
392 | *
393 | * constructor(role: "admin" | "moderator") {
394 | * super();
395 | * this.needRole = role;
396 | * }
397 | * }
398 | *
399 | * const bot = new Bot(process.env.TOKEN!)
400 | * .error("NO_RIGHTS", NoRights)
401 | * .onError(({ context, kind, error }) => {
402 | * if (context.is("message") && kind === "NO_RIGHTS")
403 | * return context.send(
404 | * format`You don't have enough rights! You need to have an «${bold(
405 | * error.needRole
406 | * )}» role.`
407 | * );
408 | * });
409 | *
410 | * bot.updates.on("message", (context) => {
411 | * if (context.text === "bun") throw new NoRights("admin");
412 | * });
413 | * ```
414 | */
415 | error<
416 | Name extends string,
417 | NewError extends { new (...args: any): any; prototype: Error },
418 | >(kind: Name, error: NewError) {
419 | //@ts-expect-error Set ErrorKind
420 | error[ErrorKind] = kind;
421 | this.errorsDefinitions[kind] = error;
422 |
423 | return this as unknown as Bot<
424 | Errors & { [name in Name]: InstanceType },
425 | Derives
426 | >;
427 | }
428 |
429 | /**
430 | * Set error handler.
431 | * @example
432 | * ```ts
433 | * bot.onError("message", ({ context, kind, error }) => {
434 | * return context.send(`${kind}: ${error.message}`);
435 | * })
436 | * ```
437 | */
438 |
439 | onError(
440 | updateName: MaybeArray,
441 | handler: Hooks.OnError>,
442 | ): this;
443 |
444 | onError(
445 | handler: Hooks.OnError & Derives["global"]>,
446 | ): this;
447 |
448 | onError(
449 | updateNameOrHandler: T | Hooks.OnError,
450 | handler?: Hooks.OnError>,
451 | ): this {
452 | if (typeof updateNameOrHandler === "function") {
453 | this.hooks.onError.push(updateNameOrHandler);
454 |
455 | return this;
456 | }
457 |
458 | if (handler) {
459 | this.hooks.onError.push(async (errContext) => {
460 | if (errContext.context.is(updateNameOrHandler))
461 | // TODO: Sorry... fix later
462 | //@ts-expect-error
463 | await handler(errContext);
464 | });
465 | }
466 |
467 | return this;
468 | }
469 |
470 | /**
471 | * Derive some data to handlers
472 | *
473 | * @example
474 | * ```ts
475 | * new Bot("token").derive((context) => {
476 | * return {
477 | * superSend: () => context.send("Derived method")
478 | * }
479 | * })
480 | * ```
481 | */
482 | derive<
483 | Handler extends Hooks.Derive & Derives["global"]>,
484 | >(
485 | handler: Handler,
486 | ): Bot> }>;
487 |
488 | derive<
489 | Update extends UpdateName,
490 | Handler extends Hooks.Derive<
491 | ContextType & Derives["global"] & Derives[Update]
492 | >,
493 | >(
494 | updateName: MaybeArray,
495 | handler: Handler,
496 | ): Bot> }>;
497 |
498 | derive<
499 | Update extends UpdateName,
500 | Handler extends Hooks.Derive<
501 | ContextType & Derives["global"] & Derives[Update]
502 | >,
503 | >(updateNameOrHandler: MaybeArray | Handler, handler?: Handler) {
504 | this.updates.composer.derive(updateNameOrHandler, handler);
505 |
506 | return this;
507 | }
508 |
509 | decorate>(
510 | value: Value,
511 | ): Bot<
512 | Errors,
513 | Derives & {
514 | global: {
515 | [K in keyof Value]: Value[K];
516 | };
517 | }
518 | >;
519 |
520 | decorate(
521 | name: Name,
522 | value: Value,
523 | ): Bot<
524 | Errors,
525 | Derives & {
526 | global: {
527 | [K in Name]: Value;
528 | };
529 | }
530 | >;
531 | decorate(
532 | nameOrRecordValue: Name | Record,
533 | value?: Value,
534 | ) {
535 | for (const contextName of Object.keys(contextsMappings)) {
536 | if (typeof nameOrRecordValue === "string")
537 | // @ts-expect-error
538 | Object.defineProperty(contextsMappings[contextName].prototype, name, {
539 | value,
540 | });
541 | else
542 | Object.defineProperties(
543 | // @ts-expect-error
544 | contextsMappings[contextName].prototype,
545 | Object.keys(nameOrRecordValue).reduce>(
546 | (acc, key) => {
547 | acc[key] = { value: nameOrRecordValue[key] };
548 | return acc;
549 | },
550 | {},
551 | ),
552 | );
553 | }
554 |
555 | return this;
556 | }
557 | /**
558 | * This hook called when the bot is `started`.
559 | *
560 | * @example
561 | * ```typescript
562 | * import { Bot } from "gramio";
563 | *
564 | * const bot = new Bot(process.env.TOKEN!).onStart(
565 | * ({ plugins, info, updatesFrom }) => {
566 | * console.log(`plugin list - ${plugins.join(", ")}`);
567 | * console.log(`bot username is @${info.username}`);
568 | * console.log(`updates from ${updatesFrom}`);
569 | * }
570 | * );
571 | *
572 | * bot.start();
573 | * ```
574 | *
575 | * [Documentation](https://gramio.dev/hooks/on-start.html)
576 | * */
577 | onStart(handler: Hooks.OnStart) {
578 | this.hooks.onStart.push(handler);
579 |
580 | return this;
581 | }
582 |
583 | /**
584 | * This hook called when the bot stops.
585 | *
586 | * @example
587 | * ```typescript
588 | * import { Bot } from "gramio";
589 | *
590 | * const bot = new Bot(process.env.TOKEN!).onStop(
591 | * ({ plugins, info, updatesFrom }) => {
592 | * console.log(`plugin list - ${plugins.join(", ")}`);
593 | * console.log(`bot username is @${info.username}`);
594 | * }
595 | * );
596 | *
597 | * bot.start();
598 | * bot.stop();
599 | * ```
600 | *
601 | * [Documentation](https://gramio.dev/hooks/on-stop.html)
602 | * */
603 | onStop(handler: Hooks.OnStop) {
604 | this.hooks.onStop.push(handler);
605 |
606 | return this;
607 | }
608 |
609 | /**
610 | * This hook called before sending a request to Telegram Bot API (allows us to impact the sent parameters).
611 | *
612 | * @example
613 | * ```typescript
614 | * import { Bot } from "gramio";
615 | *
616 | * const bot = new Bot(process.env.TOKEN!).preRequest((context) => {
617 | * if (context.method === "sendMessage") {
618 | * context.params.text = "mutate params";
619 | * }
620 | *
621 | * return context;
622 | * });
623 | *
624 | * bot.start();
625 | * ```
626 | *
627 | * [Documentation](https://gramio.dev/hooks/pre-request.html)
628 | * */
629 | preRequest<
630 | Methods extends keyof APIMethods,
631 | Handler extends Hooks.PreRequest,
632 | >(methods: MaybeArray, handler: Handler): this;
633 |
634 | preRequest(handler: Hooks.PreRequest): this;
635 |
636 | preRequest<
637 | Methods extends keyof APIMethods,
638 | Handler extends Hooks.PreRequest,
639 | >(
640 | methodsOrHandler: MaybeArray | Hooks.PreRequest,
641 | handler?: Handler,
642 | ) {
643 | if (
644 | typeof methodsOrHandler === "string" ||
645 | Array.isArray(methodsOrHandler)
646 | ) {
647 | // TODO: error
648 | if (!handler) throw new Error("TODO:");
649 |
650 | const methods =
651 | typeof methodsOrHandler === "string"
652 | ? [methodsOrHandler]
653 | : methodsOrHandler;
654 |
655 | // TODO: remove error
656 | // @ts-expect-error
657 | this.hooks.preRequest.push(async (context) => {
658 | // TODO: remove ts-ignore
659 | // @ts-expect-error
660 | if (methods.includes(context.method)) return handler(context);
661 |
662 | return context;
663 | });
664 | // @ts-expect-error
665 | } else this.hooks.preRequest.push(methodsOrHandler);
666 |
667 | return this;
668 | }
669 |
670 | /**
671 | * This hook called when API return successful response
672 | *
673 | * [Documentation](https://gramio.dev/hooks/on-response.html)
674 | * */
675 | onResponse<
676 | Methods extends keyof APIMethods,
677 | Handler extends Hooks.OnResponse,
678 | >(methods: MaybeArray, handler: Handler): this;
679 |
680 | onResponse(handler: Hooks.OnResponse): this;
681 |
682 | onResponse<
683 | Methods extends keyof APIMethods,
684 | Handler extends Hooks.OnResponse,
685 | >(
686 | methodsOrHandler: MaybeArray | Hooks.OnResponse,
687 | handler?: Handler,
688 | ) {
689 | if (
690 | typeof methodsOrHandler === "string" ||
691 | Array.isArray(methodsOrHandler)
692 | ) {
693 | // TODO: error
694 | if (!handler) throw new Error("TODO:");
695 |
696 | const methods =
697 | typeof methodsOrHandler === "string"
698 | ? [methodsOrHandler]
699 | : methodsOrHandler;
700 |
701 | this.hooks.onResponse.push(async (context) => {
702 | // TODO: remove ts-ignore
703 | // @ts-expect-error
704 | if (methods.includes(context.method)) return handler(context);
705 |
706 | return context;
707 | });
708 | // @ts-expect-error
709 | } else this.hooks.onResponse.push(methodsOrHandler);
710 |
711 | return this;
712 | }
713 |
714 | /**
715 | * This hook called when API return an error
716 | *
717 | * [Documentation](https://gramio.dev/hooks/on-response-error.html)
718 | * */
719 | onResponseError<
720 | Methods extends keyof APIMethods,
721 | Handler extends Hooks.OnResponseError,
722 | >(methods: MaybeArray, handler: Handler): this;
723 |
724 | onResponseError(handler: Hooks.OnResponseError): this;
725 |
726 | onResponseError<
727 | Methods extends keyof APIMethods,
728 | Handler extends Hooks.OnResponseError,
729 | >(
730 | methodsOrHandler: MaybeArray | Hooks.OnResponseError,
731 | handler?: Handler,
732 | ) {
733 | if (
734 | typeof methodsOrHandler === "string" ||
735 | Array.isArray(methodsOrHandler)
736 | ) {
737 | // TODO: error
738 | if (!handler) throw new Error("TODO:");
739 |
740 | const methods =
741 | typeof methodsOrHandler === "string"
742 | ? [methodsOrHandler]
743 | : methodsOrHandler;
744 |
745 | this.hooks.onResponseError.push(async (context) => {
746 | // TODO: remove ts-ignore
747 | // @ts-expect-error
748 | if (methods.includes(context.method)) return handler(context);
749 |
750 | return context;
751 | });
752 | // @ts-expect-error
753 | } else this.hooks.onResponseError.push(methodsOrHandler);
754 |
755 | return this;
756 | }
757 |
758 | // onExperimental(
759 | // // filter: Filters,
760 | // filter: (
761 | // f: Filters<
762 | // Context & Derives["global"],
763 | // [{ equal: { prop: number }; addition: { some: () => 2 } }]
764 | // >,
765 | // ) => Filters,
766 | // handler: Handler & Derives["global"]>,
767 | // ) {}
768 |
769 | /** Register handler to one or many Updates */
770 | on(
771 | updateName: MaybeArray,
772 | handler: Handler>,
773 | ) {
774 | this.updates.composer.on(updateName, handler);
775 |
776 | return this;
777 | }
778 |
779 | /** Register handler to any Updates */
780 | use(handler: Handler & Derives["global"]>) {
781 | this.updates.composer.use(handler);
782 |
783 | return this;
784 | }
785 |
786 | /**
787 | * Extend {@link Plugin} logic and types
788 | *
789 | * @example
790 | * ```ts
791 | * import { Plugin, Bot } from "gramio";
792 | *
793 | * export class PluginError extends Error {
794 | * wow: "type" | "safe" = "type";
795 | * }
796 | *
797 | * const plugin = new Plugin("gramio-example")
798 | * .error("PLUGIN", PluginError)
799 | * .derive(() => {
800 | * return {
801 | * some: ["derived", "props"] as const,
802 | * };
803 | * });
804 | *
805 | * const bot = new Bot(process.env.TOKEN!)
806 | * .extend(plugin)
807 | * .onError(({ context, kind, error }) => {
808 | * if (context.is("message") && kind === "PLUGIN") {
809 | * console.log(error.wow);
810 | * }
811 | * })
812 | * .use((context) => {
813 | * console.log(context.some);
814 | * });
815 | * ```
816 | */
817 | extend(
818 | plugin: MaybePromise,
819 | ): Bot<
820 | Errors & NewPlugin["_"]["Errors"],
821 | Derives & NewPlugin["_"]["Derives"]
822 | > {
823 | if (plugin instanceof Promise) {
824 | this.lazyloadPlugins.push(plugin);
825 |
826 | return this;
827 | }
828 |
829 | if (plugin._.dependencies.some((dep) => !this.dependencies.includes(dep)))
830 | throw new Error(
831 | `The «${
832 | plugin._.name
833 | }» plugin needs dependencies registered before: ${plugin._.dependencies
834 | .filter((dep) => !this.dependencies.includes(dep))
835 | .join(", ")}`,
836 | );
837 |
838 | if (plugin._.composer.length) {
839 | this.use(plugin._.composer.composed);
840 | }
841 |
842 | this.decorate(plugin._.decorators);
843 |
844 | for (const [key, value] of Object.entries(plugin._.errorsDefinitions)) {
845 | if (this.errorsDefinitions[key]) this.errorsDefinitions[key] = value;
846 | }
847 |
848 | for (const value of plugin._.preRequests) {
849 | const [preRequest, updateName] = value;
850 |
851 | if (!updateName) this.preRequest(preRequest);
852 | else this.preRequest(updateName, preRequest);
853 | }
854 |
855 | for (const value of plugin._.onResponses) {
856 | const [onResponse, updateName] = value;
857 |
858 | if (!updateName) this.onResponse(onResponse);
859 | else this.onResponse(updateName, onResponse);
860 | }
861 |
862 | for (const value of plugin._.onResponseErrors) {
863 | const [onResponseError, updateName] = value;
864 |
865 | if (!updateName) this.onResponseError(onResponseError);
866 | else this.onResponseError(updateName, onResponseError);
867 | }
868 |
869 | for (const handler of plugin._.groups) {
870 | this.group(handler);
871 | }
872 |
873 | for (const value of plugin._.onErrors) {
874 | this.onError(value);
875 | }
876 | for (const value of plugin._.onStarts) {
877 | this.onStart(value);
878 | }
879 | for (const value of plugin._.onStops) {
880 | this.onStop(value);
881 | }
882 |
883 | this.dependencies.push(plugin._.name);
884 |
885 | return this;
886 | }
887 |
888 | /**
889 | * Register handler to reaction (`message_reaction` update)
890 | *
891 | * @example
892 | * ```ts
893 | * new Bot().reaction("👍", async (context) => {
894 | * await context.reply(`Thank you!`);
895 | * });
896 | * ```
897 | * */
898 | reaction(
899 | trigger: MaybeArray,
900 | handler: (context: ContextType) => unknown,
901 | ) {
902 | const reactions = Array.isArray(trigger) ? trigger : [trigger];
903 |
904 | return this.on("message_reaction", (context, next) => {
905 | const newReactions: TelegramReactionType[] = [];
906 |
907 | for (const reaction of context.newReactions) {
908 | if (reaction.type !== "emoji") continue;
909 |
910 | const foundIndex = context.oldReactions.findIndex(
911 | (oldReaction) =>
912 | oldReaction.type === "emoji" &&
913 | oldReaction.emoji === reaction.emoji,
914 | );
915 | if (foundIndex === -1) {
916 | newReactions.push(reaction);
917 | } else {
918 | // TODO: REFACTOR
919 | context.oldReactions.splice(foundIndex, 1);
920 | }
921 | }
922 |
923 | if (
924 | !newReactions.some(
925 | (x) => x.type === "emoji" && reactions.includes(x.emoji),
926 | )
927 | )
928 | return next();
929 |
930 | return handler(context);
931 | });
932 | }
933 |
934 | /**
935 | * Register handler to `callback_query` event
936 | *
937 | * @example
938 | * ```ts
939 | * const someData = new CallbackData("example").number("id");
940 | *
941 | * new Bot()
942 | * .command("start", (context) =>
943 | * context.send("some", {
944 | * reply_markup: new InlineKeyboard().text(
945 | * "example",
946 | * someData.pack({
947 | * id: 1,
948 | * })
949 | * ),
950 | * })
951 | * )
952 | * .callbackQuery(someData, (context) => {
953 | * context.queryData; // is type-safe
954 | * });
955 | * ```
956 | */
957 | callbackQuery(
958 | trigger: Trigger,
959 | handler: (
960 | context: CallbackQueryShorthandContext,
961 | ) => unknown,
962 | ) {
963 | return this.on("callback_query", (context, next) => {
964 | if (!context.data) return next();
965 | if (typeof trigger === "string" && context.data !== trigger)
966 | return next();
967 | if (trigger instanceof RegExp) {
968 | if (!trigger.test(context.data)) return next();
969 | // @ts-expect-error
970 | context.queryData = context.data.match(trigger);
971 | }
972 | if (trigger instanceof CallbackData) {
973 | if (!trigger.regexp().test(context.data)) return next();
974 | // @ts-expect-error
975 | context.queryData = trigger.unpack(context.data);
976 | }
977 |
978 | //@ts-expect-error
979 | return handler(context);
980 | });
981 | }
982 |
983 | /** Register handler to `chosen_inline_result` update */
984 | chosenInlineResult>(
985 | trigger: RegExp | string | ((context: Ctx) => boolean),
986 | handler: (context: Ctx & { args: RegExpMatchArray | null }) => unknown,
987 | ) {
988 | return this.on("chosen_inline_result", (context, next) => {
989 | if (
990 | (typeof trigger === "string" && context.query === trigger) ||
991 | // @ts-expect-error
992 | (typeof trigger === "function" && trigger(context)) ||
993 | (trigger instanceof RegExp && trigger.test(context.query))
994 | ) {
995 | //@ts-expect-error
996 | context.args =
997 | trigger instanceof RegExp ? context.query?.match(trigger) : null;
998 |
999 | // TODO: remove
1000 | //@ts-expect-error
1001 | return handler(context);
1002 | }
1003 |
1004 | return next();
1005 | });
1006 | }
1007 |
1008 | /**
1009 | * Register handler to `inline_query` update
1010 | *
1011 | * @example
1012 | * ```ts
1013 | * new Bot().inlineQuery(
1014 | * /regular expression with (.*)/i,
1015 | * async (context) => {
1016 | * if (context.args) {
1017 | * await context.answer(
1018 | * [
1019 | * InlineQueryResult.article(
1020 | * "id-1",
1021 | * context.args[1],
1022 | * InputMessageContent.text("some"),
1023 | * {
1024 | * reply_markup: new InlineKeyboard().text(
1025 | * "some",
1026 | * "callback-data"
1027 | * ),
1028 | * }
1029 | * ),
1030 | * ],
1031 | * {
1032 | * cache_time: 0,
1033 | * }
1034 | * );
1035 | * }
1036 | * },
1037 | * {
1038 | * onResult: (context) => context.editText("Message edited!"),
1039 | * }
1040 | * );
1041 | * ```
1042 | * */
1043 | inlineQuery>(
1044 | trigger: RegExp | string | ((context: Ctx) => boolean),
1045 | handler: (context: Ctx & { args: RegExpMatchArray | null }) => unknown,
1046 | options: {
1047 | onResult?: (
1048 | context: ContextType & {
1049 | args: RegExpMatchArray | null;
1050 | },
1051 | ) => unknown;
1052 | } = {},
1053 | ) {
1054 | // @ts-expect-error fix later...
1055 | if (options.onResult) this.chosenInlineResult(trigger, options.onResult);
1056 |
1057 | return this.on("inline_query", (context, next) => {
1058 | if (
1059 | (typeof trigger === "string" && context.query === trigger) ||
1060 | // @ts-expect-error
1061 | (typeof trigger === "function" && trigger(context)) ||
1062 | (trigger instanceof RegExp && trigger.test(context.query))
1063 | ) {
1064 | //@ts-expect-error
1065 | context.args =
1066 | trigger instanceof RegExp ? context.query?.match(trigger) : null;
1067 |
1068 | // TODO: remove
1069 | //@ts-expect-error
1070 | return handler(context);
1071 | }
1072 |
1073 | return next();
1074 | });
1075 | }
1076 |
1077 | /**
1078 | * Register handler to `message` and `business_message` event
1079 | *
1080 | * new Bot().hears(/regular expression with (.*)/i, async (context) => {
1081 | * if (context.args) await context.send(`Params ${context.args[1]}`);
1082 | * });
1083 | */
1084 | hears<
1085 | Ctx = ContextType,
1086 | Trigger extends RegExp | string | ((context: Ctx) => boolean) =
1087 | | RegExp
1088 | | string
1089 | | ((context: Ctx) => boolean),
1090 | >(
1091 | trigger: Trigger,
1092 | handler: (context: Ctx & { args: RegExpMatchArray | null }) => unknown,
1093 | ) {
1094 | return this.on("message", (context, next) => {
1095 | const text = context.text ?? context.caption;
1096 | if (
1097 | (typeof trigger === "string" && text === trigger) ||
1098 | // @ts-expect-error
1099 | (typeof trigger === "function" && trigger(context)) ||
1100 | (trigger instanceof RegExp && text && trigger.test(text))
1101 | ) {
1102 | //@ts-expect-error
1103 | context.args = trigger instanceof RegExp ? text?.match(trigger) : null;
1104 |
1105 | // TODO: remove
1106 | //@ts-expect-error
1107 | return handler(context);
1108 | }
1109 |
1110 | return next();
1111 | });
1112 | }
1113 |
1114 | /**
1115 | * Register handler to `message` and `business_message` event when entities contains a command
1116 | *
1117 | * new Bot().command("start", async (context) => {
1118 | * return context.send(`You message is /start ${context.args}`);
1119 | * });
1120 | */
1121 | command(
1122 | command: MaybeArray,
1123 | handler: (
1124 | context: ContextType & { args: string | null },
1125 | ) => unknown,
1126 | // options?: Omit &
1127 | // Omit,
1128 | ) {
1129 | const normalizedCommands: string[] =
1130 | typeof command === "string" ? [command] : Array.from(command);
1131 |
1132 | for (const cmd of normalizedCommands) {
1133 | if (cmd.startsWith("/"))
1134 | throw new Error(`Do not use / in command name (${cmd})`);
1135 | }
1136 |
1137 | return this.on(["message", "business_message"], (context, next) => {
1138 | // TODO: change to find
1139 | if (
1140 | context.entities?.some((entity) => {
1141 | if (entity.type !== "bot_command" || entity.offset > 0) return false;
1142 |
1143 | const cmd = context.text
1144 | ?.slice(1, entity.length)
1145 | ?.replace(
1146 | this.info?.username ? `@${this.info.username}` : /@[a-zA-Z0-9_]+/,
1147 | "",
1148 | );
1149 | // @ts-expect-error
1150 | context.args = context.text?.slice(entity.length).trim() || null;
1151 |
1152 | return normalizedCommands.some(
1153 | (normalizedCommand) => cmd === normalizedCommand,
1154 | );
1155 | })
1156 | )
1157 | // @ts-expect-error
1158 | return handler(context);
1159 |
1160 | return next();
1161 | });
1162 | }
1163 |
1164 | /** Currently not isolated!!! */
1165 | group(grouped: (bot: typeof this) => AnyBot): typeof this {
1166 | return grouped(this) as any;
1167 | }
1168 |
1169 | /**
1170 | * Init bot. Call it manually only if you doesn't use {@link Bot.start}
1171 | */
1172 | async init() {
1173 | await Promise.all(
1174 | this.lazyloadPlugins.map(async (plugin) => this.extend(await plugin)),
1175 | );
1176 |
1177 | if (!this.info) {
1178 | const info = await this.api.getMe({
1179 | suppress: true,
1180 | });
1181 |
1182 | if (info instanceof TelegramError) {
1183 | if (info.code === 404)
1184 | info.message = "The bot token is incorrect. Check it in BotFather.";
1185 | throw info;
1186 | }
1187 |
1188 | this.info = info;
1189 | }
1190 | }
1191 |
1192 | /**
1193 | * Start receive updates via long-polling or webhook
1194 | *
1195 | * @example
1196 | * ```ts
1197 | * import { Bot } from "gramio";
1198 | *
1199 | * const bot = new Bot("") // put you token here
1200 | * .command("start", (context) => context.send("Hi!"))
1201 | * .onStart(console.log);
1202 | *
1203 | * bot.start();
1204 | * ```
1205 | */
1206 | async start({
1207 | webhook,
1208 | longPolling,
1209 | dropPendingUpdates,
1210 | allowedUpdates,
1211 | deleteWebhook: deleteWebhookRaw,
1212 | }: BotStartOptions = {}) {
1213 | await this.init();
1214 |
1215 | const deleteWebhook = deleteWebhookRaw ?? "on-conflict-with-polling";
1216 |
1217 | if (!webhook) {
1218 | if (deleteWebhook === true)
1219 | await withRetries(() =>
1220 | this.api.deleteWebhook({
1221 | drop_pending_updates: dropPendingUpdates,
1222 | }),
1223 | );
1224 |
1225 | this.updates.startPolling(
1226 | {
1227 | ...longPolling,
1228 | allowed_updates: allowedUpdates,
1229 | },
1230 | {
1231 | dropPendingUpdates,
1232 | deleteWebhookOnConflict: deleteWebhook === "on-conflict-with-polling",
1233 | },
1234 | );
1235 |
1236 | this.runImmutableHooks("onStart", {
1237 | plugins: this.dependencies,
1238 | // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1239 | info: this.info!,
1240 | updatesFrom: "long-polling",
1241 | });
1242 |
1243 | return this.info!;
1244 | }
1245 |
1246 | if (this.updates.isStarted) this.updates.stopPolling();
1247 |
1248 | // True means that we don't need to set webhook manually
1249 | if (webhook !== true)
1250 | await withRetries(async () =>
1251 | this.api.setWebhook({
1252 | ...(typeof webhook === "string" ? { url: webhook } : webhook),
1253 | drop_pending_updates: dropPendingUpdates,
1254 | allowed_updates: allowedUpdates,
1255 | // suppress: true,
1256 | }),
1257 | );
1258 |
1259 | this.runImmutableHooks("onStart", {
1260 | plugins: this.dependencies,
1261 | // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1262 | info: this.info!,
1263 | updatesFrom: "webhook",
1264 | });
1265 |
1266 | return this.info!;
1267 | }
1268 |
1269 | /**
1270 | * Stops receiving events via long-polling or webhook
1271 | * */
1272 | async stop(timeout = 3_000) {
1273 | await Promise.all(
1274 | [
1275 | this.updates.queue.stop(timeout),
1276 | this.updates.isStarted ? this.updates.stopPolling() : undefined,
1277 | ].filter(Boolean),
1278 | );
1279 |
1280 | await this.runImmutableHooks("onStop", {
1281 | plugins: this.dependencies,
1282 | // biome-ignore lint/style/noNonNullAssertion: bot.init() guarantees this.info
1283 | info: this.info!,
1284 | });
1285 | }
1286 | }
1287 |
--------------------------------------------------------------------------------
/src/composer.ts:
--------------------------------------------------------------------------------
1 | import type { Context, ContextType, UpdateName } from "@gramio/contexts";
2 | import {
3 | type CaughtMiddlewareHandler,
4 | type Middleware,
5 | Composer as MiddlewareComposer,
6 | noopNext,
7 | } from "middleware-io";
8 | import type { AnyBot, Handler, Hooks } from "./types.js";
9 | import type { MaybeArray } from "./utils.internal.js";
10 |
11 | /** Base-composer without chainable type-safety */
12 | export class Composer {
13 | protected composer = MiddlewareComposer.builder<
14 | Context & {
15 | [key: string]: unknown;
16 | }
17 | >();
18 | length = 0;
19 | composed!: Middleware>;
20 | protected onError: CaughtMiddlewareHandler>;
21 |
22 | constructor(onError?: CaughtMiddlewareHandler>) {
23 | this.onError =
24 | onError ||
25 | ((_, error) => {
26 | throw error;
27 | });
28 |
29 | this.recompose();
30 | }
31 |
32 | /** Register handler to one or many Updates */
33 | on(updateName: MaybeArray, handler: Handler) {
34 | return this.use(async (context, next) => {
35 | if (context.is(updateName)) return await handler(context, next);
36 |
37 | return await next();
38 | });
39 | }
40 |
41 | /** Register handler to any Update */
42 | use(handler: Handler) {
43 | this.composer.caught(this.onError).use(handler);
44 |
45 | return this.recompose();
46 | }
47 |
48 | /**
49 | * Derive some data to handlers
50 | *
51 | * @example
52 | * ```ts
53 | * new Bot("token").derive((context) => {
54 | * return {
55 | * superSend: () => context.send("Derived method")
56 | * }
57 | * })
58 | * ```
59 | */
60 | derive<
61 | Update extends UpdateName,
62 | Handler extends Hooks.Derive>,
63 | >(updateNameOrHandler: MaybeArray | Handler, handler?: Handler) {
64 | if (typeof updateNameOrHandler === "function")
65 | this.use(async (context, next) => {
66 | for (const [key, value] of Object.entries(
67 | await updateNameOrHandler(context),
68 | )) {
69 | context[key] = value;
70 | }
71 |
72 | return await next();
73 | });
74 | else if (handler)
75 | this.on(updateNameOrHandler, async (context, next) => {
76 | for (const [key, value] of Object.entries(await handler(context))) {
77 | context[key] = value;
78 | }
79 |
80 | return await next();
81 | });
82 |
83 | return this;
84 | }
85 |
86 | protected recompose() {
87 | // @ts-expect-error middleware-io moment
88 |
89 | this.composed = this.composer.compose();
90 | this.length = this.composer.length;
91 |
92 | return this;
93 | }
94 |
95 | compose(context: Context, next = noopNext) {
96 | this.composed(context, next);
97 | }
98 |
99 | composeWait(context: Context, next = noopNext) {
100 | return this.composed(context, next);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/errors.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | APIMethodParams,
3 | APIMethods,
4 | TelegramAPIResponseError,
5 | TelegramResponseParameters,
6 | } from "@gramio/types";
7 | import type { MaybeSuppressedParams } from "./types.js";
8 |
9 | /** Symbol to determine which error kind is it */
10 | export const ErrorKind: symbol = Symbol("ErrorKind");
11 |
12 | /** Represent {@link TelegramAPIResponseError} and thrown in API calls */
13 | export class TelegramError extends Error {
14 | /** Name of the API Method */
15 | method: T;
16 | /** Params that were sent */
17 | params: MaybeSuppressedParams;
18 | /** See {@link TelegramAPIResponseError.error_code}*/
19 | code: number;
20 | /** Describes why a request was unsuccessful. */
21 | payload?: TelegramResponseParameters;
22 |
23 | /** Construct new TelegramError */
24 | constructor(
25 | error: TelegramAPIResponseError,
26 | method: T,
27 | params: MaybeSuppressedParams,
28 | ) {
29 | super(error.description);
30 |
31 | this.name = method;
32 | this.method = method;
33 | this.params = params;
34 | this.code = error.error_code;
35 |
36 | if (error.parameters) this.payload = error.parameters;
37 | }
38 | }
39 |
40 | //@ts-expect-error
41 | TelegramError.constructor[ErrorKind] = "TELEGRAM";
42 |
--------------------------------------------------------------------------------
/src/filters.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Context,
3 | ContextType,
4 | MaybeArray,
5 | MessageContext,
6 | UpdateName,
7 | } from "@gramio/contexts";
8 | import type { Bot } from "./bot.js";
9 |
10 | export interface AdditionDefinitions {
11 | equal: any;
12 | addition: Record;
13 | }
14 |
15 | type ReturnIfNonNever = [T] extends [never] ? {} : T;
16 |
17 | export type Filters<
18 | BotType extends Bot = Bot,
19 | Base = Context,
20 | ConditionalAdditions extends AdditionDefinitions[] = [],
21 | > = {
22 | _s: Base;
23 | _ad: ConditionalAdditions;
24 | __filters: ((context: Context) => boolean)[];
25 | context(
26 | updateName: MaybeArray,
27 | ): Filters, ConditionalAdditions>;
28 | is2(): Filters;
29 | } & ReturnIfNonNever<
30 | {
31 | [K in keyof ConditionalAdditions &
32 | number]: ConditionalAdditions[K] extends {
33 | equal: infer E;
34 | addition: infer T;
35 | }
36 | ? Base extends E
37 | ? T
38 | : {}
39 | : {};
40 | }[number]
41 | >;
42 |
43 | // type filter = Filters<
44 | // Context & {prop: 2},
45 | // [{ equal: { prop: 2 }; addition: { some: 2 } }]
46 | // >;
47 |
48 | // const a = {} as filter;
49 |
50 | // // a.s;
51 |
52 | // type S = [{ equal: { prop: 2 }; addition: { some: 2 } }];
53 |
54 | // type C = {[K in keyof S & number]: S[K]};
55 |
56 | // type SA = {
57 | // [K in keyof S & number]: S[K] extends {
58 | // equal: infer E;
59 | // addition: infer T;
60 | // } ? Context & {prop: 2} extends E ? T : {} : {}}[number];
61 |
62 | // type A = Context & {prop: 2} extends SA ? true : false;
63 |
64 | // export const filters: Filters = {
65 | // __filters: [],
66 | // context(updateName) {
67 | // this.__filters.push((context) => context.is(updateName));
68 |
69 | // return this;
70 | // },
71 | // };
72 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module
3 | *
4 | * Powerful Telegram Bot API framework
5 | */
6 |
7 | // INFO: Temp polyfill, more info https://github.com/microsoft/TypeScript/issues/55453#issuecomment-1687496648
8 | (Symbol as any).metadata ??= Symbol("Symbol.metadata");
9 |
10 | export * from "./bot.js";
11 | export * from "./errors.js";
12 | export * from "./types.js";
13 | export * from "./plugin.js";
14 | export * from "./webhook/index.js";
15 | export * from "./composer.js";
16 | export * from "./updates.js";
17 |
18 | export * from "@gramio/contexts";
19 | export * from "@gramio/files";
20 | export * from "@gramio/keyboards";
21 | export type * from "@gramio/types";
22 | export * from "@gramio/format";
23 | export * from "@gramio/callback-data";
24 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BotLike,
3 | Context,
4 | ContextType,
5 | UpdateName,
6 | } from "@gramio/contexts";
7 | import type { APIMethods } from "@gramio/types";
8 | import type { Bot } from "./bot.js";
9 | import { Composer } from "./composer.js";
10 | import { ErrorKind } from "./errors.js";
11 | import type {
12 | AnyBot,
13 | AnyPlugin,
14 | DeriveDefinitions,
15 | ErrorDefinitions,
16 | Handler,
17 | Hooks,
18 | MaybePromise,
19 | } from "./types.js";
20 | import type { MaybeArray } from "./utils.internal.js";
21 |
22 | /**
23 | * `Plugin` is an object from which you can extends in Bot instance and adopt types
24 | *
25 | * @example
26 | * ```ts
27 | * import { Plugin, Bot } from "gramio";
28 | *
29 | * export class PluginError extends Error {
30 | * wow: "type" | "safe" = "type";
31 | * }
32 | *
33 | * const plugin = new Plugin("gramio-example")
34 | * .error("PLUGIN", PluginError)
35 | * .derive(() => {
36 | * return {
37 | * some: ["derived", "props"] as const,
38 | * };
39 | * });
40 | *
41 | * const bot = new Bot(process.env.TOKEN!)
42 | * .extend(plugin)
43 | * .onError(({ context, kind, error }) => {
44 | * if (context.is("message") && kind === "PLUGIN") {
45 | * console.log(error.wow);
46 | * }
47 | * })
48 | * .use((context) => {
49 | * console.log(context.some);
50 | * });
51 | * ```
52 | */
53 | export class Plugin<
54 | Errors extends ErrorDefinitions = {},
55 | Derives extends DeriveDefinitions = DeriveDefinitions,
56 | > {
57 | /**
58 | * @internal
59 | * Set of Plugin data
60 | *
61 | *
62 | */
63 | _ = {
64 | /** Name of plugin */
65 | name: "",
66 |
67 | /** List of plugin dependencies. If user does't extend from listed there dependencies it throw a error */
68 | dependencies: [] as string[],
69 | /** remap generic type. {} in runtime */
70 | Errors: {} as Errors,
71 | /** remap generic type. {} in runtime */
72 | Derives: {} as Derives,
73 | /** Composer */
74 | composer: new Composer(),
75 | /** Store plugin preRequests hooks */
76 | preRequests: [] as [
77 | Hooks.PreRequest,
78 | MaybeArray | undefined,
79 | ][],
80 | /** Store plugin onResponses hooks */
81 | onResponses: [] as [
82 | Hooks.OnResponse,
83 | MaybeArray | undefined,
84 | ][],
85 | /** Store plugin onResponseErrors hooks */
86 | onResponseErrors: [] as [
87 | Hooks.OnResponseError,
88 | MaybeArray | undefined,
89 | ][],
90 | /**
91 | * Store plugin groups
92 | *
93 | * If you use `on` or `use` in group and on plugin-level groups handlers are registered after plugin-level handlers
94 | * */
95 | groups: [] as ((bot: AnyBot) => AnyBot)[],
96 | /** Store plugin onStarts hooks */
97 | onStarts: [] as Hooks.OnStart[],
98 | /** Store plugin onStops hooks */
99 | onStops: [] as Hooks.OnStop[],
100 | /** Store plugin onErrors hooks */
101 | onErrors: [] as Hooks.OnError[],
102 | /** Map of plugin errors */
103 | errorsDefinitions: {} as Record<
104 | string,
105 | { new (...args: any): any; prototype: Error }
106 | >,
107 | decorators: {} as Record,
108 | };
109 |
110 | "~" = this._;
111 |
112 | /** Create new Plugin. Please provide `name` */
113 | constructor(
114 | name: string,
115 | { dependencies }: { dependencies?: string[] } = {},
116 | ) {
117 | this._.name = name;
118 | if (dependencies) this._.dependencies = dependencies;
119 | }
120 |
121 | /** Currently not isolated!!!
122 | *
123 | * > [!WARNING]
124 | * > If you use `on` or `use` in a `group` and at the plugin level, the group handlers are registered **after** the handlers at the plugin level
125 | */
126 | group(grouped: (bot: Bot) => AnyBot) {
127 | this._.groups.push(grouped);
128 |
129 | return this;
130 | }
131 |
132 | /**
133 | * Register custom class-error in plugin
134 | **/
135 | error<
136 | Name extends string,
137 | NewError extends { new (...args: any): any; prototype: Error },
138 | >(kind: Name, error: NewError) {
139 | //@ts-expect-error Set ErrorKind
140 | error[ErrorKind] = kind;
141 |
142 | this._.errorsDefinitions[kind] = error;
143 |
144 | return this as unknown as Plugin<
145 | Errors & { [name in Name]: InstanceType },
146 | Derives
147 | >;
148 | }
149 |
150 | /**
151 | * Derive some data to handlers
152 | *
153 | * @example
154 | * ```ts
155 | * new Bot("token").derive((context) => {
156 | * return {
157 | * superSend: () => context.send("Derived method")
158 | * }
159 | * })
160 | * ```
161 | */
162 | derive & Derives["global"]>>(
163 | handler: Handler,
164 | ): Plugin> }>;
165 |
166 | derive<
167 | Update extends UpdateName,
168 | Handler extends Hooks.Derive<
169 | ContextType & Derives["global"] & Derives[Update]
170 | >,
171 | >(
172 | updateName: MaybeArray,
173 | handler: Handler,
174 | ): Plugin> }>;
175 |
176 | derive<
177 | Update extends UpdateName,
178 | Handler extends Hooks.Derive<
179 | ContextType & Derives["global"] & Derives[Update]
180 | >,
181 | >(updateNameOrHandler: MaybeArray | Handler, handler?: Handler) {
182 | this._.composer.derive(updateNameOrHandler, handler);
183 |
184 | return this;
185 | }
186 |
187 | decorate>(
188 | value: Value,
189 | ): Plugin<
190 | Errors,
191 | Derives & {
192 | global: {
193 | [K in keyof Value]: Value[K];
194 | };
195 | }
196 | >;
197 |
198 | decorate(
199 | name: Name,
200 | value: Value,
201 | ): Plugin<
202 | Errors,
203 | Derives & {
204 | global: {
205 | [K in Name]: Value;
206 | };
207 | }
208 | >;
209 | decorate(
210 | nameOrValue: Name | Record,
211 | value?: Value,
212 | ) {
213 | if (typeof nameOrValue === "string") this._.decorators[nameOrValue] = value;
214 | else {
215 | for (const [name, value] of Object.entries(nameOrValue)) {
216 | this._.decorators[name] = value;
217 | }
218 | }
219 |
220 | return this;
221 | }
222 |
223 | /** Register handler to one or many Updates */
224 | on(
225 | updateName: MaybeArray,
226 | handler: Handler & Derives["global"] & Derives[T]>,
227 | ) {
228 | this._.composer.on(updateName, handler);
229 |
230 | return this;
231 | }
232 |
233 | /** Register handler to any Updates */
234 | use(handler: Handler & Derives["global"]>) {
235 | this._.composer.use(handler);
236 |
237 | return this;
238 | }
239 |
240 | /**
241 | * This hook called before sending a request to Telegram Bot API (allows us to impact the sent parameters).
242 | *
243 | * @example
244 | * ```typescript
245 | * import { Bot } from "gramio";
246 | *
247 | * const bot = new Bot(process.env.TOKEN!).preRequest((context) => {
248 | * if (context.method === "sendMessage") {
249 | * context.params.text = "mutate params";
250 | * }
251 | *
252 | * return context;
253 | * });
254 | *
255 | * bot.start();
256 | * ```
257 | *
258 | * [Documentation](https://gramio.dev/hooks/pre-request.html)
259 | * */
260 | preRequest<
261 | Methods extends keyof APIMethods,
262 | Handler extends Hooks.PreRequest,
263 | >(methods: MaybeArray, handler: Handler): this;
264 |
265 | preRequest(handler: Hooks.PreRequest): this;
266 |
267 | preRequest<
268 | Methods extends keyof APIMethods,
269 | Handler extends Hooks.PreRequest,
270 | >(
271 | methodsOrHandler: MaybeArray | Hooks.PreRequest,
272 | handler?: Handler,
273 | ) {
274 | if (
275 | (typeof methodsOrHandler === "string" ||
276 | Array.isArray(methodsOrHandler)) &&
277 | handler
278 | )
279 | this._.preRequests.push([handler, methodsOrHandler]);
280 | else if (typeof methodsOrHandler === "function")
281 | this._.preRequests.push([methodsOrHandler, undefined]);
282 |
283 | return this;
284 | }
285 |
286 | /**
287 | * This hook called when API return successful response
288 | *
289 | * [Documentation](https://gramio.dev/hooks/on-response.html)
290 | * */
291 | onResponse<
292 | Methods extends keyof APIMethods,
293 | Handler extends Hooks.OnResponse,
294 | >(methods: MaybeArray, handler: Handler): this;
295 |
296 | onResponse(handler: Hooks.OnResponse): this;
297 |
298 | onResponse<
299 | Methods extends keyof APIMethods,
300 | Handler extends Hooks.OnResponse,
301 | >(
302 | methodsOrHandler: MaybeArray | Hooks.OnResponse,
303 | handler?: Handler,
304 | ) {
305 | if (
306 | (typeof methodsOrHandler === "string" ||
307 | Array.isArray(methodsOrHandler)) &&
308 | handler
309 | )
310 | this._.onResponses.push([handler, methodsOrHandler]);
311 | else if (typeof methodsOrHandler === "function")
312 | this._.onResponses.push([methodsOrHandler, undefined]);
313 |
314 | return this;
315 | }
316 |
317 | /**
318 | * This hook called when API return an error
319 | *
320 | * [Documentation](https://gramio.dev/hooks/on-response-error.html)
321 | * */
322 | onResponseError<
323 | Methods extends keyof APIMethods,
324 | Handler extends Hooks.OnResponseError,
325 | >(methods: MaybeArray, handler: Handler): this;
326 |
327 | onResponseError(handler: Hooks.OnResponseError): this;
328 |
329 | onResponseError<
330 | Methods extends keyof APIMethods,
331 | Handler extends Hooks.OnResponseError,
332 | >(
333 | methodsOrHandler: MaybeArray | Hooks.OnResponseError,
334 | handler?: Handler,
335 | ) {
336 | if (
337 | (typeof methodsOrHandler === "string" ||
338 | Array.isArray(methodsOrHandler)) &&
339 | handler
340 | )
341 | this._.onResponseErrors.push([handler, methodsOrHandler]);
342 | else if (typeof methodsOrHandler === "function")
343 | this._.onResponseErrors.push([methodsOrHandler, undefined]);
344 |
345 | return this;
346 | }
347 |
348 | /**
349 | * This hook called when the bot is `started`.
350 | *
351 | * @example
352 | * ```typescript
353 | * import { Bot } from "gramio";
354 | *
355 | * const bot = new Bot(process.env.TOKEN!).onStart(
356 | * ({ plugins, info, updatesFrom }) => {
357 | * console.log(`plugin list - ${plugins.join(", ")}`);
358 | * console.log(`bot username is @${info.username}`);
359 | * console.log(`updates from ${updatesFrom}`);
360 | * }
361 | * );
362 | *
363 | * bot.start();
364 | * ```
365 | *
366 | * [Documentation](https://gramio.dev/hooks/on-start.html)
367 | * */
368 | onStart(handler: Hooks.OnStart) {
369 | this._.onStarts.push(handler);
370 |
371 | return this;
372 | }
373 |
374 | /**
375 | * This hook called when the bot stops.
376 | *
377 | * @example
378 | * ```typescript
379 | * import { Bot } from "gramio";
380 | *
381 | * const bot = new Bot(process.env.TOKEN!).onStop(
382 | * ({ plugins, info, updatesFrom }) => {
383 | * console.log(`plugin list - ${plugins.join(", ")}`);
384 | * console.log(`bot username is @${info.username}`);
385 | * }
386 | * );
387 | *
388 | * bot.start();
389 | * bot.stop();
390 | * ```
391 | *
392 | * [Documentation](https://gramio.dev/hooks/on-stop.html)
393 | * */
394 | onStop(handler: Hooks.OnStop) {
395 | this._.onStops.push(handler);
396 |
397 | return this;
398 | }
399 |
400 | /**
401 | * Set error handler.
402 | * @example
403 | * ```ts
404 | * bot.onError("message", ({ context, kind, error }) => {
405 | * return context.send(`${kind}: ${error.message}`);
406 | * })
407 | * ```
408 | */
409 | onError(
410 | updateName: MaybeArray,
411 | handler: Hooks.OnError<
412 | Errors,
413 | ContextType & Derives["global"] & Derives[T]
414 | >,
415 | ): this;
416 |
417 | onError(
418 | handler: Hooks.OnError & Derives["global"]>,
419 | ): this;
420 |
421 | onError(
422 | updateNameOrHandler: T | Hooks.OnError,
423 | handler?: Hooks.OnError<
424 | Errors,
425 | ContextType & Derives["global"] & Derives[T]
426 | >,
427 | ): this {
428 | if (typeof updateNameOrHandler === "function") {
429 | this._.onErrors.push(updateNameOrHandler);
430 |
431 | return this;
432 | }
433 |
434 | if (handler) {
435 | this._.onErrors.push(async (errContext) => {
436 | if (errContext.context.is(updateNameOrHandler))
437 | await handler(errContext);
438 | });
439 | }
440 |
441 | return this;
442 | }
443 |
444 | /**
445 | * ! ** At the moment, it can only pick up types** */
446 | extend(
447 | plugin: MaybePromise,
448 | ): Plugin<
449 | Errors & NewPlugin["_"]["Errors"],
450 | Derives & NewPlugin["_"]["Derives"]
451 | > {
452 | return this;
453 | }
454 | }
455 |
--------------------------------------------------------------------------------
/src/queue.ts:
--------------------------------------------------------------------------------
1 | import type { TelegramUpdate } from "@gramio/types";
2 | import { sleep } from "./utils.internal.ts";
3 |
4 | // concurrent queue (like event loop) for managing updates with graceful shutdown support
5 | export class UpdateQueue {
6 | private updateQueue: Data[] = [];
7 |
8 | private pendingUpdates: Set> = new Set();
9 | private handler: (update: Data) => Promise;
10 | private onIdleResolver: (() => void) | undefined;
11 | private onIdlePromise: Promise | undefined;
12 | private isActive = false;
13 |
14 | constructor(handler: (update: Data) => Promise) {
15 | //TODO: update errors
16 | this.handler = handler;
17 | }
18 |
19 | add(update: Data) {
20 | // console.log("ADD", update);
21 | this.updateQueue.push(update);
22 |
23 | this.start();
24 | }
25 |
26 | private start() {
27 | this.isActive = true;
28 |
29 | // try {
30 | while (this.updateQueue.length && this.isActive) {
31 | const update = this.updateQueue.shift();
32 |
33 | if (!update) continue;
34 | const promise = this.handler(update);
35 | this.pendingUpdates.add(promise);
36 |
37 | promise.finally(async () => {
38 | this.pendingUpdates.delete(promise);
39 |
40 | if (
41 | this.pendingUpdates.size === 0 &&
42 | this.updateQueue.length === 0 &&
43 | this.onIdleResolver
44 | ) {
45 | this.onIdleResolver();
46 | this.onIdleResolver = undefined;
47 | }
48 | });
49 | }
50 | // } finally {
51 | // this.isActive = false;
52 | // }
53 | }
54 |
55 | async stop(timeout = 3_000) {
56 | if (this.updateQueue.length === 0 && this.pendingUpdates.size === 0) {
57 | return;
58 | }
59 |
60 | this.onIdlePromise = new Promise((resolve) => {
61 | this.onIdleResolver = resolve;
62 | });
63 |
64 | await Promise.race([this.onIdlePromise, sleep(timeout)]);
65 |
66 | this.isActive = false;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { CallbackData } from "@gramio/callback-data";
2 | import type {
3 | BotLike,
4 | Context,
5 | ContextType,
6 | UpdateName,
7 | } from "@gramio/contexts";
8 | import type {
9 | APIMethodParams,
10 | APIMethodReturn,
11 | APIMethods,
12 | SetWebhookParams,
13 | TelegramUser,
14 | } from "@gramio/types";
15 | import type { NextMiddleware } from "middleware-io";
16 | import type { Bot } from "./bot.js";
17 | import type { TelegramError } from "./errors.js";
18 | import type { Plugin } from "./plugin.js";
19 |
20 | /** Bot options that you can provide to {@link Bot} constructor */
21 | export interface BotOptions {
22 | /** Bot token */
23 | token: string;
24 | /** When the bot begins to listen for updates, `GramIO` retrieves information about the bot to verify if the **bot token is valid**
25 | * and to utilize some bot metadata. For example, this metadata will be used to strip bot mentions in commands.
26 | *
27 | * If you set it up, `GramIO` will not send a `getMe` request on startup.
28 | *
29 | * @important
30 | * **You should set this up when horizontally scaling your bot or working in serverless environments.**
31 | * */
32 | info?: TelegramUser;
33 | /** List of plugins enabled by default */
34 | plugins?: {
35 | /** Pass `false` to disable plugin. @default true */
36 | format?: boolean;
37 | };
38 | /** Options to configure how to send requests to the Telegram Bot API */
39 | api: {
40 | /** Configure {@link fetch} parameters */
41 | fetchOptions?: Parameters[1];
42 | /** URL which will be used to send requests to. @default "https://api.telegram.org/bot" */
43 | baseURL: string;
44 | /**
45 | * Should we send requests to `test` data center?
46 | * The test environment is completely separate from the main environment, so you will need to create a new user account and a new bot with `@BotFather`.
47 | *
48 | * [Documentation](https://core.telegram.org/bots/webapps#using-bots-in-the-test-environment)
49 | * @default false
50 | * */
51 | useTest?: boolean;
52 |
53 | /**
54 | * Time in milliseconds before calling {@link APIMethods.getUpdates | getUpdates} again
55 | * @default 1000
56 | */
57 | retryGetUpdatesWait?: number;
58 | };
59 | }
60 |
61 | /**
62 | * Handler is a function with context and next function arguments
63 | *
64 | * @example
65 | * ```ts
66 | * const handler: Handler> = (context, _next) => context.send("HI!");
67 | *
68 | * bot.on("message", handler)
69 | * ```
70 | */
71 | export type Handler = (context: T, next: NextMiddleware) => unknown;
72 |
73 | interface ErrorHandlerParams<
74 | Ctx extends Context,
75 | Kind extends string,
76 | Err,
77 | > {
78 | context: Ctx;
79 | kind: Kind;
80 | error: Err;
81 | }
82 |
83 | type AnyTelegramError = {
84 | [APIMethod in Methods]: TelegramError;
85 | }[Methods];
86 |
87 | type AnyTelegramMethod = {
88 | [APIMethod in Methods]: {
89 | method: APIMethod;
90 | params: MaybeSuppressedParams;
91 | };
92 | }[Methods];
93 |
94 | /**
95 | * Interface for add `suppress` param to params
96 | */
97 | export interface Suppress<
98 | IsSuppressed extends boolean | undefined = undefined,
99 | > {
100 | /**
101 | * Pass `true` if you want to suppress throwing errors of this method.
102 | *
103 | * **But this does not undo getting into the `onResponseError` hook**.
104 | *
105 | * @example
106 | * ```ts
107 | * const response = await bot.api.sendMessage({
108 | * suppress: true,
109 | * chat_id: "@not_found",
110 | * text: "Suppressed method"
111 | * });
112 | *
113 | * if(response instanceof TelegramError) console.error("sendMessage returns an error...")
114 | * else console.log("Message has been sent successfully");
115 | * ```
116 | *
117 | * */
118 | suppress?: IsSuppressed;
119 | }
120 |
121 | /** Type that assign API params with {@link Suppress} */
122 | export type MaybeSuppressedParams<
123 | Method extends keyof APIMethods,
124 | IsSuppressed extends boolean | undefined = undefined,
125 | > = APIMethodParams & Suppress;
126 |
127 | /** Return method params but with {@link Suppress} */
128 | export type SuppressedAPIMethodParams =
129 | undefined extends APIMethodParams
130 | ? Suppress
131 | : MaybeSuppressedParams;
132 |
133 | /** Type that return MaybeSuppressed API method ReturnType */
134 | export type MaybeSuppressedReturn<
135 | Method extends keyof APIMethods,
136 | IsSuppressed extends boolean | undefined = undefined,
137 | > = true extends IsSuppressed
138 | ? TelegramError | APIMethodReturn
139 | : APIMethodReturn;
140 |
141 | /** Type that return {@link Suppress | Suppressed} API method ReturnType */
142 | export type SuppressedAPIMethodReturn =
143 | MaybeSuppressedReturn;
144 |
145 | /** Map of APIMethods but with {@link Suppress} */
146 | export type SuppressedAPIMethods<
147 | Methods extends keyof APIMethods = keyof APIMethods,
148 | > = {
149 | [APIMethod in Methods]: APIMethodParams extends undefined
150 | ? (
151 | params?: Suppress,
152 | ) => Promise>
153 | : undefined extends APIMethodParams
154 | ? (
155 | params?: MaybeSuppressedParams,
156 | ) => Promise>
157 | : (
158 | params: MaybeSuppressedParams,
159 | ) => Promise>;
160 | };
161 |
162 | type AnyTelegramMethodWithReturn = {
163 | [APIMethod in Methods]: {
164 | method: APIMethod;
165 | params: APIMethodParams;
166 | response: APIMethodReturn;
167 | };
168 | }[Methods];
169 |
170 | /** Type for maybe {@link Promise} or may not */
171 | export type MaybePromise = Promise | T;
172 |
173 | /**
174 | * Namespace with GramIO hooks types
175 | *
176 | * [Documentation](https://gramio.dev/hooks/overview.html)
177 | * */
178 | export namespace Hooks {
179 | /** Derive */
180 | export type Derive = (
181 | context: Ctx,
182 | ) => MaybePromise>;
183 |
184 | /** Argument type for {@link PreRequest} */
185 | export type PreRequestContext =
186 | AnyTelegramMethod;
187 |
188 | /**
189 | * Type for `preRequest` hook
190 | *
191 | * @example
192 | * ```typescript
193 | * import { Bot } from "gramio";
194 | *
195 | * const bot = new Bot(process.env.TOKEN!).preRequest((context) => {
196 | * if (context.method === "sendMessage") {
197 | * context.params.text = "mutate params";
198 | * }
199 | *
200 | * return context;
201 | * });
202 | *
203 | * bot.start();
204 | * ```
205 | *
206 | * [Documentation](https://gramio.dev/hooks/pre-request.html)
207 | * */
208 | export type PreRequest =
209 | (
210 | ctx: PreRequestContext,
211 | ) => MaybePromise>;
212 |
213 | /** Argument type for {@link OnError} */
214 | export type OnErrorContext<
215 | Ctx extends Context,
216 | T extends ErrorDefinitions,
217 | > =
218 | | ErrorHandlerParams
219 | | ErrorHandlerParams
220 | | {
221 | // TODO: improve typings
222 | [K in keyof T]: ErrorHandlerParams;
223 | }[keyof T];
224 |
225 | /**
226 | * Type for `onError` hook
227 | *
228 | * @example
229 | * ```typescript
230 | * bot.on("message", () => {
231 | * bot.api.sendMessage({
232 | * chat_id: "@not_found",
233 | * text: "Chat not exists....",
234 | * });
235 | * });
236 | *
237 | * bot.onError(({ context, kind, error }) => {
238 | * if (context.is("message")) return context.send(`${kind}: ${error.message}`);
239 | * });
240 | * ```
241 | *
242 | * [Documentation](https://gramio.dev/hooks/on-error.html)
243 | * */
244 | export type OnError<
245 | T extends ErrorDefinitions,
246 | Ctx extends Context = Context,
247 | > = (options: OnErrorContext) => unknown;
248 |
249 | /**
250 | * Type for `onStart` hook
251 | *
252 | * @example
253 | * ```typescript
254 | * import { Bot } from "gramio";
255 | *
256 | * const bot = new Bot(process.env.TOKEN!).onStart(
257 | * ({ plugins, info, updatesFrom }) => {
258 | * console.log(`plugin list - ${plugins.join(", ")}`);
259 | * console.log(`bot username is @${info.username}`);
260 | * console.log(`updates from ${updatesFrom}`);
261 | * }
262 | * );
263 | *
264 | * bot.start();
265 | * ```
266 | *
267 | * [Documentation](https://gramio.dev/hooks/on-start.html)
268 | * */
269 | export type OnStart = (context: {
270 | plugins: string[];
271 | info: TelegramUser;
272 | updatesFrom: "webhook" | "long-polling";
273 | }) => unknown;
274 |
275 | /**
276 | * Type for `onStop` hook
277 | *
278 | * @example
279 | * ```typescript
280 | * import { Bot } from "gramio";
281 | *
282 | * const bot = new Bot(process.env.TOKEN!).onStop(
283 | * ({ plugins, info, updatesFrom }) => {
284 | * console.log(`plugin list - ${plugins.join(", ")}`);
285 | * console.log(`bot username is @${info.username}`);
286 | * }
287 | * );
288 | *
289 | * bot.start();
290 | * bot.stop();
291 | * ```
292 | *
293 | * [Documentation](https://gramio.dev/hooks/on-stop.html)
294 | * */
295 | export type OnStop = (context: {
296 | plugins: string[];
297 | info: TelegramUser;
298 | }) => unknown;
299 |
300 | /**
301 | * Type for `onResponseError` hook
302 | *
303 | * [Documentation](https://gramio.dev/hooks/on-response-error.html)
304 | * */
305 | export type OnResponseError<
306 | Methods extends keyof APIMethods = keyof APIMethods,
307 | > = (context: AnyTelegramError, api: Bot["api"]) => unknown;
308 |
309 | /**
310 | * Type for `onResponse` hook
311 | *
312 | * [Documentation](https://gramio.dev/hooks/on-response.html)
313 | * */
314 | export type OnResponse =
315 | (context: AnyTelegramMethodWithReturn) => unknown;
316 |
317 | /** Store hooks */
318 | export interface Store {
319 | preRequest: PreRequest[];
320 | onResponse: OnResponse[];
321 | onResponseError: OnResponseError[];
322 | onError: OnError[];
323 | onStart: OnStart[];
324 | onStop: OnStop[];
325 | }
326 | }
327 |
328 | /** Error map should be map of string: error */
329 | export type ErrorDefinitions = Record;
330 |
331 | /** Map of derives */
332 | export type DeriveDefinitions = Record;
333 |
334 | export type FilterDefinitions = Record<
335 | string,
336 | (...args: any[]) => (context: Context) => boolean
337 | >;
338 |
339 | /** Type of Bot that accepts any generics */
340 | export type AnyBot = Bot;
341 |
342 | /** Type of Bot that accepts any generics */
343 | export type AnyPlugin = Plugin;
344 |
345 | export type CallbackQueryShorthandContext<
346 | BotType extends BotLike,
347 | Trigger extends CallbackData | string | RegExp,
348 | > = Omit, "data"> &
349 | BotType["__Derives"]["global"] &
350 | BotType["__Derives"]["callback_query"] & {
351 | queryData: Trigger extends CallbackData
352 | ? ReturnType
353 | : Trigger extends RegExp
354 | ? RegExpMatchArray
355 | : never;
356 | };
357 |
358 | export type BotStartOptionsLongPolling = Omit<
359 | NonNullable>,
360 | "allowed_updates" | "offset"
361 | >;
362 |
363 | export type BotStartOptionsWebhook =
364 | | true
365 | | string
366 | | Omit;
367 |
368 | export type AllowedUpdates = Exclude<
369 | NonNullable>["allowed_updates"],
370 | "update_id"
371 | >;
372 |
373 | export interface BotStartOptions {
374 | webhook?: BotStartOptionsWebhook;
375 | longPolling?: BotStartOptionsLongPolling;
376 | dropPendingUpdates?: boolean;
377 | allowedUpdates?: AllowedUpdates;
378 | // "on conflict with long-polling"
379 | deleteWebhook?: boolean | "on-conflict-with-polling";
380 | }
381 |
382 | export interface PollingStartOptions {
383 | dropPendingUpdates?: boolean;
384 | deleteWebhookOnConflict?: boolean;
385 | }
386 |
--------------------------------------------------------------------------------
/src/updates.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Context,
3 | type UpdateName,
4 | contextsMappings,
5 | } from "@gramio/contexts";
6 | import type { APIMethodParams, TelegramUpdate } from "@gramio/types";
7 | import type { CaughtMiddlewareHandler } from "middleware-io";
8 | import { Composer } from "./composer.js";
9 | import { TelegramError } from "./errors.js";
10 | import { UpdateQueue } from "./queue.js";
11 | import type { AnyBot, PollingStartOptions } from "./types.js";
12 | import { debug$updates, sleep } from "./utils.internal.ts";
13 | import { withRetries } from "./utils.ts";
14 |
15 | export class Updates {
16 | private readonly bot: AnyBot;
17 | isStarted = false;
18 | isRequestActive = false;
19 | private offset = 0;
20 | composer: Composer;
21 | queue: UpdateQueue;
22 | stopPollingPromiseResolve: ((value?: undefined) => void) | undefined;
23 |
24 | constructor(bot: AnyBot, onError: CaughtMiddlewareHandler>) {
25 | this.bot = bot;
26 | this.composer = new Composer(onError);
27 | this.queue = new UpdateQueue(this.handleUpdate.bind(this));
28 | }
29 |
30 | async handleUpdate(data: TelegramUpdate, mode: "wait" | "lazy" = "wait") {
31 | const updateType = Object.keys(data).at(1) as UpdateName;
32 |
33 | const UpdateContext = contextsMappings[updateType];
34 | if (!UpdateContext) throw new Error(updateType);
35 |
36 | const updatePayload =
37 | data[updateType as Exclude];
38 | if (!updatePayload) throw new Error("Unsupported event??");
39 | try {
40 | let context = new UpdateContext({
41 | bot: this.bot,
42 | update: data,
43 | // @ts-expect-error
44 | payload: updatePayload,
45 | type: updateType,
46 | updateId: data.update_id,
47 | });
48 |
49 | if ("isEvent" in context && context.isEvent() && context.eventType) {
50 | const payload =
51 | data.message ??
52 | data.edited_message ??
53 | data.channel_post ??
54 | data.edited_channel_post ??
55 | data.business_message;
56 | if (!payload) throw new Error("Unsupported event??");
57 |
58 | context = new contextsMappings[context.eventType]({
59 | bot: this.bot,
60 | update: data,
61 | payload,
62 | // @ts-expect-error
63 | type: context.eventType,
64 | updateId: data.update_id,
65 | });
66 | }
67 |
68 | return mode === "wait"
69 | ? this.composer.composeWait(context as unknown as Context)
70 | : this.composer.compose(context as unknown as Context);
71 | } catch (error) {
72 | throw new Error(`[UPDATES] Update type ${updateType} not supported.`);
73 | }
74 | }
75 |
76 | /** @deprecated use bot.start instead @internal */
77 | startPolling(
78 | params: APIMethodParams<"getUpdates"> = {},
79 | options: PollingStartOptions = {},
80 | ) {
81 | if (this.isStarted) throw new Error("[UPDATES] Polling already started!");
82 |
83 | this.isStarted = true;
84 |
85 | this.startFetchLoop(params, options);
86 |
87 | return;
88 | }
89 |
90 | async startFetchLoop(
91 | params: APIMethodParams<"getUpdates"> = {},
92 | options: PollingStartOptions = {},
93 | ) {
94 | if (options.dropPendingUpdates)
95 | await this.dropPendingUpdates(options.deleteWebhookOnConflict);
96 |
97 | while (this.isStarted) {
98 | try {
99 | this.isRequestActive = true;
100 | const updates = await withRetries(() =>
101 | this.bot.api.getUpdates({
102 | timeout: 30,
103 | ...params,
104 | offset: this.offset,
105 | }),
106 | );
107 | this.isRequestActive = false;
108 |
109 | const updateId = updates.at(-1)?.update_id;
110 | this.offset = updateId ? updateId + 1 : this.offset;
111 |
112 | for await (const update of updates) {
113 | this.queue.add(update);
114 | // await this.handleUpdate(update).catch(console.error);
115 | }
116 | } catch (error) {
117 | if (error instanceof TelegramError) {
118 | if (error.code === 409 && error.message.includes("deleteWebhook")) {
119 | if (options.deleteWebhookOnConflict)
120 | await this.bot.api.deleteWebhook();
121 | continue;
122 | }
123 | }
124 |
125 | // TODO: possible to remove logs
126 | console.error("Error received when fetching updates", error);
127 | await sleep(this.bot.options.api.retryGetUpdatesWait ?? 1000);
128 | }
129 | }
130 |
131 | this.stopPollingPromiseResolve?.();
132 | }
133 |
134 | async dropPendingUpdates(deleteWebhookOnConflict = false): Promise {
135 | const result = await this.bot.api.getUpdates({
136 | // The negative offset can be specified to retrieve updates starting from *-offset* update from the end of the updates queue.
137 | // All previous updates will be forgotten.
138 | offset: -1,
139 | timeout: 0,
140 | suppress: true,
141 | });
142 |
143 | if (result instanceof TelegramError) {
144 | if (result.code === 409 && result.message.includes("deleteWebhook")) {
145 | if (deleteWebhookOnConflict) {
146 | await this.bot.api.deleteWebhook({
147 | drop_pending_updates: true,
148 | });
149 |
150 | return;
151 | }
152 | }
153 |
154 | throw result;
155 | }
156 |
157 | const lastUpdateId = result.at(-1)?.update_id;
158 |
159 | debug$updates(
160 | "Dropping pending updates... Set offset to last update id %s + 1",
161 | lastUpdateId,
162 | );
163 |
164 | this.offset = lastUpdateId ? lastUpdateId + 1 : this.offset;
165 | }
166 |
167 | /**
168 | * ! Soon waitPendingRequests param default will changed to true
169 | */
170 | stopPolling(waitPendingRequests = false): Promise {
171 | this.isStarted = false;
172 |
173 | if (!this.isRequestActive || !waitPendingRequests) return Promise.resolve();
174 |
175 | return new Promise((resolve) => {
176 | this.stopPollingPromiseResolve = resolve;
177 | });
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/utils.internal.ts:
--------------------------------------------------------------------------------
1 | import debug from "debug";
2 |
3 | // cant use node:timers/promises because possible browser usage...
4 | export const sleep = (ms: number) =>
5 | new Promise((resolve) => setTimeout(resolve, ms));
6 |
7 | function convertToString(value: unknown): string {
8 | const typeOfValue = typeof value;
9 |
10 | // wtf
11 | if (typeOfValue === "string") return value as string;
12 | if (typeOfValue === "object") return JSON.stringify(value);
13 | return String(value);
14 | }
15 |
16 | export function simplifyObject(obj: Record) {
17 | const result: Record = {};
18 |
19 | for (const [key, value] of Object.entries(obj)) {
20 | const typeOfValue = typeof value;
21 |
22 | if (value === undefined || value === null || typeOfValue === "function")
23 | continue;
24 |
25 | result[key] = convertToString(value);
26 | }
27 |
28 | return result;
29 | }
30 |
31 | export const IS_BUN = typeof Bun !== "undefined";
32 |
33 | export const debug$api = debug("gramio:api");
34 | export const debug$updates = debug("gramio:updates");
35 |
36 | export type MaybeArray = T | T[] | ReadonlyArray;
37 |
38 | export function timeoutWebhook(
39 | task: Promise,
40 | timeout: number,
41 | mode: "throw" | "return",
42 | ) {
43 | return new Promise((resolve, reject) => {
44 | const timeoutTask = setTimeout(() => {
45 | if (mode === "throw") {
46 | reject(
47 | new Error(`Webhook handler execution timed out after ${timeout}ms`),
48 | );
49 | } else {
50 | resolve(undefined);
51 | }
52 | }, timeout);
53 |
54 | task
55 | .then(resolve)
56 | .catch(reject)
57 | .finally(() => clearTimeout(timeoutTask));
58 | });
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module
3 | *
4 | * Pack of useful utilities for Telegram Bot API and GramIO
5 | */
6 |
7 | import { TelegramError } from "./errors.ts";
8 | import { sleep } from "./utils.internal.ts";
9 |
10 | async function suppressError(
11 | fn: () => Promise,
12 | ): Promise<[T | unknown, boolean]> {
13 | try {
14 | return [await fn(), false];
15 | } catch (error) {
16 | return [error, true];
17 | }
18 | }
19 |
20 | export async function withRetries(
21 | resultPromise: () => Promise,
22 | ): Promise {
23 | let [result, isFromCatch] = await suppressError(resultPromise);
24 |
25 | while (result instanceof TelegramError) {
26 | const retryAfter = result.payload?.retry_after;
27 |
28 | if (retryAfter) {
29 | await sleep(retryAfter * 1000);
30 | [result, isFromCatch] = await suppressError(resultPromise);
31 | } else {
32 | if (isFromCatch) throw result;
33 |
34 | // TODO: hard conditional typings. fix later
35 | // @ts-expect-error
36 | return result;
37 | }
38 | }
39 |
40 | if (result instanceof Error && isFromCatch) throw result;
41 |
42 | // @ts-expect-error
43 | return result;
44 | }
45 |
--------------------------------------------------------------------------------
/src/webhook/adapters.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer";
2 | import type { TelegramUpdate } from "@gramio/types";
3 | import type { MaybePromise } from "../types.js";
4 |
5 | const SECRET_TOKEN_HEADER = "X-Telegram-Bot-Api-Secret-Token";
6 | const WRONG_TOKEN_ERROR = "secret token is invalid";
7 | const RESPONSE_OK = "ok!";
8 |
9 | export interface FrameworkHandler {
10 | update: MaybePromise;
11 | header: string;
12 | unauthorized: () => unknown;
13 | response?: () => unknown;
14 | }
15 | export type FrameworkAdapter = (...args: any[]) => FrameworkHandler;
16 |
17 | // @ts-ignore
18 | const responseOK = () => new Response(RESPONSE_OK) as Response;
19 |
20 | const responseUnauthorized = () =>
21 | new Response(WRONG_TOKEN_ERROR, {
22 | status: 401,
23 | // @ts-ignore
24 | }) as Response;
25 |
26 | export const frameworks = {
27 | elysia: ({ body, headers }) => ({
28 | update: body,
29 | header: headers[SECRET_TOKEN_HEADER],
30 | unauthorized: responseUnauthorized,
31 | response: responseOK,
32 | }),
33 | fastify: (request, reply) => ({
34 | update: request.body,
35 | header: request.headers[SECRET_TOKEN_HEADER],
36 | unauthorized: () => reply.code(401).send(WRONG_TOKEN_ERROR),
37 | response: () => reply.send(RESPONSE_OK),
38 | }),
39 | hono: (c) => ({
40 | update: c.req.json(),
41 | header: c.req.header(SECRET_TOKEN_HEADER),
42 | unauthorized: () => c.text(WRONG_TOKEN_ERROR, 401),
43 | response: responseOK,
44 | }),
45 | express: (req, res) => ({
46 | update: req.body,
47 | header: req.header(SECRET_TOKEN_HEADER),
48 | unauthorized: () => res.status(401).send(WRONG_TOKEN_ERROR),
49 | response: () => res.send(RESPONSE_OK),
50 | }),
51 | koa: (ctx) => ({
52 | update: ctx.request.body,
53 | header: ctx.get(SECRET_TOKEN_HEADER),
54 | unauthorized: () => {
55 | ctx.status === 401;
56 | ctx.body = WRONG_TOKEN_ERROR;
57 | },
58 | response: () => {
59 | ctx.status = 200;
60 | ctx.body = RESPONSE_OK;
61 | },
62 | }),
63 | http: (req, res) => ({
64 | update: new Promise((resolve) => {
65 | let body = "";
66 |
67 | req.on("data", (chunk: Buffer) => {
68 | body += chunk.toString();
69 | });
70 |
71 | req.on("end", () => resolve(JSON.parse(body)));
72 | }),
73 | header: req.headers[SECRET_TOKEN_HEADER.toLowerCase()],
74 | unauthorized: () => res.writeHead(401).end(WRONG_TOKEN_ERROR),
75 | response: () => res.writeHead(200).end(RESPONSE_OK),
76 | }),
77 | "std/http": (req) => ({
78 | update: req.json(),
79 | header: req.headers.get(SECRET_TOKEN_HEADER),
80 | response: responseOK,
81 | unauthorized: responseUnauthorized,
82 | }),
83 | "Bun.serve": (req) => ({
84 | update: req.json(),
85 | header: req.headers.get(SECRET_TOKEN_HEADER),
86 | response: responseOK,
87 | unauthorized: responseUnauthorized,
88 | }),
89 | cloudflare: (req) => ({
90 | update: req.json(),
91 | header: req.headers.get(SECRET_TOKEN_HEADER),
92 | response: responseOK,
93 | unauthorized: responseUnauthorized,
94 | }),
95 | Request: (req) => ({
96 | update: req.json(),
97 | header: req.headers.get(SECRET_TOKEN_HEADER),
98 | response: responseOK,
99 | unauthorized: responseUnauthorized,
100 | }),
101 | } satisfies Record;
102 |
--------------------------------------------------------------------------------
/src/webhook/index.ts:
--------------------------------------------------------------------------------
1 | import type { Bot } from "../bot.js";
2 | import { timeoutWebhook } from "../utils.internal.js";
3 | import { type FrameworkAdapter, frameworks } from "./adapters.js";
4 |
5 | /** Union type of webhook handlers name */
6 | export type WebhookHandlers = keyof typeof frameworks;
7 |
8 | export interface WebhookHandlerOptionsShouldWait {
9 | /** Action to take when timeout occurs. @default "throw" */
10 | onTimeout?: "throw" | "return";
11 | /** Timeout in milliseconds. @default 10_000 */
12 | timeout?: number;
13 | }
14 |
15 | export interface WebhookHandlerOptions {
16 | secretToken?: string;
17 |
18 | shouldWait?: boolean | WebhookHandlerOptionsShouldWait;
19 | }
20 |
21 | /**
22 | * Setup handler with yours web-framework to receive updates via webhook
23 | *
24 | * @example
25 | * ```ts
26 | * import { Bot } from "gramio";
27 | * import Fastify from "fastify";
28 | *
29 | * const bot = new Bot(process.env.TOKEN as string).on(
30 | * "message",
31 | * (context) => {
32 | * return context.send("Fastify!");
33 | * },
34 | * );
35 | *
36 | * const fastify = Fastify();
37 | *
38 | * fastify.post("/telegram-webhook", webhookHandler(bot, "fastify"));
39 | *
40 | * fastify.listen({ port: 3445, host: "::" });
41 | *
42 | * bot.start({
43 | * webhook: {
44 | * url: "https://example.com:3445/telegram-webhook",
45 | * },
46 | * });
47 | * ```
48 | */
49 | export function webhookHandler(
50 | bot: Bot,
51 | framework: Framework,
52 | secretTokenOrOptions?: string | WebhookHandlerOptions,
53 | ) {
54 | const frameworkAdapter = frameworks[framework];
55 | const secretToken =
56 | typeof secretTokenOrOptions === "string"
57 | ? secretTokenOrOptions
58 | : secretTokenOrOptions?.secretToken;
59 | const shouldWaitOptions =
60 | typeof secretTokenOrOptions === "string"
61 | ? false
62 | : secretTokenOrOptions?.shouldWait;
63 | const isShouldWait =
64 | shouldWaitOptions &&
65 | (typeof shouldWaitOptions === "object" ||
66 | typeof shouldWaitOptions === "boolean");
67 |
68 | return (async (...args: any[]) => {
69 | const { update, response, header, unauthorized } = frameworkAdapter(
70 | // @ts-expect-error
71 | ...args,
72 | );
73 |
74 | if (secretToken && header !== secretToken) return unauthorized();
75 |
76 | if (!isShouldWait) {
77 | // await bot.updates.handleUpdate(await update);
78 | // TODO: more think about queue based or wait in handler update
79 | bot.updates.queue.add(await update);
80 |
81 | if (response) return response();
82 | } else {
83 | const timeoutOptions =
84 | typeof shouldWaitOptions === "object" ? shouldWaitOptions : undefined;
85 | const timeout = timeoutOptions?.timeout ?? 30000;
86 | const onTimeout = timeoutOptions?.onTimeout ?? "throw";
87 |
88 | await timeoutWebhook(
89 | bot.updates.handleUpdate(await update, "wait"),
90 | timeout,
91 | onTimeout,
92 | );
93 |
94 | if (response) return response();
95 | }
96 | }) as unknown as ReturnType<(typeof frameworks)[Framework]> extends {
97 | response: () => any;
98 | }
99 | ? (
100 | ...args: Parameters<(typeof frameworks)[Framework]>
101 | ) => ReturnType["response"]>
102 | : (...args: Parameters<(typeof frameworks)[Framework]>) => void;
103 | }
104 |
--------------------------------------------------------------------------------
/tests/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { BotLike, ContextType, UpdateName, User } from "@gramio/contexts";
2 | import { expectTypeOf } from "expect-type";
3 | import { Bot } from "../../src/bot.ts";
4 | import { Plugin } from "../../src/plugin.ts";
5 |
6 | {
7 | const plugin = new Plugin("test");
8 |
9 | expectTypeOf().toBeObject();
10 |
11 | plugin
12 | .derive("message", (context) => {
13 | expectTypeOf().toEqualTypeOf<
14 | ContextType
15 | >();
16 | expectTypeOf().toEqualTypeOf();
17 |
18 | return {
19 | someThing: "plugin1" as const,
20 | };
21 | })
22 | .derive("message", (context) => {
23 | expectTypeOf().toBeObject();
24 | expectTypeOf().toEqualTypeOf();
25 |
26 | expectTypeOf().toEqualTypeOf<"plugin1">();
27 |
28 | return {
29 | someThing: "plugin2" as const,
30 | };
31 | });
32 | }
33 |
34 | {
35 | const events = ["message", "callback_query", "chat_member"] as const;
36 |
37 | const bot = new Bot("test")
38 | .derive(events, (context) => {
39 | expectTypeOf().toBeObject();
40 | expectTypeOf().toEqualTypeOf();
41 |
42 | return {
43 | contextUser: context.from,
44 | };
45 | })
46 | .on(events, (context) => {
47 | expectTypeOf().toBeObject();
48 | expectTypeOf