tgplus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +52 -0
  2. package/README.md +1512 -0
  3. package/dist/adapters.d.ts +46 -0
  4. package/dist/adapters.js +83 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/bot.d.ts +50 -0
  7. package/dist/bot.js +180 -0
  8. package/dist/bot.js.map +1 -0
  9. package/dist/client.d.ts +1139 -0
  10. package/dist/client.js +529 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/composer.d.ts +88 -0
  13. package/dist/composer.js +233 -0
  14. package/dist/composer.js.map +1 -0
  15. package/dist/context.d.ts +72 -0
  16. package/dist/context.js +149 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/decorators.d.ts +33 -0
  19. package/dist/decorators.js +39 -0
  20. package/dist/decorators.js.map +1 -0
  21. package/dist/filters.d.ts +19 -0
  22. package/dist/filters.js +81 -0
  23. package/dist/filters.js.map +1 -0
  24. package/dist/i18n.d.ts +30 -0
  25. package/dist/i18n.js +41 -0
  26. package/dist/i18n.js.map +1 -0
  27. package/dist/index.d.ts +16 -0
  28. package/dist/index.js +95 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/keyboard.d.ts +62 -0
  31. package/dist/keyboard.js +96 -0
  32. package/dist/keyboard.js.map +1 -0
  33. package/dist/rateLimit.d.ts +19 -0
  34. package/dist/rateLimit.js +32 -0
  35. package/dist/rateLimit.js.map +1 -0
  36. package/dist/richMessage.d.ts +111 -0
  37. package/dist/richMessage.js +221 -0
  38. package/dist/richMessage.js.map +1 -0
  39. package/dist/scenes.d.ts +57 -0
  40. package/dist/scenes.js +105 -0
  41. package/dist/scenes.js.map +1 -0
  42. package/dist/session-redis.d.ts +27 -0
  43. package/dist/session-redis.js +36 -0
  44. package/dist/session-redis.js.map +1 -0
  45. package/dist/session-sqlite.d.ts +29 -0
  46. package/dist/session-sqlite.js +34 -0
  47. package/dist/session-sqlite.js.map +1 -0
  48. package/dist/session.d.ts +30 -0
  49. package/dist/session.js +35 -0
  50. package/dist/session.js.map +1 -0
  51. package/dist/types.d.ts +2485 -0
  52. package/dist/types.js +11 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,1512 @@
