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.
- package/LICENSE +52 -0
- package/README.md +1512 -0
- package/dist/adapters.d.ts +46 -0
- package/dist/adapters.js +83 -0
- package/dist/adapters.js.map +1 -0
- package/dist/bot.d.ts +50 -0
- package/dist/bot.js +180 -0
- package/dist/bot.js.map +1 -0
- package/dist/client.d.ts +1139 -0
- package/dist/client.js +529 -0
- package/dist/client.js.map +1 -0
- package/dist/composer.d.ts +88 -0
- package/dist/composer.js +233 -0
- package/dist/composer.js.map +1 -0
- package/dist/context.d.ts +72 -0
- package/dist/context.js +149 -0
- package/dist/context.js.map +1 -0
- package/dist/decorators.d.ts +33 -0
- package/dist/decorators.js +39 -0
- package/dist/decorators.js.map +1 -0
- package/dist/filters.d.ts +19 -0
- package/dist/filters.js +81 -0
- package/dist/filters.js.map +1 -0
- package/dist/i18n.d.ts +30 -0
- package/dist/i18n.js +41 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/keyboard.d.ts +62 -0
- package/dist/keyboard.js +96 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/rateLimit.d.ts +19 -0
- package/dist/rateLimit.js +32 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/richMessage.d.ts +111 -0
- package/dist/richMessage.js +221 -0
- package/dist/richMessage.js.map +1 -0
- package/dist/scenes.d.ts +57 -0
- package/dist/scenes.js +105 -0
- package/dist/scenes.js.map +1 -0
- package/dist/session-redis.d.ts +27 -0
- package/dist/session-redis.js +36 -0
- package/dist/session-redis.js.map +1 -0
- package/dist/session-sqlite.d.ts +29 -0
- package/dist/session-sqlite.js +34 -0
- package/dist/session-sqlite.js.map +1 -0
- package/dist/session.d.ts +30 -0
- package/dist/session.js +35 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +2485 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- 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.
|