├── .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 | [![Bot API](https://img.shields.io/badge/Bot%20API-9.0-blue?logo=telegram&style=flat&labelColor=000&color=3b82f6)](https://core.telegram.org/bots/api) 6 | [![npm](https://img.shields.io/npm/v/gramio?logo=npm&style=flat&labelColor=000&color=3b82f6)](https://www.npmjs.org/package/gramio) 7 | [![npm downloads](https://img.shields.io/npm/dw/gramio?logo=npm&style=flat&labelColor=000&color=3b82f6)](https://www.npmjs.org/package/gramio) 8 | [![JSR](https://jsr.io/badges/@gramio/core)](https://jsr.io/@gramio/core) 9 | [![JSR Score](https://jsr.io/badges/@gramio/core/score)](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().toEqualTypeOf(); 49 | 50 | expectTypeOf().toEqualTypeOf(); 51 | }); 52 | 53 | const plugin = new Plugin("test") 54 | .derive(events, (context) => { 55 | expectTypeOf().toBeObject(); 56 | expectTypeOf().toEqualTypeOf(); 57 | 58 | return { 59 | contextUser: context.from, 60 | }; 61 | }) 62 | .on(events, (context) => { 63 | expectTypeOf().toBeObject(); 64 | expectTypeOf().toEqualTypeOf(); 65 | 66 | expectTypeOf().toEqualTypeOf(); 67 | }); 68 | 69 | bot.extend(plugin); 70 | } 71 | -------------------------------------------------------------------------------- /tests/types/triggers.ts: -------------------------------------------------------------------------------- 1 | import { CallbackData } from "@gramio/callback-data"; 2 | import { expectTypeOf } from "expect-type"; 3 | import { Bot } from "../../src/bot.ts"; 4 | 5 | { 6 | const bot = new Bot("test") 7 | .command(["a", "b"], (context) => { 8 | expectTypeOf().toBeObject(); 9 | }) 10 | .command("a", (context) => { 11 | expectTypeOf().toBeObject(); 12 | }) 13 | .callbackQuery("a", (context) => { 14 | expectTypeOf().toBeObject(); 15 | }) 16 | .callbackQuery(new CallbackData("test").number("test"), (context) => { 17 | expectTypeOf().toBeObject(); 18 | expectTypeOf().toEqualTypeOf<{ 19 | test: number; 20 | }>(); 21 | }) 22 | .hears("a", (context) => { 23 | expectTypeOf().toBeObject(); 24 | }) 25 | .hears(/test (.*)/, (context) => { 26 | expectTypeOf().toBeObject(); 27 | expectTypeOf< 28 | typeof context.args 29 | >().toEqualTypeOf(); 30 | }) 31 | .inlineQuery( 32 | "a", 33 | (context) => { 34 | expectTypeOf().toBeObject(); 35 | }, 36 | { 37 | onResult: (context) => { 38 | expectTypeOf().toBeObject(); 39 | }, 40 | }, 41 | ) 42 | .chosenInlineResult("a", (context) => { 43 | expectTypeOf().toBeObject(); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /tests/updates.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, jest } from "bun:test"; 2 | import { Updates } from "../src/updates.js"; 3 | 4 | class MockBot { 5 | api = { 6 | getUpdates: jest.fn(), 7 | }; 8 | options = { api: {} }; 9 | } 10 | 11 | describe("Updates.stopPolling", () => { 12 | let updates: Updates; 13 | let bot: MockBot; 14 | 15 | beforeEach(() => { 16 | bot = new MockBot(); 17 | updates = new Updates(bot as any, jest.fn()); 18 | }); 19 | 20 | it("resolves immediately if no request is active", async () => { 21 | updates.isRequestActive = false; 22 | const promise = updates.stopPolling(); 23 | 24 | expect(promise).resolves.toBeUndefined(); 25 | }); 26 | 27 | it("resolves after request completes if request is active", async () => { 28 | updates.isRequestActive = true; 29 | let resolved = false; 30 | const promise = updates.stopPolling().then(() => { 31 | resolved = true; 32 | }); 33 | setTimeout(() => { 34 | updates.isRequestActive = false; 35 | 36 | updates.stopPollingPromiseResolve?.(); 37 | }, 50); 38 | await promise; 39 | 40 | expect(resolved).toBe(true); 41 | }); 42 | 43 | it("sets isStarted to false", () => { 44 | updates.isStarted = true; 45 | updates.stopPolling(); 46 | expect(updates.isStarted).toBe(false); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, mock, test } from "bun:test"; 2 | import { TelegramError } from "../src/errors.ts"; 3 | import { withRetries } from "../src/utils.ts"; 4 | 5 | describe("utils", () => { 6 | describe("withRetries", () => { 7 | let isErrorReturned = false; 8 | 9 | const mockApi = mock(async (success = true, suppress = false) => { 10 | if (!success && !isErrorReturned) { 11 | const error = new TelegramError( 12 | { 13 | ok: false, 14 | description: "test", 15 | error_code: 429, 16 | parameters: { retry_after: 0.01 }, 17 | }, 18 | "sendMessage", 19 | { 20 | chat_id: 1, 21 | text: "test", 22 | }, 23 | ); 24 | 25 | isErrorReturned = true; 26 | 27 | if (suppress) return error; 28 | 29 | throw error; 30 | } 31 | 32 | return "success"; 33 | }); 34 | 35 | beforeEach(() => { 36 | mockApi.mockClear(); 37 | isErrorReturned = false; 38 | }); 39 | 40 | test("Should return success", async () => { 41 | const result = await withRetries(() => mockApi(true)); 42 | 43 | expect(result).toBe("success"); 44 | }); 45 | 46 | test("Should retry on error", async () => { 47 | const result = await withRetries(() => mockApi(false)); 48 | 49 | expect(result).toBe("success"); 50 | 51 | expect(mockApi).toHaveBeenCalledTimes(2); 52 | }); 53 | 54 | test("Should suppress errors", async () => { 55 | const result = await withRetries(() => mockApi(false, true)); 56 | 57 | expect(result).toBe("success"); 58 | }); 59 | 60 | test("Should consider retry_after", async () => { 61 | const error = new TelegramError( 62 | { 63 | ok: false, 64 | description: "test", 65 | error_code: 429, 66 | parameters: { retry_after: 0.01 }, 67 | }, 68 | "sendMessage", 69 | { 70 | chat_id: 1, 71 | text: "test", 72 | }, 73 | ); 74 | let isErrorReturned = false; 75 | 76 | const failingApi = mock(async () => { 77 | if (!isErrorReturned) { 78 | isErrorReturned = true; 79 | 80 | throw error; 81 | } 82 | 83 | return "success"; 84 | }); 85 | 86 | const start = Date.now(); 87 | await withRetries(() => failingApi()); 88 | const duration = Date.now() - start; 89 | 90 | expect(duration).toBeGreaterThanOrEqual(8); 91 | expect(failingApi).toHaveBeenCalledTimes(2); 92 | }); 93 | 94 | test("Should rethrow non-Telegram errors", async () => { 95 | const error = new Error("Critical error"); 96 | const failingApi = mock(async () => { 97 | throw error; 98 | 99 | // biome-ignore lint/correctness/noUnreachable: 100 | return "success"; 101 | }); 102 | 103 | // TODO: https://github.com/oven-sh/bun/issues/16144 104 | expect( 105 | (async () => { 106 | const result = await withRetries(() => failingApi()); 107 | 108 | console.log("result", result); 109 | })(), 110 | ).rejects.toThrow(error); 111 | 112 | expect(failingApi).toHaveBeenCalledTimes(1); 113 | }); 114 | 115 | test("Should resolves when TelegramError comes from then()", async () => { 116 | const error = new TelegramError( 117 | { 118 | ok: false, 119 | description: "test", 120 | error_code: 400, 121 | parameters: {}, 122 | }, 123 | "sendMessage", 124 | { 125 | chat_id: 1, 126 | text: "test", 127 | }, 128 | ); 129 | 130 | const api = mock(async () => error); 131 | 132 | expect(withRetries(api)).resolves.toThrow(error); 133 | expect(api).toHaveBeenCalledTimes(1); 134 | }); 135 | 136 | test("Should resolves when Error comes from then()", async () => { 137 | const error = new Error("Critical error"); 138 | const api = mock(async () => error); 139 | 140 | expect(withRetries(api)).resolves.toThrow(error); 141 | expect(api).toHaveBeenCalledTimes(1); 142 | }); 143 | 144 | test("Should reject when TelegramError comes from catch()", async () => { 145 | const error = new TelegramError( 146 | { 147 | ok: false, 148 | description: "test", 149 | error_code: 400, 150 | parameters: {}, 151 | }, 152 | "sendMessage", 153 | { 154 | chat_id: 1, 155 | text: "test", 156 | }, 157 | ); 158 | 159 | const api = mock(async () => { 160 | throw error; 161 | }); 162 | 163 | expect(withRetries(api)).rejects.toThrow(error); 164 | expect(api).toHaveBeenCalledTimes(1); 165 | }); 166 | 167 | test("Should reject when Error comes from catch()", async () => { 168 | const error = new Error("Critical error"); 169 | const api = mock(async () => { 170 | throw error; 171 | }); 172 | 173 | expect(withRetries(api)).rejects.toThrow(error); 174 | expect(api).toHaveBeenCalledTimes(1); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "NodeNext", 5 | "target": "ESNext", 6 | "moduleResolution": "NodeNext", 7 | "esModuleInterop": true, 8 | /* Linting */ 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "forceConsistentCasingInFileNames": true, 13 | 14 | "composite": true, 15 | "allowImportingTsExtensions": true, 16 | "noEmit": true 17 | }, 18 | "include": ["./src", "./tests"] 19 | } 20 | --------------------------------------------------------------------------------