1
+ <div align="center">
2
+
3
+ # TG plus ✨
4
+
5
+ **A friendlier, fully-typed Telegram Bot API library for Node.js**
6
+
7
+ Works equally well from plain JavaScript or TypeScript. Covers **Bot API 7.0 through 10.3** — 187 methods across 21 categories.
8
+
9
+ <p>
10
+ <a href="https://www.npmjs.com/package/tgplus"><img src="https://img.shields.io/npm/v/tgplus?style=flat-square&color=blue" alt="npm version"></a>
11
+ <a href="https://www.npmjs.com/package/tgplus"><img src="https://img.shields.io/npm/dm/tgplus?style=flat-square&color=green" alt="npm downloads"></a>
12
+ <a href="https://github.com/crysnovax/TGplus/actions"><img src="https://img.shields.io/github/actions/workflow/status/crysnovax/TGplus/test.yml?style=flat-square" alt="CI"></a>
13
+ <img src="https://img.shields.io/badge/zero%20runtime%20deps-✓-brightgreen?style=flat-square" alt="Zero runtime dependencies">
14
+ <img src="https://img.shields.io/badge/Node.js-%3E%3D18-blue?style=flat-square&logo=node.js" alt="Node.js">
15
+ </p>
16
+
17
+ <img src="https://cdn.crysnova.qzz.io/files/1789261660580-631a0aeb-b041-466d-ac89-400b968b1920.jpeg" alt="tgplus" width="720" />
18
+
19
+ [Quick Start](#quick-start) · [Features With Examples](#features-with-examples) · [Every Class](#every-class-with-an-example) · [Full API Surface](#the-complete-api-surface) · [Testing](#testing) · [Status](#status)
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ ## Table of contents
26
+
27
+ - [Installation](#installation)
28
+ - [Quick Start](#quick-start)
29
+ - [Features With Examples](#features-with-examples)
30
+ 1. [Router handlers](#1-router-handlers)
31
+ 2. [Keyboard builders](#2-keyboard-builders)
32
+ 3. [Editing messages from button taps](#3-editing-messages-from-button-taps)
33
+ 4. [Rich Messages](#4-rich-messages-bot-api-101103)
34
+ 5. [Inline streaming & AI drafts](#5-inline-streaming--ai-drafts)
35
+ 6. [Decorator classes](#6-decorator-classes)
36
+ 7. [Scenes (multi-step conversations)](#7-scenes-multi-step-conversations)
37
+ 8. [Sessions](#8-sessions)
38
+ 9. [Rate limiting](#9-rate-limiting)
39
+ 10. [Flood control & throttling](#10-flood-control--throttling)
40
+ 11. [i18n](#11-i18n)
41
+ 12. [Entity & reaction filters](#12-entity--reaction-filters)
42
+ 13. [File uploads & downloads](#13-file-uploads--downloads)
43
+ 14. [Composer gates, combinators & Router](#14-composer-gates-combinators--router)
44
+ 15. [Polling, webhooks & serverless](#15-polling-webhooks--serverless)
45
+ 16. [Context helpers](#16-context-helpers)
46
+ 17. [Typed API & escape hatch](#17-typed-api--escape-hatch)
47
+ 18. [Error handling](#18-error-handling)
48
+ - [Every Class, With An Example](#every-class-with-an-example)
49
+ - [The Complete API Surface](#the-complete-api-surface)
50
+ - [Coverage Confidence](#coverage-confidence)
51
+ - [Project Layout](#project-layout)
52
+ - [Testing](#testing)
53
+ - [Status](#status)
54
+ - [Contributing](#contributing)
55
+ - [Credits](#credits)
56
+ - [License](#license)
57
+
58
+ ---
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ npm install tgplus
64
+ ```
65
+
66
+ Requires Node.js 18+ (uses the native `fetch`/`FormData`/`Blob` globals — no runtime dependencies to install).
67
+
68
+ Optional peer dependencies, only if you use them:
69
+
70
+ ```bash
71
+ npm install ioredis # for RedisSessionStore
72
+ npm install better-sqlite3 # for SqliteSessionStore
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Quick Start
78
+
79
+ ```typescript
80
+ import { Bot } from "tgplus";
81
+
82
+ const bot = new Bot(process.env.BOT_TOKEN!);
83
+
84
+ bot.command("start", (ctx) => ctx.reply(`Hey ${ctx.from?.first_name}!`));
85
+ bot.hears(/^ping$/i, (ctx) => ctx.reply("pong"));
86
+ bot.on("photo", (ctx) => ctx.reply("Nice photo!"));
87
+
88
+ bot.launch(); // long polling
89
+ ```
90
+
91
+ Everything below is a worked example of one part of the package.
92
+
93
+ ---
94
+
95
+ ## Features With Examples
96
+
97
+ ### 1. Router handlers
98
+
99
+ `command` / `hears` / `on` / `action`. Regex captures land in `ctx.match`; `command()` accepts aliases and `/cmd@BotName`.
100
+
101
+ ```typescript
102
+ // Multiple aliases for one handler — "/help" and "/h" both match
103
+ bot.command(["help", "h"], (ctx) => ctx.reply("Here's what I can do."));
104
+
105
+ // Arguments after the command: "/ban 42 spam" -> ctx.match[1] === "42 spam"
106
+ bot.command("ban", (ctx) => {
107
+ const args = ctx.match![1] ?? "";
108
+ ctx.reply(`Banning ${args || "…"} (not really).`);
109
+ });
110
+
111
+ // /start@MyBotName also matches (deep links, group usage)
112
+ bot.command("start", (ctx) => ctx.reply("Started."));
113
+
114
+ // Exact string or RegExp on message text
115
+ bot.hears("hello", (ctx) => ctx.reply("Hi!"));
116
+ bot.hears(/^good (morning|evening)$/i, (ctx) => ctx.reply(`Good ${ctx.match![1].toLowerCase()}!`));
117
+
118
+ // Update types
119
+ bot.on("message", (ctx) => ctx.reply("A message arrived."));
120
+
121
+ // Message sub-types
122
+ bot.on("text", (ctx) => ctx.reply(`You said: ${ctx.text}`));
123
+ bot.on("photo", (ctx) => ctx.reply("Nice photo!"));
124
+ bot.on("sticker", (ctx) => ctx.reply("Nice sticker."));
125
+ bot.on("location", (ctx) => ctx.reply("Got a location."));
126
+ bot.on("contact", (ctx) => ctx.reply("Got a contact."));
127
+ bot.on("poll", (ctx) => ctx.reply("A poll."));
128
+ bot.on("dice", (ctx) => ctx.reply("A dice."));
129
+ bot.on("video_note", (ctx) => ctx.reply("Round video."));
130
+ bot.on("rich_message", (ctx) => ctx.reply("A rich message."));
131
+
132
+ // Non-message update types
133
+ bot.on("callback_query", (ctx) => ctx.answerCbQuery());
134
+ bot.on("inline_query", (ctx) => ctx.answerInlineQuery([]));
135
+ bot.on("chat_join_request", (ctx) => ctx.api.approveChatJoinRequest({ chat_id: ctx.chatId!, user_id: ctx.from!.id }));
136
+ bot.on("poll_answer", (ctx) => console.log("Poll answered by", ctx.from?.id));
137
+
138
+ // Callback button data — exact or RegExp with captures
139
+ bot.action("dice", (ctx) => ctx.api.sendDice({ chat_id: ctx.chatId! }));
140
+ bot.action(/^server:(.+)$/, (ctx) => ctx.reply(`Opening server ${ctx.match![1]}…`));
141
+
142
+ // A custom predicate instead of a name
143
+ bot.on((ctx) => ctx.text?.includes("urgent") === true, (ctx) => ctx.reply("Marked urgent."));
144
+
145
+ // Catch-all — put it last
146
+ bot.otherwise((ctx) => ctx.reply("I didn't understand that."));
147
+ ```
148
+
149
+ ### 2. Keyboard builders
150
+
151
+ `Keyboard.inline()`, `Keyboard.reply()`, `Keyboard.remove()`, `Keyboard.forceReply()`.
152
+
153
+ ```typescript
154
+ import { Keyboard } from "tgplus";
155
+
156
+ // ---- Inline keyboard (buttons attached to a message) ----
157
+ bot.command("menu", (ctx) =>
158
+ ctx.reply("Pick one:", {
159
+ reply_markup: Keyboard.inline()
160
+ .button("Confirm", "confirm:yes", { style: "success" }) // callback button + color
161
+ .button("Cancel", "confirm:no", { style: "danger" })
162
+ .button("Danger!", "d", { icon_custom_emoji_id: "5368324170671202286" })
163
+ .row() // start a new row
164
+ .url("Docs", "https://example.com") // URL button
165
+ .urlButton("Join channel", "https://t.me/example") // alias of .url()
166
+ .row()
167
+ .webApp("Open Mini App", "https://your.app") // Web App button
168
+ .row()
169
+ .switchInline("Share this bot", "query") // switch to inline mode
170
+ .row()
171
+ .pay("Buy now") // payment button
172
+ .row()
173
+ .copyText("Copy code", "AB12-CD34") // Bot API 7.11+ clipboard copy
174
+ .build(),
175
+ })
176
+ );
177
+
178
+ // ---- Reply keyboard (the persistent bottom keyboard) ----
179
+ bot.command("settings", (ctx) =>
180
+ ctx.reply("Settings:", {
181
+ reply_markup: Keyboard.reply()
182
+ .text("Notifications")
183
+ .text("Language", { style: "primary" })
184
+ .row()
185
+ .requestContact("Share my contact") // request_contact: true
186
+ .requestLocation("Share my location") // request_location: true
187
+ .build({ resize: true, oneTime: true, placeholder: "Choose one" }),
188
+ })
189
+ );
190
+
191
+ // ---- Remove a reply keyboard ----
192
+ bot.command("done", (ctx) =>
193
+ ctx.reply("Keyboard hidden.", { reply_markup: Keyboard.remove() }) // or Keyboard.remove(true) for selective
194
+ );
195
+
196
+ // ---- Force the user to type a reply ----
197
+ bot.command("age", (ctx) =>
198
+ ctx.reply("How old are you?", { reply_markup: Keyboard.forceReply("Type a number") })
199
+ );
200
+ ```
201
+
202
+ > `ReplyKeyboardBuilder` supports `.text()`, `.requestContact()`, `.requestLocation()`, and `.row()`. Inline supports `.button()`, `.url()`, `.urlButton()`, `.webApp()`, `.switchInline()`, `.pay()`, `.copyText()`, `.row()`.
203
+
204
+ ### 3. Editing messages from button taps
205
+
206
+ `editText` / `editCaption` / `editMedia` / `editReplyMarkup` / `deleteMessage` all resolve the message id from `callback_query.message` automatically — the single most common Telegram UI pattern.
207
+
208
+ ```typescript
209
+ bot.command("confirm", (ctx) =>
210
+ ctx.reply("Proceed?", {
211
+ reply_markup: Keyboard.inline().button("Yes", "confirm:yes").button("No", "confirm:no").build(),
212
+ })
213
+ );
214
+
215
+ bot.action(/^confirm:(yes|no)$/, async (ctx) => {
216
+ await ctx.answerCbQuery(); // stop the button spinner
217
+ await ctx.editText(ctx.match![1] === "yes" ? "Confirmed ✅" : "Cancelled ❌");
218
+ // ctx.editCaption(), ctx.editMedia(), ctx.editReplyMarkup() work the same way
219
+ });
220
+
221
+ // Toggle just the keyboard in place, without resending the message
222
+ bot.action("toggle:notifs", async (ctx) => {
223
+ await ctx.answerCbQuery("Toggled");
224
+ await ctx.editReplyMarkup(
225
+ Keyboard.inline().button("Toggle again", "toggle:notifs").build()
226
+ );
227
+ });
228
+
229
+ // Replace the media under a caption
230
+ bot.action("swap:image", async (ctx) => {
231
+ await ctx.answerCbQuery();
232
+ await ctx.editMedia({ type: "photo", media: { url: "https://picsum.photos/600" } });
233
+ });
234
+
235
+ // Delete the message the button was attached to
236
+ bot.action("done", async (ctx) => {
237
+ await ctx.answerCbQuery();
238
+ await ctx.deleteMessage();
239
+ });
240
+ ```
241
+
242
+ ### 4. Rich Messages (Bot API 10.1–10.3)
243
+
244
+ Three builders. `markdown()` / `html()` write formatted text; `blocks()` builds the structured block tree — media blocks reference separately-declared media by id, and the `.photo()`/`.video()`/`.document()` helpers handle that bookkeeping (and throw locally if you reference an undeclared id).
245
+
246
+ ```typescript
247
+ import { RichMessage } from "tgplus";
248
+
249
+ // ---------- Rich Markdown ----------
250
+ bot.command("report", (ctx) =>
251
+ ctx.replyRich(
252
+ RichMessage.markdown()
253
+ .heading("Build report", 2)
254
+ .text("Here's today's run:")
255
+ .table(["Step", "Status"], [["Install", "✅"], ["Test", "✅"], ["Deploy", "⏳"]]) // GFM table
256
+ .checklist(["Install deps", "Run tests", { label: "Deploy", done: false }]) // - [ ] / - [x]
257
+ .list(["First", "Second"], false) // bullet list
258
+ .list(["One", "Two"], true) // ordered list
259
+ .codeBlock("npm install tgplus", "bash") // fenced code
260
+ .blockquote("Generated automatically.") // > quote
261
+ .photo("https://example.com/screenshot.png", "Build screenshot") // inline image
262
+ .divider() // ---
263
+ .build()
264
+ )
265
+ );
266
+
267
+ // ---------- Rich HTML ----------
268
+ bot.command("html", (ctx) =>
269
+ ctx.replyRich(
270
+ RichMessage.html()
271
+ .heading("Hello", 2)
272
+ .paragraph("This is <b>Rich HTML</b>.")
273
+ .raw("<tg-spoiler>hidden</tg-spoiler>")
274
+ .build()
275
+ )
276
+ );
277
+
278
+ // ---------- Rich blocks (10.2+) — real structure, media, buttons, details ----------
279
+ bot.command("card", (ctx) =>
280
+ ctx.replyRich(
281
+ RichMessage.blocks()
282
+ .heading("Server status", 2)
283
+ .paragraph("All systems operational.")
284
+ .divider()
285
+ .table(
286
+ [
287
+ [{ text: "Region", is_header: true }, { text: "Uptime", is_header: true }],
288
+ [{ text: "EU" }, { text: "99.99%" }],
289
+ ],
290
+ { bordered: true, striped: true, caption: "Last 24h" }
291
+ )
292
+ .list([
293
+ { label: "EU-west", blocks: [{ type: "paragraph", text: "99.99% uptime" }], is_checked: true },
294
+ ])
295
+ .photo({ url: "https://example.com/chart.png" }, { hasSpoiler: true, caption: { text: "Traffic" } })
296
+ .video({ url: "https://example.com/clip.mp4" }, { caption: { text: "Demo" } })
297
+ .audio({ url: "https://example.com/audio.mp3" })
298
+ .animation({ url: "https://example.com/loop.gif" })
299
+ .voiceNote({ url: "https://example.com/voice.ogg" })
300
+ .document({ url: "https://example.com/report.pdf" }, { caption: { text: "Full report" } }) // 10.3
301
+ .buttons([{ text: "Open dashboard", url: "https://example.com" }]) // 10.3
302
+ .details("More details", [{ type: "paragraph", text: "Hidden until expanded." }], true) // collapsible
303
+ .expandableBlockquote([{ type: "paragraph", text: "Long quote collapsed by default." }], { credit: "— Team" })
304
+ .raw({ type: "divider" }) // escape hatch for any block
305
+ .build()
306
+ )
307
+ );
308
+
309
+ // The same InputRichMessage works with the raw API and in edits
310
+ await bot.api.sendRichMessage({ chat_id: 42, rich_message: RichMessage.markdown().text("Hi").build() });
311
+ await bot.api.editMessageText({ chat_id: 42, message_id: 10, rich_message: RichMessage.markdown().text("Updated").build() });
312
+ ```
313
+
314
+ ### 5. Inline streaming & AI drafts
315
+
316
+ For AI/LLM output, Telegram supports **draft messages**: partial content is previewed in a private chat while it is being generated, then replaced by a real message when the stream finishes. `tgplus` wraps both the plain-text and rich-message drafts.
317
+
318
+ - Drafts are **private-chat only** (`chat_id` must be the numeric user id).
319
+ - A draft is **ephemeral** — a short preview, not saved in the chat.
320
+ - `draft_id` must be **non-zero**; repeated calls with the same `draft_id` animate between states.
321
+ - Bot API 10.3 adds `can_stop` (the user may stop generation) and `keep_on_stop` (keep the draft visible after stopping).
322
+
323
+ ```typescript
324
+ // ---------- Stream plain text while an LLM generates it ----------
325
+ bot.command("ask", async (ctx) => {
326
+ const draftId = Date.now(); // any non-zero number, stable for this generation
327
+ let accumulated = "";
328
+
329
+ for await (const chunk of fakeLLMStream("Write me a haiku about bots")) {
330
+ accumulated += chunk;
331
+ await bot.api.sendMessageDraft({
332
+ chat_id: ctx.from!.id, // drafts are private-chat only
333
+ draft_id: draftId,
334
+ text: accumulated,
335
+ can_stop: true, // Bot API 10.3+
336
+ keep_on_stop: false, // Bot API 10.3+
337
+ });
338
+ }
339
+
340
+ // Finalize with a real, saved message
341
+ await ctx.reply(accumulated);
342
+ });
343
+
344
+ // ---------- Stream a rich message (markdown/HTML/blocks) ----------
345
+ bot.command("ask-rich", async (ctx) => {
346
+ const draftId = Date.now();
347
+ let accumulated = "";
348
+
349
+ for await (const chunk of fakeLLMStream("Summarize the release notes")) {
350
+ accumulated += chunk;
351
+ await bot.api.sendRichMessageDraft({
352
+ chat_id: ctx.from!.id,
353
+ draft_id: draftId,
354
+ rich_message: RichMessage.markdown().text(accumulated).build(),
355
+ can_stop: true,
356
+ });
357
+ }
358
+
359
+ // Commit the finished content as a real message
360
+ await ctx.replyRich(RichMessage.markdown().text(accumulated).build());
361
+ });
362
+
363
+ // Helper standing in for a real streaming model call
364
+ async function* fakeLLMStream(_prompt: string): AsyncGenerator<string> {
365
+ for (const word of ["Streaming ", "a ", "reply ", "token ", "by ", "token."]) yield word;
366
+ }
367
+ ```
368
+
369
+ > Sending a draft is `sendMessageDraft` / `sendRichMessageDraft`; both return `Promise<true>`. See the source comments in `src/client.ts` for the confidence notes on each (the rich variant's shape is field-verified; the plain-text variant is modeled on it).
370
+
371
+ ### 6. Decorator classes
372
+
373
+ Pure sugar over the same router — mix freely with the other styles.
374
+
375
+ ```typescript
376
+ import { Bot, Context, BotController, Command, Hears, Action, On } from "tgplus";
377
+
378
+ @BotController() // optional marker decorator
379
+ class MainController {
380
+ @Command("start")
381
+ start(ctx: Context) { return ctx.reply("Welcome!"); }
382
+
383
+ @Command(["help", "h"])
384
+ help(ctx: Context) { return ctx.reply("Send /start, or say thanks."); }
385
+
386
+ @Hears(/^thanks?$/i)
387
+ thanks(ctx: Context) { return ctx.reply("You're welcome!"); }
388
+
389
+ @Action(/^confirm:(yes|no)$/)
390
+ confirm(ctx: Context) {
391
+ const choice = ctx.match?.[1];
392
+ return ctx.reply(choice === "yes" ? "Confirmed ✅" : "Cancelled ❌");
393
+ }
394
+
395
+ @On("photo")
396
+ photo(ctx: Context) { return ctx.reply("Nice photo!"); }
397
+ }
398
+
399
+ const bot = new Bot(process.env.BOT_TOKEN!);
400
+ bot.useController(new MainController());
401
+ bot.launch();
402
+ ```
403
+
404
+ ### 7. Scenes (multi-step conversations)
405
+
406
+ Mount `session()` **before** `Stage.middleware()`. Each step runs once per incoming update; `next()` advances the pointer (the next step runs on the *next* update), `selectStep(index)` jumps and runs immediately, `scene.leave()` exits.
407
+
408
+ ```typescript
409
+ import { Context, session, Stage, WizardScene } from "tgplus";
410
+
411
+ bot.use(session());
412
+
413
+ const signup = new WizardScene<Context>("signup", [
414
+ async (ctx) => { await ctx.reply("What's your name?"); await ctx.wizard.next(); },
415
+ async (ctx) => { ctx.wizard.state.name = ctx.text; await ctx.reply("How old are you?"); await ctx.wizard.next(); },
416
+ async (ctx) => {
417
+ ctx.wizard.state.age = ctx.text;
418
+ await ctx.reply(`Thanks, ${ctx.wizard.state.name} (age ${ctx.wizard.state.age})!`);
419
+ await ctx.scene.leave();
420
+ },
421
+ ]);
422
+
423
+ const stage = new Stage<Context>([signup]);
424
+ stage.register(/* more WizardScenes… */);
425
+ bot.use(stage.middleware());
426
+
427
+ bot.command("signup", (ctx) => ctx.scene!.enter("signup")); // enter, optionally with initial state
428
+ bot.command("edit", (ctx) => ctx.scene!.selectStep?.(0) as any); // (wizard.selectStep from inside a step)
429
+ bot.command("cancel", (ctx) => ctx.scene!.leave());
430
+ bot.command("where", (ctx) => ctx.reply(`Current scene: ${ctx.scene!.current ?? "none"}`));
431
+
432
+ // Inside a step you also have:
433
+ // ctx.wizard.cursor -> current step index
434
+ // ctx.wizard.state -> your collected data
435
+ // ctx.wizard.next() -> advance one step
436
+ // ctx.wizard.selectStep(n) -> jump to step n and run it now
437
+ ```
438
+
439
+ ### 8. Sessions
440
+
441
+ One `SessionStore` interface, three implementations. Swap without touching handlers. Sessions are keyed per chat by default — pass `getSessionKey` for per-user sessions.
442
+
443
+ ```typescript
444
+ import { session, MemorySessionStore, RedisSessionStore, SqliteSessionStore } from "tgplus";
445
+
446
+ // ---------- In-memory (default) ----------
447
+ bot.use(session({ defaultSession: () => ({ count: 0 }) }));
448
+ bot.on("text", (ctx) => {
449
+ const s = ctx.session as { count: number };
450
+ s.count += 1;
451
+ return ctx.reply(`Message #${s.count}`);
452
+ });
453
+
454
+ // ---------- Redis (ioredis or node-redis v4+; duck-typed) ----------
455
+ import Redis from "ioredis";
456
+ bot.use(session({
457
+ store: new RedisSessionStore(new Redis(), { prefix: "mybot:session:", ttlSeconds: 86400 }),
458
+ defaultSession: () => ({ lang: "en" }),
459
+ }));
460
+
461
+ // ---------- SQLite (better-sqlite3; duck-typed, synchronous) ----------
462
+ import Database from "better-sqlite3";
463
+ bot.use(session({
464
+ store: new SqliteSessionStore(new Database("bot.db"), { tableName: "my_sessions" }),
465
+ }));
466
+
467
+ // ---------- Per-user sessions instead of per-chat ----------
468
+ bot.use(session({ getSessionKey: (ctx) => (ctx.from ? String(ctx.from.id) : undefined) }));
469
+
470
+ // ---------- Deleting a session ----------
471
+ bot.command("reset", async (ctx) => {
472
+ (ctx as any).session = undefined; // session() removes the stored entry when ctx.session is undefined after your handler
473
+ await ctx.reply("Session cleared.");
474
+ });
475
+ ```
476
+
477
+ ### 9. Rate limiting
478
+
479
+ Sliding-window limiter for **incoming** updates. Mount it early so spam never reaches your handlers.
480
+
481
+ ```typescript
482
+ import { rateLimit } from "tgplus";
483
+
484
+ bot.use(rateLimit({
485
+ windowMs: 2000, // window length, default 3000
486
+ limit: 3, // max updates per window per key, default 1
487
+ keyFn: (ctx) => (ctx.from ? String(ctx.from.id) : undefined), // default: per chat
488
+ onLimitExceeded: (ctx) => ctx.reply("Slow down!"), // default: silently drop
489
+ }));
490
+ ```
491
+
492
+ ### 10. Flood control & throttling
493
+
494
+ 429 responses are retried automatically on **every** API call (not just polling), honoring Telegram's `retry_after`. `minIntervalMs` queues outgoing calls at a fixed pace.
495
+
496
+ ```typescript
497
+ const bot = new Bot(process.env.BOT_TOKEN!, {
498
+ floodControl: { maxRetries: 5 }, // default 3
499
+ minIntervalMs: 35, // ~28 msgs/sec, safely under Telegram's ~30/sec
500
+ apiRoot: "https://api.telegram.org", // point at a local Bot API server if you run one
501
+ });
502
+ ```
503
+
504
+ ### 11. i18n
505
+
506
+ `createI18n` attaches `ctx.t(key, ...args)` using the user's Telegram `language_code`, falling back to `defaultLocale`. Dictionary values may be strings or functions (for interpolation/pluralization).
507
+
508
+ ```typescript
509
+ import { createI18n } from "tgplus";
510
+
511
+ const i18n = createI18n({
512
+ defaultLocale: "en",
513
+ locales: {
514
+ en: {
515
+ hi: (n: string) => `Hi ${n}!`,
516
+ apples: (n: number) => (n === 1 ? "1 apple" : `${n} apples`),
517
+ missing: "This one is a plain string",
518
+ },
519
+ fr: {
520
+ hi: (n: string) => `Salut ${n} !`,
521
+ apples: (n: number) => (n === 1 ? "1 pomme" : `${n} pommes`),
522
+ },
523
+ },
524
+ // Optional: resolve the locale yourself instead of using language_code
525
+ // getLocale: (ctx) => (ctx as any).session?.lang,
526
+ });
527
+
528
+ bot.use(i18n.middleware());
529
+
530
+ bot.command("start", (ctx) => ctx.reply(ctx.t("hi", ctx.from?.first_name ?? "there")));
531
+ bot.command("apples", (ctx) => ctx.reply(ctx.t("apples", 3)));
532
+ bot.command("lang", (ctx) => ctx.reply(`Your locale: ${ctx.locale ?? "unknown"}`));
533
+ // Unknown keys return the key itself instead of throwing
534
+ ```
535
+
536
+ ### 12. Entity & reaction filters
537
+
538
+ Composable predicates used with `bot.on(filter())` or `bot.filter(...)`. Each maps to exactly one real Bot API entity type — they are deliberately not merged.
539
+
540
+ ```typescript
541
+ import {
542
+ mention, hashtag, cashtag, url, textLink, textMention, phone, email,
543
+ botCommand, spoiler, customEmoji, reaction,
544
+ } from "tgplus";
545
+
546
+ bot.on(mention(), (ctx) => ctx.reply("A @username was mentioned.")); // entity: mention
547
+ bot.on(textMention(), (ctx) => ctx.reply("Mentioned a user with no username.")); // entity: text_mention
548
+ bot.on(hashtag(), (ctx) => ctx.reply("Nice hashtag.")); // entity: hashtag
549
+ bot.on(cashtag(), (ctx) => ctx.reply("Cashtag spotted.")); // entity: cashtag
550
+ bot.on(url(), (ctx) => ctx.reply("Plain URL in the text.")); // entity: url
551
+ bot.on(textLink(), (ctx) => ctx.reply("Labeled [link](url).")); // entity: text_link
552
+ bot.on(phone(), (ctx) => ctx.reply("That looks like a phone number.")); // entity: phone_number
553
+ bot.on(email(), (ctx) => ctx.reply("That looks like an email.")); // entity: email
554
+ bot.on(botCommand(), (ctx) => ctx.reply("A /command appeared.")); // entity: bot_command
555
+ bot.on(spoiler(), (ctx) => ctx.reply("Spoiler content.")); // entity: spoiler
556
+ bot.on(customEmoji(), (ctx) => ctx.reply("Premium custom emoji.")); // entity: custom_emoji
557
+ bot.on(reaction("🔥"), (ctx) => ctx.reply("Things are heating up.")); // message_reaction with 🔥
558
+ bot.on(reaction(), (ctx) => ctx.reply("A reaction changed.")); // any reaction change
559
+
560
+ // Filters are plain predicates — usable with bot.filter() too
561
+ bot.filter(hashtag(), (ctx) => ctx.reply("Also a hashtag."));
562
+ ```
563
+
564
+ ### 13. File uploads & downloads
565
+
566
+ Paths and Node streams are genuinely streamed through a hand-built multipart encoder (never fully buffered). Buffers use the proven `FormData` path.
567
+
568
+ ```typescript
569
+ import fs from "node:fs";
570
+
571
+ // ---------- Uploads ----------
572
+ await ctx.replyWithPhoto({ source: "./photo.jpg" }); // local path
573
+ await ctx.replyWithDocument({ source: fs.createReadStream("./big.zip"), filename: "big.zip" }); // stream
574
+ await ctx.replyWithPhoto({ source: buffer, filename: "generated.png" }); // Buffer
575
+ await ctx.replyWithVideo({ source: "./clip.mp4", filename: "clip.mp4" }, { caption: "Demo" });
576
+ await ctx.replyWithAudio({ source: "./song.mp3" });
577
+ await ctx.replyWithSticker({ source: "./cat.webp" });
578
+
579
+ // Any send method accepts the same InputFile shape
580
+ await bot.api.sendDocument({ chat_id: ctx.chatId!, document: { source: "./report.pdf" } });
581
+ await bot.api.setChatPhoto({ chat_id: ctx.chatId!, photo: { source: "./icon.png" } });
582
+
583
+ // ---------- Receiving files ----------
584
+ bot.on("document", async (ctx) => {
585
+ const file = await bot.api.getFile({ file_id: ctx.message!.document!.file_id });
586
+ const url = await bot.api.getFileLink(file); // resolves getFile() for you if you pass an id
587
+ console.log("Download:", url);
588
+ });
589
+
590
+ // Build a download URL directly from a known file_path
591
+ const direct = bot.api.fileUrl("documents/file_12.pdf");
592
+ ```
593
+
594
+ ### 14. Composer gates, combinators & Router
595
+
596
+ Gates restrict handlers by chat type or sender role. Note: a nested handler must come from the **static** `Composer` methods (the instance methods return the composer, not a middleware).
597
+
598
+ ```typescript
599
+ import { Composer, Router } from "tgplus";
600
+
601
+ // ---------- Gates ----------
602
+ bot.privateChat(Composer.command("start", (ctx) => ctx.reply("Welcome!")));
603
+ bot.groupChat(Composer.command("rules", (ctx) => ctx.reply("Be nice.")));
604
+ bot.chatType("private", Composer.hears("hi", (ctx) => ctx.reply("Hi!")));
605
+ bot.chatType(["group", "supergroup"], Composer.command("stats", (ctx) => ctx.reply("Stats…")));
606
+ bot.admin(Composer.command("ban", (ctx) => ctx.reply("Banning…"))); // one getChatMember per matching update
607
+ bot.creator(Composer.command("shutdown", (ctx) => ctx.reply("Shutting down…")));
608
+
609
+ // ---------- Combinators ----------
610
+ bot.filter((ctx) => ctx.chat?.type === "private", (ctx) => ctx.reply("private!"));
611
+ bot.optional((ctx) => ctx.from?.id === 42, (ctx) => ctx.reply("Hello, 42.")); // alias for filter()
612
+ bot.branch(
613
+ (ctx) => ctx.text === "vip",
614
+ (ctx) => ctx.reply("VIP path"),
615
+ [(ctx) => ctx.reply("Normal path")]
616
+ );
617
+ bot.lazy((ctx) => async (c) => c.reply(`Resolved at dispatch time: ${c.text}`));
618
+ bot.otherwise((ctx) => ctx.reply("Fallback."));
619
+
620
+ // ---------- Mount a whole sub-composer as a module ----------
621
+ const adminModule = new Composer();
622
+ adminModule.command("reload", (ctx) => ctx.reply("Reloading…"));
623
+ bot.admin(adminModule.handle.bind(adminModule));
624
+ bot.mount(adminModule);
625
+
626
+ // ---------- Compose middlewares without a Composer instance ----------
627
+ const composed = Composer.compose([
628
+ async (ctx, next) => { console.log("before"); await next(); },
629
+ async (ctx) => { await ctx.reply("after"); },
630
+ ]);
631
+ bot.use(composed);
632
+
633
+ // ---------- Router: key-based dispatch ----------
634
+ const router = new Router((ctx) => (ctx as any).session?.lang);
635
+ router.on("en", (ctx) => ctx.reply("Hi!"));
636
+ router.on("fr", (ctx) => ctx.reply("Salut !"));
637
+ router.otherwise((ctx) => ctx.reply("Hi! (set a language with /lang)"));
638
+ bot.use(router.middleware());
639
+ ```
640
+
641
+ ### 15. Polling, webhooks & serverless
642
+
643
+ ```typescript
644
+ // ---------- Long polling (default) ----------
645
+ await bot.launch({ dropPendingUpdates: false, allowedUpdates: ["message", "callback_query"] });
646
+ bot.stop(); // also wired to SIGINT/SIGTERM automatically
647
+
648
+ // ---------- Standalone webhook server (sets the webhook and listens) ----------
649
+ await bot.launchWebhook({
650
+ url: "https://your-domain.com",
651
+ port: 8080,
652
+ path: "/webhook", // default "/webhook"
653
+ secretToken: process.env.WEBHOOK_SECRET,
654
+ });
655
+
656
+ // ---------- Raw http — mount anywhere (Express/Fastify/Next custom server) ----------
657
+ import http from "node:http";
658
+ const server = http.createServer(bot.webhookCallback("/webhook", { secretToken: process.env.WEBHOOK_SECRET }));
659
+
660
+ // ---------- Serverless adapters ----------
661
+ import { vercelWebhookHandler, lambdaWebhookHandler, cloudflareWebhookHandler } from "tgplus";
662
+
663
+ // Vercel (pages/api or app route handler)
664
+ export default vercelWebhookHandler(bot, { secretToken: process.env.WEBHOOK_SECRET });
665
+
666
+ // AWS Lambda behind API Gateway
667
+ export const handler = lambdaWebhookHandler(bot, { secretToken: process.env.WEBHOOK_SECRET });
668
+
669
+ // Cloudflare Workers
670
+ export default { fetch: cloudflareWebhookHandler(bot, { secretToken: process.env.WEBHOOK_SECRET }) };
671
+
672
+ // Or drive a parsed update yourself (bring your own framework)
673
+ await bot.handleUpdate(update);
674
+ ```
675
+
676
+ ### 16. Context helpers
677
+
678
+ Every helper auto-fills `chat_id` and resolves ids from the current update type.
679
+
680
+ ```typescript
681
+ bot.on("text", async (ctx) => {
682
+ // Accessors
683
+ ctx.update; ctx.updateType; ctx.botInfo;
684
+ ctx.message; ctx.callbackQuery; ctx.inlineQuery;
685
+ ctx.chat; ctx.chatId; ctx.from; ctx.text; ctx.match; ctx.state;
686
+
687
+ // Sending
688
+ await ctx.reply("text");
689
+ await ctx.replyRich(RichMessage.markdown().text("rich").build());
690
+ await ctx.replyWithPhoto({ source: "./p.jpg" });
691
+ await ctx.replyWithVideo({ source: "./v.mp4" });
692
+ await ctx.replyWithDocument({ source: "./d.pdf" });
693
+ await ctx.replyWithAudio({ source: "./a.mp3" });
694
+ await ctx.replyWithSticker({ source: "./s.webp" });
695
+ await ctx.replyWithPoll("Pineapple on pizza?", ["Yes", "Obviously"], { is_anonymous: false });
696
+ await ctx.replyWithChatAction("typing"); // sendChatAction shortcut
697
+
698
+ // Editing / deleting the current message (or the message a button was attached to)
699
+ await ctx.editText("edited");
700
+ await ctx.editCaption("edited caption");
701
+ await ctx.editMedia({ type: "photo", media: { url: "https://picsum.photos/600" } });
702
+ await ctx.editReplyMarkup(Keyboard.inline().button("Again", "again").build());
703
+ await ctx.deleteMessage(); // or ctx.deleteMessage(messageId)
704
+
705
+ // Callback/inline responses
706
+ if (ctx.callbackQuery) await ctx.answerCbQuery("Done");
707
+ if (ctx.inlineQuery) await ctx.answerInlineQuery([]);
708
+ });
709
+ ```
710
+
711
+ ### 17. Typed API & escape hatch
712
+
713
+ Every Bot API method has a typed, autocompleted wrapper. Anything Telegram ships next works immediately through `api.call()`.
714
+
715
+ ```typescript
716
+ // Fully typed — parameters and return value
717
+ const me = await bot.api.getMe();
718
+ const sent = await bot.api.sendMessage({ chat_id: 42, text: "typed and autocompleted" });
719
+ await bot.api.setChatPermissions({ chat_id: -100123, permissions: { can_send_messages: false } });
720
+ await bot.api.setMyCommands({ commands: [{ command: "start", description: "Start the bot" }] });
721
+
722
+ // Escape hatch — brand-new/undocumented methods, the same day Telegram ships them
723
+ await bot.api.call("someBrandNewMethod", { chat_id: 42, whatever: true });
724
+ const meToo = await bot.api.call<{ id: number }>("getMe");
725
+ ```
726
+
727
+ ### 18. Error handling
728
+
729
+ Central handler for anything thrown in your handlers. API failures throw a typed `TelegramApiError`.
730
+
731
+ ```typescript
732
+ import { TelegramApiError } from "tgplus";
733
+
734
+ bot.catch((err, ctx) => {
735
+ console.error("Update failed:", ctx.updateType, err);
736
+ if (ctx.chat) ctx.reply("Something went wrong.").catch(() => {});
737
+ });
738
+
739
+ try {
740
+ await bot.api.sendMessage({ chat_id: 42, text: "hi" });
741
+ } catch (err) {
742
+ if (err instanceof TelegramApiError) {
743
+ console.log(err.method, err.errorCode, err.message); // "sendMessage", 403, "sendMessage failed (403): …"
744
+ if (err.errorCode === 403) { /* blocked */ }
745
+ if (err.parameters?.retry_after) { /* flood wait */ }
746
+ if (err.parameters?.migrate_to_chat_id) { /* group → supergroup migration */ }
747
+ }
748
+ }
749
+ ```
750
+
751
+ ---
752
+
753
+ ## Every Class, With An Example
754
+
755
+ Every class exported from `tgplus`, in one place.
756
+
757
+ ### `Bot` — `src/bot.ts`
758
+
759
+ The entry point. `Bot` **extends `Composer`**, so every router/helper method is available directly on it. Holds `bot.api`.
760
+
761
+ ```typescript
762
+ import { Bot } from "tgplus";
763
+
764
+ const bot = new Bot(process.env.BOT_TOKEN!, {
765
+ contextType: MyContext, // optional: a Context subclass for extra helpers
766
+ apiRoot: "https://api.telegram.org", // optional: local Bot API server
767
+ floodControl: { maxRetries: 5 }, // optional
768
+ minIntervalMs: 35, // optional
769
+ });
770
+
771
+ bot.command("start", (ctx) => ctx.reply("hi")); // Composer methods
772
+ bot.useController(new MainController()); // decorator controllers
773
+ bot.catch((err, ctx) => console.error(err)); // central error handler
774
+ await bot.launch(); // long polling (SIGINT/SIGTERM safe)
775
+ bot.stop();
776
+ await bot.launchWebhook({ url, port, secretToken });
777
+ const handler = bot.webhookCallback("/webhook", { secretToken });
778
+ await bot.handleUpdate(update); // for your own serverless glue
779
+ bot.api; // the Api instance
780
+ ```
781
+
782
+ ### `Context` — `src/context.ts`
783
+
784
+ One incoming update, with the accessors and reply/edit helpers people reach for constantly. See [Context helpers](#16-context-helpers) for the full method list.
785
+
786
+ ```typescript
787
+ bot.on("text", (ctx) => {
788
+ ctx.message; ctx.chat; ctx.from; ctx.chatId; ctx.text; ctx.match;
789
+ ctx.update; ctx.updateType; ctx.api; ctx.botInfo; ctx.state;
790
+ ctx.session; ctx.scene; ctx.wizard; ctx.locale; ctx.t("key");
791
+ return ctx.reply("hello");
792
+ });
793
+ ```
794
+
795
+ ### `Composer` — `src/composer.ts`
796
+
797
+ The middleware engine. `use`, `mount`, `filter`, `on`, `command`, `hears`, `action`, `otherwise`, `chatType`, `privateChat`, `groupChat`, `admin`, `creator`, `optional`, `branch`, `lazy`, plus static `filter`/`on`/`command`/`hears`/`action`/`compose`. See [section 14](#14-composer-gates-combinators--router).
798
+
799
+ ```typescript
800
+ import { Composer } from "tgplus";
801
+ const module = new Composer<Context>();
802
+ module.command("ping", (ctx) => ctx.reply("pong"));
803
+ bot.mount(module);
804
+ ```
805
+
806
+ ### `Router` — `src/composer.ts`
807
+
808
+ Key-based dispatch. Construct with a key function, register `on(key, …)`, optional `otherwise(…)`, then `bot.use(router.middleware())` (also `new Router<Context>((ctx) => ctx.session?.lang)`).
809
+
810
+ ```typescript
811
+ const router = new Router<Context>((ctx) => (ctx.session as any)?.lang);
812
+ router.on("en", (ctx) => ctx.reply("Hi!"));
813
+ router.on("fr", (ctx) => ctx.reply("Salut !"));
814
+ router.otherwise((ctx) => ctx.reply("Hi!"));
815
+ bot.use(router.middleware());
816
+ ```
817
+
818
+ ### `Api` — `src/client.ts`
819
+
820
+ All 187 typed Bot API methods, plus `call()` and `fileUrl()`. Reachable as `bot.api`, or constructed directly over a `Transport`.
821
+
822
+ ```typescript
823
+ await bot.api.sendMessage({ chat_id: 42, text: "hi" });
824
+ await bot.api.getMe();
825
+ await bot.api.call("anyMethod", { … });
826
+ bot.api.fileUrl("documents/file_12.pdf");
827
+ await bot.api.getFileLink(fileIdOrFile);
828
+ ```
829
+
830
+ ### `Transport` — `src/client.ts`
831
+
832
+ HTTP transport: JSON + streaming multipart, flood-control retries, optional pacing `minIntervalMs`, custom `apiRoot`. Exported for advanced use (custom clients/tests).
833
+
834
+ ```typescript
835
+ import { Api, Transport } from "tgplus";
836
+
837
+ const transport = new Transport(process.env.BOT_TOKEN!, {
838
+ apiRoot: "http://localhost:8081", // e.g. a local Bot API server
839
+ floodControl: { maxRetries: 4 },
840
+ minIntervalMs: 50,
841
+ });
842
+ const api = new Api(transport);
843
+ await api.sendMessage({ chat_id: 42, text: "via a custom transport" });
844
+ transport.getFileDownloadUrl("documents/file_12.pdf");
845
+
846
+ // Note: ClientOptions also declares `timeoutMs`, but it is currently accepted
847
+ // and not applied to requests — don't rely on it for timeouts yet.
848
+ ```
849
+
850
+ ### `TelegramApiError` — `src/client.ts`
851
+
852
+ Thrown by every API call on a non-`ok` response. Carries `method`, `errorCode`, and `parameters`.
853
+
854
+ ```typescript
855
+ import { TelegramApiError } from "tgplus";
856
+
857
+ try {
858
+ await bot.api.sendMessage({ chat_id: 42, text: "hi" });
859
+ } catch (err) {
860
+ if (err instanceof TelegramApiError) {
861
+ err.method; // "sendMessage"
862
+ err.errorCode; // 400 | 403 | 429 | …
863
+ err.message; // "sendMessage failed (403): …"
864
+ err.parameters?.retry_after; // 429 flood wait, seconds
865
+ err.parameters?.migrate_to_chat_id;
866
+ }
867
+ }
868
+ ```
869
+
870
+ ### `Keyboard`, `InlineKeyboardBuilder`, `ReplyKeyboardBuilder` — `src/keyboard.ts`
871
+
872
+ Fluent keyboard builders. See [section 2](#2-keyboard-builders).
873
+
874
+ ```typescript
875
+ import { Keyboard, InlineKeyboardBuilder, ReplyKeyboardBuilder } from "tgplus";
876
+
877
+ Keyboard.inline().button("Yes", "y").row().url("Docs", "https://x.dev").build();
878
+ Keyboard.reply().text("Yes").row().requestLocation("Where?").build({ oneTime: true });
879
+ Keyboard.remove();
880
+ Keyboard.forceReply("Type here");
881
+
882
+ const inline = new InlineKeyboardBuilder().button("A", "a").build();
883
+ const reply = new ReplyKeyboardBuilder().text("A").build();
884
+ ```
885
+
886
+ ### `RichMessage`, `RichMarkdownBuilder`, `RichHtmlBuilder`, `RichBlocksBuilder` — `src/richMessage.ts`
887
+
888
+ Rich Message builders. See [section 4](#4-rich-messages-bot-api-101103).
889
+
890
+ ```typescript
891
+ import { RichMessage, RichMarkdownBuilder, RichHtmlBuilder, RichBlocksBuilder } from "tgplus";
892
+
893
+ RichMessage.markdown(); RichMessage.html(); RichMessage.blocks();
894
+
895
+ const md: RichMarkdownBuilder = new RichMarkdownBuilder();
896
+ const html: RichHtmlBuilder = new RichHtmlBuilder();
897
+ const blocks: RichBlocksBuilder = new RichBlocksBuilder();
898
+ ctx.replyRich(md.text("Hello").build());
899
+ ```
900
+
901
+ ### `BotController`, `Command`, `Hears`, `Action`, `On` — `src/decorators.ts`
902
+
903
+ Class-based controllers. See [section 6](#6-decorator-classes).
904
+
905
+ ```typescript
906
+ import { BotController, Command, Hears, Action, On } from "tgplus";
907
+
908
+ @BotController()
909
+ class Controller {
910
+ @Command("start") start(ctx: Context) { return ctx.reply("hi"); }
911
+ @Hears(/^thanks$/i) thanks(ctx: Context) { return ctx.reply("yw"); }
912
+ @Action("like") like(ctx: Context) { return ctx.answerCbQuery("❤️"); }
913
+ @On("photo") photo(ctx: Context) { return ctx.reply("nice"); }
914
+ }
915
+ bot.useController(new Controller());
916
+ ```
917
+
918
+ ### `session`, `MemorySessionStore`, `RedisSessionStore`, `SqliteSessionStore` — `src/session*.ts`
919
+
920
+ Session middleware and the three stores. See [section 8](#8-sessions).
921
+
922
+ ```typescript
923
+ import { session, MemorySessionStore, RedisSessionStore, SqliteSessionStore } from "tgplus";
924
+
925
+ session({ store, getSessionKey, defaultSession });
926
+ new MemorySessionStore<{ count: number }>();
927
+ new RedisSessionStore(new Redis(), { prefix: "mybot:session:", ttlSeconds: 86400 });
928
+ new SqliteSessionStore(new Database("bot.db"), { tableName: "my_sessions" });
929
+
930
+ // Custom store: implement SessionStore's get/set/delete
931
+ import type { SessionStore } from "tgplus";
932
+ const myStore: SessionStore<{ count: number }> = {
933
+ get: async (k) => undefined,
934
+ set: async (k, v) => {},
935
+ delete: async (k) => {},
936
+ };
937
+ ```
938
+
939
+ ### `WizardScene`, `Stage` — `src/scenes.ts`
940
+
941
+ Multi-step conversations. See [section 7](#7-scenes-multi-step-conversations).
942
+
943
+ ```typescript
944
+ import { WizardScene, Stage } from "tgplus";
945
+
946
+ const scene = new WizardScene<Context>("signup", [step1, step2, step3]);
947
+ const stage = new Stage<Context>([scene]).register(anotherScene);
948
+ bot.use(session());
949
+ bot.use(stage.middleware());
950
+ bot.command("signup", (ctx) => ctx.scene!.enter("signup", { from: "menu" }));
951
+ ```
952
+
953
+ ### `rateLimit` — `src/rateLimit.ts`
954
+
955
+ Incoming-update limiter. See [section 9](#9-rate-limiting).
956
+
957
+ ```typescript
958
+ import { rateLimit, RateLimitOptions } from "tgplus";
959
+
960
+ const opts: RateLimitOptions = { windowMs: 3000, limit: 2, onLimitExceeded: (ctx) => ctx.reply("Slow down!") };
961
+ bot.use(rateLimit(opts));
962
+ ```
963
+
964
+ ### `createI18n` — `src/i18n.ts`
965
+
966
+ Locale middleware and `ctx.t()`. See [section 11](#11-i18n).
967
+
968
+ ```typescript
969
+ import { createI18n } from "tgplus";
970
+
971
+ const i18n = createI18n({ defaultLocale: "en", locales: { en: { hi: "Hi" }, fr: { hi: "Salut" } } });
972
+ bot.use(i18n.middleware());
973
+ i18n.translate("fr", "hi");
974
+ ctx.t("hi");
975
+ ```
976
+
977
+ ### Serverless adapters — `src/adapters.ts`
978
+
979
+ Vercel / Lambda / Cloudflare Workers request adapters. See [section 15](#15-polling-webhooks--serverless).
980
+
981
+ ```typescript
982
+ import { vercelWebhookHandler, lambdaWebhookHandler, cloudflareWebhookHandler } from "tgplus";
983
+
984
+ export default vercelWebhookHandler(bot, { secretToken });
985
+ export const handler = lambdaWebhookHandler(bot, { secretToken });
986
+ export default { fetch: cloudflareWebhookHandler(bot, { secretToken }) };
987
+ ```
988
+
989
+ ### Filters — `src/filters.ts`
990
+
991
+ `mention`, `hashtag`, `cashtag`, `url`, `textLink`, `textMention`, `phone`, `email`, `botCommand`, `spoiler`, `customEmoji`, `reaction`. See [section 12](#12-entity--reaction-filters).
992
+
993
+ ```typescript
994
+ import { mention, hashtag, reaction } from "tgplus";
995
+ bot.on(mention(), (ctx) => ctx.reply("mentioned!"));
996
+ bot.on(hashtag(), (ctx) => ctx.reply("hashtag!"));
997
+ bot.on(reaction("👍"), (ctx) => ctx.reply("thanks!"));
998
+ ```
999
+
1000
+ ### Types namespace — `src/types.ts`
1001
+
1002
+ Every Bot API type is exported as `Types` (and used throughout the typed API).
1003
+
1004
+ ```typescript
1005
+ import { Types } from "tgplus";
1006
+
1007
+ const update: Types.Update = { update_id: 1 };
1008
+ const file: Types.InputFile = { source: "./a.pdf", filename: "a.pdf" };
1009
+ const params: Types.SendMessageParams = { chat_id: 42, text: "hi" }; // where available
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## The Complete API Surface
1015
+
1016
+ `bot.api` exposes **187 typed methods across 21 categories**, plus the `call()` escape hatch. Below: every category with an example, and the full method inventory.
1017
+
1018
+ ### Core
1019
+
1020
+ ```typescript
1021
+ await bot.api.call("anyMethod", { … }); // escape hatch, always current
1022
+ bot.api.fileUrl("documents/file_12.pdf"); // build a file download URL
1023
+ ```
1024
+
1025
+ `call`, `fileUrl`
1026
+
1027
+ ### Getting updates (7)
1028
+
1029
+ ```typescript
1030
+ const me = await bot.api.getMe();
1031
+ await bot.api.setWebhook({ url: "https://x.dev/hook", secret_token: "s", max_connections: 40, allowed_updates: ["message"], drop_pending_updates: true });
1032
+ const info = await bot.api.getWebhookInfo();
1033
+ await bot.api.deleteWebhook({ drop_pending_updates: true });
1034
+ const updates = await bot.api.getUpdates({ offset: 0, timeout: 30, limit: 100 });
1035
+ await bot.api.logOut();
1036
+ await bot.api.close();
1037
+ ```
1038
+
1039
+ `getMe`, `logOut`, `close`, `getUpdates`, `setWebhook`, `deleteWebhook`, `getWebhookInfo`
1040
+
1041
+ ### Sending messages (28)
1042
+
1043
+ ```typescript
1044
+ await bot.api.sendMessage({ chat_id, text: "hi", parse_mode: "Markdown", link_preview_options: { is_disabled: true } });
1045
+ await bot.api.sendMessageDraft({ chat_id: 42, draft_id: 1, text: "partial…" }); // streaming draft
1046
+ await bot.api.sendRichMessage({ chat_id, rich_message: RichMessage.markdown().text("hi").build() });
1047
+ await bot.api.sendRichMessageDraft({ chat_id: 42, draft_id: 1, rich_message: RichMessage.markdown().text("…").build() });
1048
+
1049
+ await bot.api.forwardMessage({ chat_id, from_chat_id: -100, message_id: 5 });
1050
+ await bot.api.forwardMessages({ chat_id, from_chat_id: -100, message_ids: [5, 6] });
1051
+ await bot.api.copyMessage({ chat_id, from_chat_id: -100, message_id: 5 });
1052
+ await bot.api.copyMessages({ chat_id, from_chat_id: -100, message_ids: [5, 6] });
1053
+
1054
+ await bot.api.sendPhoto({ chat_id, photo: { source: "./p.jpg" }, caption: "hi", has_spoiler: true });
1055
+ await bot.api.sendLivePhoto({ chat_id, live_photo: { source: "./lp.jpg" } });
1056
+ await bot.api.sendAudio({ chat_id, audio: { source: "./a.mp3" }, performer: "X", title: "Y" });
1057
+ await bot.api.sendDocument({ chat_id, document: { source: "./d.pdf" } });
1058
+ await bot.api.sendVideo({ chat_id, video: { source: "./v.mp4" }, supports_streaming: true });
1059
+ await bot.api.sendAnimation({ chat_id, animation: { source: "./l.gif" } });
1060
+ await bot.api.sendSticker({ chat_id, sticker: { source: "./s.webp" }, emoji: "🙂" });
1061
+ await bot.api.sendVoice({ chat_id, voice: { source: "./v.ogg" } });
1062
+ await bot.api.sendVideoNote({ chat_id, video_note: { source: "./note.mp4" } });
1063
+ await bot.api.sendMediaGroup({ chat_id, media: [{ type: "photo", media: "https://x.dev/1.jpg" }] });
1064
+ await bot.api.sendPaidMedia({ chat_id, star_count: 10, media: [{ type: "photo", media: "https://x.dev/p.jpg" }] });
1065
+
1066
+ await bot.api.sendLocation({ chat_id, latitude: 51.5, longitude: -0.12 });
1067
+ await bot.api.editMessageLiveLocation({ chat_id, message_id: 5, latitude: 51.6, longitude: -0.13 });
1068
+ await bot.api.stopMessageLiveLocation({ chat_id, message_id: 5 });
1069
+ await bot.api.sendVenue({ chat_id, latitude: 51.5, longitude: -0.12, title: "Office", address: "1 Main St" });
1070
+ await bot.api.sendContact({ chat_id, phone_number: "+15551234", first_name: "Ada" });
1071
+
1072
+ await bot.api.sendPoll({ chat_id, question: "Pizza?", options: ["Yes", "No"], is_anonymous: false });
1073
+ await bot.api.sendQuiz({ chat_id, question: "2+2?", options: ["3", "4"], correct_option_ids: [1] });
1074
+ await bot.api.sendDice({ chat_id, emoji: "🎲" });
1075
+ await bot.api.sendChatAction({ chat_id, action: "typing" });
1076
+ ```
1077
+
1078
+ `sendMessage`, `sendMessageDraft`, `sendRichMessage`, `sendRichMessageDraft`, `forwardMessage`, `forwardMessages`, `copyMessage`, `copyMessages`, `sendPhoto`, `sendLivePhoto`, `sendAudio`, `sendDocument`, `sendVideo`, `sendAnimation`, `sendSticker`, `sendVoice`, `sendVideoNote`, `sendMediaGroup`, `sendPaidMedia`, `sendLocation`, `editMessageLiveLocation`, `stopMessageLiveLocation`, `sendVenue`, `sendContact`, `sendPoll`, `sendQuiz`, `sendDice`, `sendChatAction`
1079
+
1080
+ ### Editing / deleting (7)
1081
+
1082
+ ```typescript
1083
+ await bot.api.editMessageText({ chat_id, message_id: 5, text: "edited", reply_markup: Keyboard.inline().button("x", "x").build() });
1084
+ await bot.api.editMessageCaption({ chat_id, message_id: 5, caption: "new caption" });
1085
+ await bot.api.editMessageMedia({ chat_id, message_id: 5, media: { type: "photo", media: "https://x.dev/new.jpg" } });
1086
+ await bot.api.editMessageReplyMarkup({ chat_id, message_id: 5, reply_markup: undefined });
1087
+ await bot.api.stopPoll({ chat_id, message_id: 5 });
1088
+ await bot.api.deleteMessage({ chat_id, message_id: 5 });
1089
+ await bot.api.deleteMessages({ chat_id, message_ids: [5, 6, 7] });
1090
+ ```
1091
+
1092
+ `editMessageText`, `editMessageCaption`, `editMessageMedia`, `editMessageReplyMarkup`, `stopPoll`, `deleteMessage`, `deleteMessages`
1093
+
1094
+ ### Ephemeral Messages (Bot API 10.2) (5)
1095
+
1096
+ ```typescript
1097
+ // Send an ephemeral message (visible only to one user) via any send method
1098
+ await bot.api.sendMessage({ chat_id, text: "only you see this", ephemeral_message_parameters: { receiver_user_id: 42 } as any });
1099
+
1100
+ await bot.api.editEphemeralMessageText({ receiver_user_id: 42, chat_id, ephemeral_message_id: 5, text: "edited" });
1101
+ await bot.api.editEphemeralMessageMedia({ receiver_user_id: 42, chat_id, ephemeral_message_id: 5, media: { type: "photo", media: "https://x.dev/p.jpg" } });
1102
+ await bot.api.editEphemeralMessageCaption({ receiver_user_id: 42, chat_id, ephemeral_message_id: 5, caption: "cap" });
1103
+ await bot.api.editEphemeralMessageReplyMarkup({ receiver_user_id: 42, chat_id, ephemeral_message_id: 5, reply_markup: undefined });
1104
+ await bot.api.deleteEphemeralMessage({ receiver_user_id: 42, chat_id, ephemeral_message_id: 5 });
1105
+ ```
1106
+
1107
+ `editEphemeralMessageText`, `editEphemeralMessageMedia`, `editEphemeralMessageCaption`, `editEphemeralMessageReplyMarkup`, `deleteEphemeralMessage`
1108
+
1109
+ ### Files (3)
1110
+
1111
+ ```typescript
1112
+ const file = await bot.api.getFile({ file_id: "AgAC…" });
1113
+ const url = await bot.api.getFileLink(file); // or pass the file_id directly
1114
+ await bot.api.getUserProfilePhotos({ user_id: 42, limit: 10 });
1115
+ await bot.api.getUserProfileAudios({ user_id: 42, limit: 10 });
1116
+ ```
1117
+
1118
+ `getFile`, `getFileLink`, `getUserProfilePhotos`, `getUserProfileAudios`
1119
+
1120
+ ### Chat administration (35)
1121
+
1122
+ ```typescript
1123
+ await bot.api.banChatMember({ chat_id, user_id: 42, revoke_messages: true });
1124
+ await bot.api.unbanChatMember({ chat_id, user_id: 42 });
1125
+ await bot.api.restrictChatMember({ chat_id, user_id: 42, permissions: { can_send_messages: false } });
1126
+ await bot.api.promoteChatMember({ chat_id, user_id: 42, can_delete_messages: true, can_send_welcome_messages: true });
1127
+ await bot.api.setChatAdministratorCustomTitle({ chat_id, user_id: 42, custom_title: "Moderator" });
1128
+ await bot.api.setChatMemberTag({ chat_id, user_id: 42, tag: "vip" });
1129
+ await bot.api.banChatSenderChat({ chat_id, sender_chat_id: -1009 });
1130
+ await bot.api.unbanChatSenderChat({ chat_id, sender_chat_id: -1009 });
1131
+ await bot.api.setChatPermissions({ chat_id, permissions: { can_send_messages: false } });
1132
+
1133
+ const link = await bot.api.exportChatInviteLink({ chat_id });
1134
+ await bot.api.createChatInviteLink({ chat_id, name: "campaign", member_limit: 100 });
1135
+ await bot.api.editChatInviteLink({ chat_id, invite_link: link, name: "renamed" });
1136
+ await bot.api.revokeChatInviteLink({ chat_id, invite_link: link });
1137
+ await bot.api.approveChatJoinRequest({ chat_id, user_id: 42 });
1138
+ await bot.api.declineChatJoinRequest({ chat_id, user_id: 42 });
1139
+ await bot.api.answerChatJoinRequestQuery({ query_id: "q", approve: true });
1140
+ await bot.api.sendChatJoinRequestWebApp({ query_id: "q", web_app: { url: "https://x.dev/app" } });
1141
+
1142
+ await bot.api.setChatPhoto({ chat_id, photo: { source: "./icon.png" } });
1143
+ await bot.api.deleteChatPhoto({ chat_id });
1144
+ await bot.api.setChatTitle({ chat_id, title: "New title" });
1145
+ await bot.api.setChatDescription({ chat_id, description: "New description" });
1146
+ await bot.api.pinChatMessage({ chat_id, message_id: 5 });
1147
+ await bot.api.unpinChatMessage({ chat_id, message_id: 5 });
1148
+ await bot.api.unpinAllChatMessages({ chat_id });
1149
+ await bot.api.leaveChat({ chat_id });
1150
+
1151
+ const chat = await bot.api.getChat({ chat_id });
1152
+ const admins = await bot.api.getChatAdministrators({ chat_id, return_bots: true });
1153
+ const count = await bot.api.getChatMemberCount({ chat_id });
1154
+ const member = await bot.api.getChatMember({ chat_id, user_id: 42 });
1155
+ await bot.api.setChatStickerSet({ chat_id, sticker_set_name: "my_set" });
1156
+ await bot.api.deleteChatStickerSet({ chat_id });
1157
+ await bot.api.setMessageReaction({ chat_id, message_id: 5, reaction: [{ type: "emoji", emoji: "👍" }] });
1158
+ await bot.api.deleteAllMessageReactions({ chat_id, message_id: 5 });
1159
+ await bot.api.deleteMessageReaction({ chat_id, message_id: 5, user_id: 42 });
1160
+ await bot.api.getUserChatBoosts({ chat_id, user_id: 42 });
1161
+ ```
1162
+
1163
+ `banChatMember`, `unbanChatMember`, `restrictChatMember`, `promoteChatMember`, `setChatAdministratorCustomTitle`, `setChatMemberTag`, `banChatSenderChat`, `unbanChatSenderChat`, `setChatPermissions`, `exportChatInviteLink`, `createChatInviteLink`, `editChatInviteLink`, `revokeChatInviteLink`, `approveChatJoinRequest`, `declineChatJoinRequest`, `answerChatJoinRequestQuery`, `sendChatJoinRequestWebApp`, `setChatPhoto`, `deleteChatPhoto`, `setChatTitle`, `setChatDescription`, `pinChatMessage`, `unpinChatMessage`, `unpinAllChatMessages`, `leaveChat`, `getChat`, `getChatAdministrators`, `getChatMemberCount`, `getChatMember`, `setChatStickerSet`, `deleteChatStickerSet`, `setMessageReaction`, `deleteAllMessageReactions`, `deleteMessageReaction`, `getUserChatBoosts`
1164
+
1165
+ ### Forum topics (13)
1166
+
1167
+ ```typescript
1168
+ const icons = await bot.api.getForumTopicIconStickers();
1169
+ const topic = await bot.api.createForumTopic({ chat_id, name: "Support", icon_color: 0x6FB9F0 });
1170
+ await bot.api.editForumTopic({ chat_id, message_thread_id: topic.message_thread_id, name: "Renamed" });
1171
+ await bot.api.closeForumTopic({ chat_id, message_thread_id: topic.message_thread_id });
1172
+ await bot.api.reopenForumTopic({ chat_id, message_thread_id: topic.message_thread_id });
1173
+ await bot.api.deleteForumTopic({ chat_id, message_thread_id: topic.message_thread_id });
1174
+ await bot.api.unpinAllForumTopicMessages({ chat_id, message_thread_id: topic.message_thread_id });
1175
+
1176
+ await bot.api.editGeneralForumTopic({ chat_id, name: "General renamed" });
1177
+ await bot.api.closeGeneralForumTopic({ chat_id });
1178
+ await bot.api.reopenGeneralForumTopic({ chat_id });
1179
+ await bot.api.hideGeneralForumTopic({ chat_id });
1180
+ await bot.api.unhideGeneralForumTopic({ chat_id });
1181
+ await bot.api.unpinAllGeneralForumTopicMessages({ chat_id });
1182
+ ```
1183
+
1184
+ `getForumTopicIconStickers`, `createForumTopic`, `editForumTopic`, `closeForumTopic`, `reopenForumTopic`, `deleteForumTopic`, `unpinAllForumTopicMessages`, `editGeneralForumTopic`, `closeGeneralForumTopic`, `reopenGeneralForumTopic`, `hideGeneralForumTopic`, `unhideGeneralForumTopic`, `unpinAllGeneralForumTopicMessages`
1185
+
1186
+ ### Bot profile / commands / menu (17)
1187
+
1188
+ ```typescript
1189
+ await bot.api.setMyCommands({ commands: [{ command: "start", description: "Start" }], scope: { type: "default" }, language_code: "en" });
1190
+ const commands = await bot.api.getMyCommands();
1191
+ await bot.api.deleteMyCommands();
1192
+
1193
+ await bot.api.setChatMenuButton({ menu_button: { type: "commands" } });
1194
+ await bot.api.getChatMenuButton();
1195
+ await bot.api.setMyDefaultAdministratorRights({ rights: { can_delete_messages: true }, for_channels: false });
1196
+ await bot.api.getMyDefaultAdministratorRights();
1197
+
1198
+ await bot.api.setMyName({ name: "My Bot" });
1199
+ await bot.api.getMyName();
1200
+ await bot.api.setMyDescription({ description: "A helpful bot." });
1201
+ await bot.api.getMyDescription();
1202
+ await bot.api.setMyShortDescription({ short_description: "Helpful." });
1203
+ await bot.api.getMyShortDescription();
1204
+ await bot.api.setMyProfilePhoto({ photo: { source: "./avatar.png" } });
1205
+ await bot.api.removeMyProfilePhoto();
1206
+ await bot.api.setUserEmojiStatus({ user_id: 42, emoji_status_custom_emoji_id: "5368…" });
1207
+ await bot.api.setPassportDataErrors({ user_id: 42, errors: [] });
1208
+ ```
1209
+
1210
+ `setMyCommands`, `deleteMyCommands`, `getMyCommands`, `setChatMenuButton`, `getChatMenuButton`, `setMyDefaultAdministratorRights`, `getMyDefaultAdministratorRights`, `setMyName`, `getMyName`, `setMyDescription`, `getMyDescription`, `setMyShortDescription`, `getMyShortDescription`, `setMyProfilePhoto`, `removeMyProfilePhoto`, `setUserEmojiStatus`, `setPassportDataErrors`
1211
+
1212
+ ### Callback / inline queries (6)
1213
+
1214
+ ```typescript
1215
+ await bot.api.answerCallbackQuery({ callback_query_id: "cb1", text: "Done", show_alert: false });
1216
+ await bot.api.answerGameQuery({ callback_query_id: "cb1", url: "https://game.example" });
1217
+ await bot.api.answerInlineQuery({ inline_query_id: "iq1", results: [], cache_time: 5, is_personal: true });
1218
+ await bot.api.answerWebAppQuery({ web_app_query_id: "wa1", result: { type: "article", id: "1", title: "T", input_message_content: { message_text: "hi" } } });
1219
+ await bot.api.savePreparedInlineMessage({ user_id: 42, result: { type: "article", id: "1", title: "T", input_message_content: { message_text: "hi" } } });
1220
+ await bot.api.savePreparedKeyboardButton({ user_id: 42, button: { text: "Create bot" } as any });
1221
+ ```
1222
+
1223
+ `answerCallbackQuery`, `answerGameQuery`, `answerInlineQuery`, `answerWebAppQuery`, `savePreparedInlineMessage`, `savePreparedKeyboardButton`
1224
+
1225
+ ### Payments (4)
1226
+
1227
+ ```typescript
1228
+ await bot.api.sendInvoice({
1229
+ chat_id, title: "Pro", description: "Monthly", payload: "order-1", currency: "USD",
1230
+ prices: [{ label: "Pro", amount: 500 }],
1231
+ });
1232
+ const invoiceLink = await bot.api.createInvoiceLink({ title: "Pro", description: "Monthly", payload: "order-1", currency: "USD", prices: [{ label: "Pro", amount: 500 }] });
1233
+ await bot.api.answerShippingQuery({ shipping_query_id: "sq1", ok: true });
1234
+ await bot.api.answerPreCheckoutQuery({ pre_checkout_query_id: "pc1", ok: true });
1235
+ ```
1236
+
1237
+ `sendInvoice`, `createInvoiceLink`, `answerShippingQuery`, `answerPreCheckoutQuery`
1238
+
1239
+ ### Stickers (14)
1240
+
1241
+ ```typescript
1242
+ const set = await bot.api.getStickerSet({ name: "my_set" });
1243
+ await bot.api.getCustomEmojiStickers({ custom_emoji_ids: ["5368…"] });
1244
+ const uploaded = await bot.api.uploadStickerFile({ user_id: 42, sticker: { source: "./s.png" }, sticker_format: "static" });
1245
+ await bot.api.createNewStickerSet({ user_id: 42, name: "my_set_by_bot", title: "My Set", stickers: [{ sticker: uploaded.file_id, format: "static", emoji_list: ["🙂"] }] });
1246
+ await bot.api.addStickerToSet({ user_id: 42, name: "my_set_by_bot", sticker: { sticker: "file_id", format: "static", emoji_list: ["🙂"] } });
1247
+ await bot.api.setStickerPositionInSet({ sticker: "file_id", position: 0 });
1248
+ await bot.api.deleteStickerFromSet({ sticker: "file_id" });
1249
+ await bot.api.setStickerEmojiList({ sticker: "file_id", emoji_list: ["🙂"] });
1250
+ await bot.api.setStickerKeywords({ sticker: "file_id", keywords: ["happy"] });
1251
+ await bot.api.setStickerMaskPosition({ sticker: "file_id" });
1252
+ await bot.api.setStickerSetTitle({ name: "my_set_by_bot", title: "Renamed" });
1253
+ await bot.api.setStickerSetThumbnail({ name: "my_set_by_bot", user_id: 42, format: "static" });
1254
+ await bot.api.setCustomEmojiStickerSetThumbnail({ name: "my_set_by_bot", custom_emoji_id: "5368…" });
1255
+ await bot.api.deleteStickerSet({ name: "my_set_by_bot" });
1256
+ ```
1257
+
1258
+ `getStickerSet`, `getCustomEmojiStickers`, `uploadStickerFile`, `createNewStickerSet`, `addStickerToSet`, `setStickerPositionInSet`, `deleteStickerFromSet`, `setStickerEmojiList`, `setStickerKeywords`, `setStickerMaskPosition`, `setStickerSetTitle`, `setStickerSetThumbnail`, `setCustomEmojiStickerSetThumbnail`, `deleteStickerSet`
1259
+
1260
+ ### Games (3)
1261
+
1262
+ ```typescript
1263
+ await bot.api.sendGame({ chat_id, game_short_name: "my_game" });
1264
+ await bot.api.setGameScore({ user_id: 42, score: 9001, chat_id, message_id: 5 });
1265
+ const scores = await bot.api.getGameHighScores({ user_id: 42, chat_id, message_id: 5 });
1266
+ ```
1267
+
1268
+ `sendGame`, `setGameScore`, `getGameHighScores`
1269
+
1270
+ ### Checklists (Bot API 9.1) (2)
1271
+
1272
+ ```typescript
1273
+ // Checklists are sent/edited on behalf of a business account only
1274
+ await bot.api.sendChecklist({ business_connection_id: "bc1", chat_id, checklist: { title: "Todo", tasks: [] } as any });
1275
+ await bot.api.editMessageChecklist({ business_connection_id: "bc1", chat_id, message_id: 5, checklist: { title: "Todo", tasks: [] } as any });
1276
+ ```
1277
+
1278
+ `sendChecklist`, `editMessageChecklist`
1279
+
1280
+ ### Business accounts (21)
1281
+
1282
+ ```typescript
1283
+ const conn = await bot.api.getBusinessConnection({ business_connection_id: "bc1" });
1284
+ await bot.api.readBusinessMessage({ business_connection_id: "bc1", chat_id, message_id: 5 });
1285
+ await bot.api.deleteBusinessMessages({ business_connection_id: "bc1", message_ids: [5] });
1286
+ await bot.api.setBusinessAccountName({ business_connection_id: "bc1", first_name: "Shop" });
1287
+ await bot.api.setBusinessAccountUsername({ business_connection_id: "bc1", username: "myshop" });
1288
+ await bot.api.setBusinessAccountBio({ business_connection_id: "bc1", bio: "We sell things." });
1289
+ await bot.api.setBusinessAccountProfilePhoto({ business_connection_id: "bc1", photo: { source: "./logo.png" } });
1290
+ await bot.api.removeBusinessAccountProfilePhoto({ business_connection_id: "bc1" });
1291
+ await bot.api.setBusinessAccountGiftSettings({ business_connection_id: "bc1", show_gift_button: true, accepted_gift_types: {} as any });
1292
+ const balance = await bot.api.getBusinessAccountStarBalance({ business_connection_id: "bc1" });
1293
+ await bot.api.transferBusinessAccountStars({ business_connection_id: "bc1", star_count: 100 });
1294
+ const gifts = await bot.api.getBusinessAccountGifts({ business_connection_id: "bc1", limit: 20 });
1295
+ await bot.api.getUserGifts({ user_id: 42 });
1296
+ await bot.api.getChatGifts({ chat_id });
1297
+ await bot.api.convertGiftToStars({ business_connection_id: "bc1", owned_gift_id: "g1" });
1298
+ await bot.api.upgradeGift({ business_connection_id: "bc1", owned_gift_id: "g1", star_count: 50 });
1299
+ await bot.api.transferGift({ business_connection_id: "bc1", owned_gift_id: "g1", new_owner_chat_id: 43 });
1300
+ const story = await bot.api.postStory({ business_connection_id: "bc1", content: { type: "photo", photo: "https://x.dev/s.jpg" } as any, active_period: 86400 });
1301
+ await bot.api.editStory({ business_connection_id: "bc1", story_id: story.id, content: { type: "photo", photo: "https://x.dev/s2.jpg" } as any });
1302
+ await bot.api.deleteStory({ business_connection_id: "bc1", story_id: story.id });
1303
+ await bot.api.repostStory({ business_connection_id: "bc1", story_id: story.id });
1304
+ ```
1305
+
1306
+ `getBusinessConnection`, `readBusinessMessage`, `deleteBusinessMessages`, `setBusinessAccountName`, `setBusinessAccountUsername`, `setBusinessAccountBio`, `setBusinessAccountProfilePhoto`, `removeBusinessAccountProfilePhoto`, `setBusinessAccountGiftSettings`, `getBusinessAccountStarBalance`, `transferBusinessAccountStars`, `getBusinessAccountGifts`, `getUserGifts`, `getChatGifts`, `convertGiftToStars`, `upgradeGift`, `transferGift`, `postStory`, `editStory`, `deleteStory`, `repostStory`
1307
+
1308
+ ### Verification (Bot API 8.2+) (4)
1309
+
1310
+ ```typescript
1311
+ await bot.api.verifyUser({ user_id: 42, custom_description: "Verified member" });
1312
+ await bot.api.verifyChat({ chat_id, custom_description: "Official" });
1313
+ await bot.api.removeUserVerification({ user_id: 42 });
1314
+ await bot.api.removeChatVerification({ chat_id });
1315
+ ```
1316
+
1317
+ `verifyUser`, `verifyChat`, `removeUserVerification`, `removeChatVerification`
1318
+
1319
+ ### Gifts (Bot API 8.0+) (3)
1320
+
1321
+ ```typescript
1322
+ const available = await bot.api.getAvailableGifts();
1323
+ await bot.api.sendGift({ gift_id: "gift-1", user_id: 42, text: "Enjoy!" });
1324
+ await bot.api.giftPremiumSubscription({ user_id: 42, month_count: 3, star_count: 100 });
1325
+ ```
1326
+
1327
+ `getAvailableGifts`, `sendGift`, `giftPremiumSubscription`
1328
+
1329
+ ### Stars & Payments transactions (Bot API 7.4+) (4)
1330
+
1331
+ ```typescript
1332
+ const txns = await bot.api.getStarTransactions({ limit: 50 });
1333
+ const mine = await bot.api.getMyStarBalance();
1334
+ await bot.api.refundStarPayment({ user_id: 42, telegram_payment_charge_id: "charge-1" });
1335
+ await bot.api.editUserStarSubscription({ user_id: 42, telegram_payment_charge_id: "charge-1", is_canceled: true });
1336
+ ```
1337
+
1338
+ `getStarTransactions`, `getMyStarBalance`, `refundStarPayment`, `editUserStarSubscription`
1339
+
1340
+ ### Suggested Posts / Direct Messages in Channels (Bot API 9.2+) (2)
1341
+
1342
+ ```typescript
1343
+ // Turn a send into a suggested post via sendMessage's suggested_post_parameters, then approve/decline
1344
+ await bot.api.sendMessage({ chat_id, text: "Draft post", suggested_post_parameters: { send_date: 0 } as any });
1345
+ await bot.api.approveSuggestedPost({ chat_id, message_id: 5 });
1346
+ await bot.api.declineSuggestedPost({ chat_id, message_id: 5, comment: "Not now" });
1347
+ ```
1348
+
1349
+ `approveSuggestedPost`, `declineSuggestedPost`
1350
+
1351
+ ### Guest mode (Bot API 10.0+) (1)
1352
+
1353
+ ```typescript
1354
+ // Respond to a guest_message update (bots mentioned/queried with no direct relationship)
1355
+ await bot.api.answerGuestQuery({ guest_query_id: "gq1", result: { type: "article", id: "1", title: "T", input_message_content: { message_text: "hi" } } });
1356
+ ```
1357
+
1358
+ `answerGuestQuery`
1359
+
1360
+ ### Managed Bots (Bot API 9.5/9.6) (5)
1361
+
1362
+ ```typescript
1363
+ const { token } = await bot.api.getManagedBotToken({ bot_id: 123 });
1364
+ const replaced = await bot.api.replaceManagedBotToken({ bot_id: 123 });
1365
+ const settings = await bot.api.getManagedBotAccessSettings({ bot_id: 123 });
1366
+ await bot.api.setManagedBotAccessSettings({ bot_id: 123, access_settings: settings });
1367
+ const personal = await bot.api.getUserPersonalChatMessages({ user_id: 42, limit: 20 });
1368
+ ```
1369
+
1370
+ `getManagedBotToken`, `replaceManagedBotToken`, `getManagedBotAccessSettings`, `setManagedBotAccessSettings`, `getUserPersonalChatMessages`
1371
+
1372
+ ---
1373
+
1374
+ ## Coverage Confidence
1375
+
1376
+ Honest, area-by-area — so you know where to double-check before relying on something:
1377
+
1378
+ | Area | Confidence | How it was verified |
1379
+ |---|---|---|
1380
+ | Core messaging, chat admin, forum topics, stickers, games, payments | High | Diffed against telegraf's published method list |
1381
+ | Inline mode (`InlineQueryResult` — all 20 variants), Telegram Passport | High | Field-by-field against python-telegram-bot's generated docs |
1382
+ | `ReplyParameters`, polls, reactions, chat boosts | High | Cross-checked directly against the official Bot API changelog |
1383
+ | Rich Messages (`RichBlock`/`RichText` — 21 + 25 variants) | High | Field-by-field against a community reference + the official changelog |
1384
+ | Ephemeral Messages, Communities (Bot API 10.2/10.3) | High | Verified against the official changelog the same day it shipped |
1385
+ | Gifts, Stars transactions, Business accounts | High | Field-by-field against python-telegram-bot's generated docs |
1386
+ | `ChatBackground`/`BackgroundType`/`BackgroundFill` | High | Stable schema, cross-checked against generated docs |
1387
+ | Checklists, Guest mode, Suggested Posts | Medium | Top-level shapes confirmed via changelog; some nested fields inferred |
1388
+ | `StoryArea`/`InputStoryContent` (Business Stories) | Medium-low | General structure confirmed via changelog mentions only |
1389
+ | `sendMessageDraft` (plain-text streaming) | Medium | Implemented from its changelog entry; parameter shape modeled on the field-verified `sendRichMessageDraft` |
1390
+ | `UniqueGiftColors`, `GiftBackground`, `UserRating` | Low, explicitly | Confirmed to exist; field names inferred from a sibling type's pattern, not verified. Flagged in `types.ts`'s JSDoc. |
1391
+ | `getManagedBotToken`/`replaceManagedBotToken`, `answerGuestQuery` | Lower | Implemented from the changelog alone, not cross-checked against a second source |
1392
+
1393
+ Every method's parameters and return values are backed by a named, field-checked type — the remaining `Record<string, unknown>` occurrences in `client.ts` are internal transport plumbing (multipart encoding), not API surface. The auto-generated reference at `docs/api-reference.html` is parsed straight out of `src/client.ts` (`npm run docs:api`), so it cannot silently drift.
1394
+
1395
+ ---
1396
+
1397
+ ## Project Layout
1398
+
1399
+ ```
1400
+ src/
1401
+ types.ts Telegram Bot API type definitions (Bot API 7.0-10.3)
1402
+ client.ts Transport (HTTP + multipart + flood control) + typed API surface (187 methods)
1403
+ context.ts Context — fluent ctx.reply()/ctx.editText()/etc.
1404
+ composer.ts Middleware engine: use/on/command/hears/action/branch/lazy/Router
1405
+ keyboard.ts Fluent inline/reply keyboard builders
1406
+ richMessage.ts Rich Message builders — markdown/html text or structured blocks (10.3)
1407
+ decorators.ts @Command/@Hears/@Action/@On class decorators
1408
+ session.ts session() middleware + MemorySessionStore
1409
+ session-redis.ts RedisSessionStore (duck-typed against any Redis client)
1410
+ session-sqlite.ts SqliteSessionStore (duck-typed against better-sqlite3)
1411
+ scenes.ts WizardScene + Stage — multi-step conversations
1412
+ rateLimit.ts Per-chat/user rate limiting middleware
1413
+ i18n.ts Minimal i18n (translate by Telegram language_code)
1414
+ filters.ts Entity & reaction filters (mention(), hashtag(), reaction(), …)
1415
+ adapters.ts Vercel / Lambda / Cloudflare Workers webhook handlers
1416
+ bot.ts Bot — polling, webhook, ties it all together
1417
+ examples/
1418
+ basic.ts router style
1419
+ fluent.ts keyboard/rich-message + webhook example
1420
+ decorators.ts class-based style
1421
+ scenes.ts multi-step signup flow
1422
+ pterodactyl-bot.ts real-world example: a Pterodactyl panel control bot
1423
+ test/
1424
+ *.test.ts 106 tests — Composer, Context, Scenes, Keyboard, RichMessage,
1425
+ i18n, rateLimit, adapters, decorators, Transport flood control
1426
+ decorator-smoke/ real @Command syntax, compiled by real tsc, run with plain node
1427
+ smoke.plain-js.test.js plain-require() proof the compiled output works with zero TS
1428
+ docs/
1429
+ index.html full documentation site
1430
+ api-reference.html auto-generated API reference (npm run docs:api)
1431
+ ```
1432
+
1433
+ ---
1434
+
1435
+ ## Testing
1436
+
1437
+ ```bash
1438
+ npm test
1439
+ ```
1440
+
1441
+ Runs, in order: a real `tsc` build to `dist/`, 106 unit/integration tests via Node's
1442
+ built-in test runner, a plain-JavaScript smoke test proving the compiled output
1443
+ works with zero TypeScript involved, and a real-decorator-syntax smoke test
1444
+ (isolated `tsc` compile + plain `node`, proving legacy decorators work end-to-end
1445
+ for real consumers — `tsx`, used for the fast test suite, doesn't honor
1446
+ `experimentalDecorators`, which is a test-runner quirk, not a library bug; see
1447
+ `test/decorators.test.ts`'s comment for the full explanation).
1448
+
1449
+ ```bash
1450
+ npm run docs:api # regenerate docs/api-reference.html from src/client.ts
1451
+ ```
1452
+
1453
+ **Not covered by `npm test`:** an actual live run against Telegram's real servers.
1454
+ See [Status](#status).
1455
+
1456
+ ---
1457
+
1458
+ ## Status
1459
+
1460
+ ### Verified
1461
+ - Full build pipeline (`tsc` → 106 tests → JS smoke test → real-decorator smoke test)
1462
+ - Core method coverage diffed against telegraf v4's actual published method list
1463
+ - Newer areas verified against the official changelog directly
1464
+ - **Live-tested against real Telegram servers**: `getMe()`, `getWebhookInfo()`, and the
1465
+ core happy path (commands, inline keyboards, callback queries, `hears()` matching,
1466
+ text echo, photo receipt) confirmed working end-to-end via real long polling
1467
+ - Three real bugs found and fixed by porting an actual working bot into tgplus
1468
+ (see `CHANGELOG.md`'s 0.2.0 entry) — `ctx.editText()`/`ctx.deleteMessage()` from
1469
+ callback_query handlers, and `ctx.chat`/`ctx.from` missing several update types
1470
+
1471
+ ### Not yet verified
1472
+ - Webhook mode (code is there, not live-tested)
1473
+ - Scenes/sessions in a live multi-turn conversation
1474
+ - File uploads (sending a Buffer via `sendPhoto`/`sendDocument`)
1475
+ - Streaming drafts (`sendMessageDraft`/`sendRichMessageDraft`) against a real server
1476
+ - Newer/lower-confidence feature areas (Rich Messages, Ephemeral Messages, Gifts,
1477
+ Stars, Business accounts) — sent to mocked `fetch` in tests, never a real server
1478
+ - Production mileage or community — telegraf and aiogram have years of real-world
1479
+ edge cases already found and fixed; that only comes from real usage over time,
1480
+ and can't be written in ahead of time
1481
+
1482
+ CI runs the full suite on every push (Node 18/20/22) — see `.github/workflows/test.yml`.
1483
+
1484
+ ---
1485
+
1486
+ ## Contributing
1487
+
1488
+ Issues and PRs welcome. Given how new this project is, the most valuable
1489
+ contribution right now is simply **using it and reporting what breaks** —
1490
+ see "Not yet verified" above for where the real risk currently is.
1491
+
1492
+ 1. Fork the repository
1493
+ 2. Create a feature branch (`git checkout -b feature/thing`)
1494
+ 3. `npm test` before committing
1495
+ 4. Open a PR
1496
+
1497
+ ---
1498
+
1499
+ ## Credits
1500
+
1501
+ tgplus is developed and maintained by **[@crysnovax](https://github.com/crysnovax)**.
1502
+
1503
+ tgplus builds on the original work of **[@ayo](https://github.com/AyoFemi10)** —
1504
+ all credit for the original design and implementation belongs to @ayo.
1505
+
1506
+ ## License
1507
+
1508
+ **Proprietary — All Rights Reserved.** See [LICENSE](LICENSE).
1509
+
1510
+ This package is not open source. No permission is granted to use, reproduce,
1511
+ distribute, modify, or commercially exploit this software, in whole or in part,
1512
+ without prior written permission from the author.