@xbibzlibrary/telebibz 0.4.4 → 0.4.5
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/README.id.md +14 -0
- package/README.md +14 -0
- package/README.zh-CN.md +14 -0
- package/docs/COOKBOOK.id.md +321 -0
- package/docs/COOKBOOK.md +321 -0
- package/docs/COOKBOOK.zh-CN.md +321 -0
- package/docs/ERRORS.id.md +194 -0
- package/docs/ERRORS.md +194 -0
- package/docs/ERRORS.zh-CN.md +194 -0
- package/docs/FILES.id.md +243 -0
- package/docs/FILES.md +243 -0
- package/docs/FILES.zh-CN.md +243 -0
- package/docs/GETTING_STARTED.id.md +6 -2
- package/docs/GETTING_STARTED.md +6 -2
- package/docs/GETTING_STARTED.zh-CN.md +6 -2
- package/docs/MIGRATION_TELEGRAF.id.md +147 -0
- package/docs/MIGRATION_TELEGRAF.md +154 -0
- package/docs/MIGRATION_TELEGRAF.zh-CN.md +147 -0
- package/docs/README.md +38 -19
- package/docs/TESTING.id.md +203 -0
- package/docs/TESTING.md +203 -0
- package/docs/TESTING.zh-CN.md +203 -0
- package/docs/WEBHOOK.id.md +212 -0
- package/docs/WEBHOOK.md +215 -0
- package/docs/WEBHOOK.zh-CN.md +212 -0
- package/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# 文件操作:上传、下载、校验(简体中文)
|
|
2
|
+
|
|
3
|
+
telebibz 文件流程的完整指南:下载用户发来的文件、上传文件到 Telegram、通过 `file_id` 转发、上传前校验,以及 Telegram Bot API 的限制与陷阱。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [下载:一次调用 `downloadFile()`](#1-下载一次调用-downloadfile)
|
|
8
|
+
2. [下载:`getFile()` 手动流程](#2-下载getfile-手动流程)
|
|
9
|
+
3. [属性命名:`file_path` 与 `filePath`](#3-属性命名file_path-与-filepath)
|
|
10
|
+
4. [上传:所有来源类型](#4-上传所有来源类型)
|
|
11
|
+
5. [上传:发送前校验](#5-上传发送前校验)
|
|
12
|
+
6. [媒体组与 `attach://`](#6-媒体组与-attach)
|
|
13
|
+
7. [限制与有效期](#7-限制与有效期)
|
|
14
|
+
8. [本地 Bot API 服务器](#8-本地-bot-api-服务器)
|
|
15
|
+
9. [离线测试文件流程](#9-离线测试文件流程)
|
|
16
|
+
10. [故障排查](#10-故障排查)
|
|
17
|
+
|
|
18
|
+
## 1. 下载:一次调用 `downloadFile()`
|
|
19
|
+
|
|
20
|
+
`bot.downloadFile()` / `ctx.downloadFile()` 通过 `getFile` 解析 `file_id`,然后在一次调用中下载原始字节:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
bot.on("message:document", async (ctx) => {
|
|
24
|
+
const fileId = ctx.message.document.file_id;
|
|
25
|
+
|
|
26
|
+
// 下载到内存……
|
|
27
|
+
const file = await ctx.downloadFile(fileId);
|
|
28
|
+
console.log(file.fileName, file.sizeBytes, file.url);
|
|
29
|
+
// file.bytes 是 Uint8Array
|
|
30
|
+
|
|
31
|
+
// ……或直接写入磁盘
|
|
32
|
+
const saved = await ctx.downloadFile(fileId, { destination: "downloads/report.pdf" });
|
|
33
|
+
console.log(`已保存到 ${saved.savedTo}`);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
返回的 `DownloadedFile` 携带你所需的一切:
|
|
38
|
+
|
|
39
|
+
| 字段 | 含义 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `file` | `getFile` 返回的 Telegram `File` 对象 |
|
|
42
|
+
| `bytes` | 文件原始字节(`Uint8Array`) |
|
|
43
|
+
| `filePath` | 用于下载的 `file_path` |
|
|
44
|
+
| `url` | 直接下载链接 —— **至少 1 小时**内有效 |
|
|
45
|
+
| `fileName` | `filePath` 的最后一段(如 `report.pdf`) |
|
|
46
|
+
| `sizeBytes` | `bytes` 的字节长度 |
|
|
47
|
+
| `savedTo` | 本地路径,仅在传入 `destination` 时填充 |
|
|
48
|
+
|
|
49
|
+
错误同样精确:
|
|
50
|
+
- Telegram 未返回 `file_path` → `TelegramError`,`kind: "validation"`
|
|
51
|
+
- HTTP 下载本身失败 → `TelegramNetworkError`(含状态码)
|
|
52
|
+
- `getFile` 失败(file_id 错误、文件过大)→ Telegram 原始的 `TelegramError`
|
|
53
|
+
|
|
54
|
+
两者都支持 `AbortSignal`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
setTimeout(() => controller.abort(), 10_000);
|
|
59
|
+
const file = await bot.downloadFile(fileId, { signal: controller.signal });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 2. 下载:`getFile()` 手动流程
|
|
63
|
+
|
|
64
|
+
如果你想自己构造 URL(例如交给其他 HTTP 客户端):
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const file = await ctx.getFile(fileId); // Telegram File 对象
|
|
68
|
+
if (!file.file_path) throw new Error("文件不可用(超过 20 MB 或已过期)");
|
|
69
|
+
|
|
70
|
+
const url = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${file.file_path}`;
|
|
71
|
+
const response = await fetch(url); // ← 用 fetch —— 不要用 createReadStream(无法打开 URL)
|
|
72
|
+
if (!response.ok) throw new Error(`下载失败:HTTP ${response.status}`);
|
|
73
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
这段代码隐含三条规则:
|
|
77
|
+
1. URL 前缀是 `/file/bot<TOKEN>/` —— **`bot` 一词必不可少**。漏掉它是最常见的错误,结果是 `404 Not Found`。
|
|
78
|
+
2. 字节由 `fetch` 下载;`fs.createReadStream()` 只能打开**本地路径** —— 传入 URL 会在流错误回调里抛出未捕获的 `ENOENT`。
|
|
79
|
+
3. URL 保证**至少 1 小时**有效。不要长期缓存;过期后重新调用 `getFile`。
|
|
80
|
+
|
|
81
|
+
## 3. 属性命名:`file_path` 与 `filePath`
|
|
82
|
+
|
|
83
|
+
这个问题至少坑每个开发者一次。两种命名属于不同的层:
|
|
84
|
+
|
|
85
|
+
| 命名 | 归属 | 示例 |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `snake_case`(`file_path`) | **Telegram 原始对象** —— `getFile()` 的结果、`ctx.message.document`、`ctx.message.photo` | `file.file_path`、`document.file_id`、`photo.file_unique_id` |
|
|
88
|
+
| `camelCase`(`filePath`) | **telebibz 的结果类型** —— `DownloadedFile` 和库选项 | `downloaded.filePath`、`downloaded.fileName`、`downloaded.sizeBytes` |
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const file = await ctx.getFile(fileId);
|
|
92
|
+
file.file_path; // ✅ Telegram 对象 → snake_case
|
|
93
|
+
file.filePath; // ❌ undefined —— 那是 DownloadedFile 的字段名
|
|
94
|
+
|
|
95
|
+
const downloaded = await ctx.downloadFile(fileId);
|
|
96
|
+
downloaded.filePath; // ✅ 库结果 → camelCase
|
|
97
|
+
downloaded.file_path; // ❌ undefined
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
如果你明明在日志里看到了 `file_path`,"没有路径" 的检查却失败了,说明你在读 snake_case 对象上的 camelCase 属性。
|
|
101
|
+
|
|
102
|
+
## 4. 上传:所有来源类型
|
|
103
|
+
|
|
104
|
+
所有 `replyWith*` 发送器和原始 API 调用都接受 `InputFile` 类型。上传时传 `{ source, filename? }`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// 来自磁盘路径(绝对、./ 或 ../ 均可)
|
|
108
|
+
await ctx.replyWithDocument({ source: "reports/q3.pdf", filename: "Q3-report.pdf" });
|
|
109
|
+
|
|
110
|
+
// 来自原始字节
|
|
111
|
+
const bytes = new Uint8Array(await someFile.bytes());
|
|
112
|
+
await ctx.replyWithDocument({ source: bytes, filename: "data.bin" });
|
|
113
|
+
|
|
114
|
+
// 来自 Blob 或 File(File 自带文件名)
|
|
115
|
+
await ctx.replyWithDocument({ source: new File([bytes], "photo.png") });
|
|
116
|
+
|
|
117
|
+
// 来自 Web ReadableStream 或 Node 流(自动排空)
|
|
118
|
+
import { createReadStream } from "node:fs";
|
|
119
|
+
await ctx.replyWithVideo({ source: createReadStream("clip.mp4"), filename: "clip.mp4" });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
注意:
|
|
123
|
+
- `filename` 会覆盖来源自带的名称(路径默认取 basename)。
|
|
124
|
+
- **你永远不需要自己拼 `FormData`。** 传输层检测到上传负载后自动切换 multipart。手工构造内含 Node 流的 `FormData` 必定失败(`append` 需要 `Blob`)—— 请始终把流/字节交给库。
|
|
125
|
+
- 也可以传裸值:`await ctx.replyWithDocument(bytes)`(无文件名),或直接传已有的 Telegram file id:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// 通过 file_id 转发 —— 不下载、不上传、无大小限制
|
|
129
|
+
await ctx.replyWithDocument(ctx.message.document.file_id);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 5. 上传:发送前校验
|
|
133
|
+
|
|
134
|
+
`validateUpload()` / `assertValidUpload()` 在字节离开进程之前强制执行你的规则:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { assertValidUpload, UploadValidationError } from "@xbibzlibrary/telebibz";
|
|
138
|
+
|
|
139
|
+
bot.command("doc", async (ctx) => {
|
|
140
|
+
const filePath = ctx.message?.text?.split(/\s+/)[1];
|
|
141
|
+
if (!filePath) return void (await ctx.reply("用法:/doc <路径>"));
|
|
142
|
+
|
|
143
|
+
const info = await stat(filePath);
|
|
144
|
+
try {
|
|
145
|
+
assertValidUpload(
|
|
146
|
+
{ sizeBytes: info.size, fileName: filePath },
|
|
147
|
+
{
|
|
148
|
+
maxBytes: 50 * 1024 * 1024, // Telegram 文档上限
|
|
149
|
+
allowedExtensions: [".pdf", ".docx", ".pptx"], // 大小写不敏感
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof UploadValidationError) {
|
|
154
|
+
return void (await ctx.reply(`❌ ${error.message}`)); // 列出所有违规项
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await ctx.replyWithDocument({ source: filePath });
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`validateUpload()` 返回问题列表而不是抛异常(空数组 = 通过)。MIME 规则支持通配符:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
validateUpload({ mimeType: "image/png" }, { allowedMimeTypes: ["image/*"] }); // []
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
可用规则:`maxBytes`、`allowedMimeTypes`(精确或 `image/*` 通配)、`allowedExtensions`(点号可选,大小写不敏感)。
|
|
170
|
+
|
|
171
|
+
## 6. 媒体组与 `attach://`
|
|
172
|
+
|
|
173
|
+
`sendMediaGroup` 接受 JSON 输入媒体数组;二进制文件作为**独立的表单部分**通过 `attach://<name>` 引用:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
await ctx.replyWithMediaGroup([
|
|
177
|
+
{ type: "photo", media: "attach://pic1" },
|
|
178
|
+
{ type: "photo", media: "attach://pic2" },
|
|
179
|
+
], { pic1: bytes1, pic2: bytes2 } as never);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
传输层检测到二进制部分后自动切换 multipart;`media` 数组本身序列化为单个 JSON 字段 —— 与 Telegram 的契约完全一致。
|
|
183
|
+
|
|
184
|
+
## 7. 限制与有效期
|
|
185
|
+
|
|
186
|
+
| 限制 | 数值 | 说明 |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 通过 `getFile` 下载 | **20 MB** | 更大的文件:`getFile` 直接失败(HTTP 400 "file is too big")—— 不是 `file_path` 为空 |
|
|
189
|
+
| 上传照片 | 10 MB | |
|
|
190
|
+
| 上传其他文件 | 50 MB | |
|
|
191
|
+
| 通过 `file_id` 转发 | **无限制** | 文件已在 Telegram 侧 |
|
|
192
|
+
| 通过 URL 发送 | 照片 5 MB / 其他 20 MB | Telegram 侧抓取该 URL |
|
|
193
|
+
| 下载链接有效期 | **≥ 1 小时** | 过期后重新调用 `getFile` |
|
|
194
|
+
| `file_path` 是否存在 | schema 中为可选 | 使用前务必检查 |
|
|
195
|
+
|
|
196
|
+
上传上限属于 Telegram 而非本库 —— `validateUpload()` 让你用友好的提示提前拒绝。
|
|
197
|
+
|
|
198
|
+
## 8. 本地 Bot API 服务器
|
|
199
|
+
|
|
200
|
+
自建[本地 Bot API 服务器](https://core.telegram.org/bots/api#using-a-local-bot-api-server)可解除 20 MB 下载限制,并允许最大 2000 MB 的上传:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const bot = new Bot({
|
|
204
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
205
|
+
apiBaseUrl: "http://localhost:8081", // Bot API 选项
|
|
206
|
+
transportOptions: { timeoutMs: 600_000 },
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`downloadFile()` 和 `fileUrl()` 会把 `/bot<token>` 映射为任意 base URL 下的 `/file/bot<token>`,下载同样走本地服务器。注意:本地服务器返回的 `file_path` 是**服务器磁盘上的绝对路径** —— 服务器远程时 fetch 该 URL,bot 与服务器同机时直接读取该路径。
|
|
211
|
+
|
|
212
|
+
## 9. 离线测试文件流程
|
|
213
|
+
|
|
214
|
+
`MockTransport`(来自 `@xbibzlibrary/telebibz/testing`)实现了下载成员:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { createTestBot } from "@xbibzlibrary/telebibz/testing";
|
|
218
|
+
|
|
219
|
+
const { bot, transport } = createTestBot();
|
|
220
|
+
transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "documents/a.pdf" } });
|
|
221
|
+
transport.downloadBytes = new TextEncoder().encode("pdf-content");
|
|
222
|
+
|
|
223
|
+
const file = await bot.downloadFile("F1");
|
|
224
|
+
file.fileName; // "a.pdf"
|
|
225
|
+
new TextDecoder().decode(file.bytes); // "pdf-content"
|
|
226
|
+
transport.downloads; // ["documents/a.pdf"] —— 已记录的下载
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
完整测试指南见 [TESTING.zh-CN.md](TESTING.zh-CN.md)。
|
|
230
|
+
|
|
231
|
+
## 10. 故障排查
|
|
232
|
+
|
|
233
|
+
| 症状 | 原因 | 修复 |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| 日志里明明有 `file_path`,却报"无法获取文件路径" | 在 Telegram 原始对象上读取了 `file.filePath`/`file.path` | 使用 `file.file_path`(snake_case)—— 或干脆用 `ctx.downloadFile()` 跳过手动流程 |
|
|
236
|
+
| ReadStream 报 `ENOENT … open 'https://…'` | `createReadStream()` 只能打开本地路径 | URL 用 `fetch(url)`,或使用 `ctx.downloadFile()` |
|
|
237
|
+
| 下载 URL 返回 404 | `/file/bot<TOKEN>/` 中缺少 `bot` 前缀 | 使用 `downloadFile()` 的 `url` 字段 —— 它总是构造正确 |
|
|
238
|
+
| `getFile` 返回 HTTP 400 "file is too big" | 文件超过 20 MB | 使用本地 Bot API 服务器,或通过 `file_id` 转发 |
|
|
239
|
+
| `FormData append: parameter 2 is not of type 'Blob'` | 手工构造的 FormData 里放了 Node 流 | 把 `{ source: stream, filename }` 交给 `replyWith*`;multipart 由库处理 |
|
|
240
|
+
| `file_path` 之前有值,现在没了 | 链接过期(>1 小时) | 重新调用 `getFile` |
|
|
241
|
+
| 下载的文件为 0 字节 / 内容错误 | `file_id` 属于另一个 bot | `file_id` 与 bot 绑定;请使用你自己 bot 的 update 中的 id |
|
|
242
|
+
|
|
243
|
+
English: [FILES.md](FILES.md) · Bahasa Indonesia: [FILES.id.md](FILES.id.md)
|
|
@@ -80,6 +80,10 @@ Gunakan HTTPS untuk webhook, validasi secret webhook Telegram, simpan token di s
|
|
|
80
80
|
|
|
81
81
|
- [Runnable examples](../examples/README.md)
|
|
82
82
|
- [Referensi API lengkap](API.id.md)
|
|
83
|
-
- [
|
|
84
|
-
- [
|
|
83
|
+
- [File: upload dan download](FILES.id.md)
|
|
84
|
+
- [Penanganan error dan rate limit](ERRORS.id.md)
|
|
85
|
+
- [Deployment webhook](WEBHOOK.id.md)
|
|
86
|
+
- [Testing bot secara offline](TESTING.id.md)
|
|
87
|
+
- [Migrasi dari Telegraf](MIGRATION_TELEGRAF.id.md)
|
|
88
|
+
- [Cookbook produksi](COOKBOOK.id.md)
|
|
85
89
|
- [Panduan kontribusi](../CONTRIBUTING.md)
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -80,6 +80,10 @@ Use HTTPS for webhooks, verify the Telegram webhook secret, keep tokens in a sec
|
|
|
80
80
|
|
|
81
81
|
- [Runnable examples](../examples/README.md)
|
|
82
82
|
- [Complete API reference](API.md)
|
|
83
|
-
- [
|
|
84
|
-
- [
|
|
83
|
+
- [Files: upload and download](FILES.md)
|
|
84
|
+
- [Error handling and rate limits](ERRORS.md)
|
|
85
|
+
- [Webhook deployment](WEBHOOK.md)
|
|
86
|
+
- [Testing your bot offline](TESTING.md)
|
|
87
|
+
- [Migrating from Telegraf](MIGRATION_TELEGRAF.md)
|
|
88
|
+
- [Production cookbook](COOKBOOK.md)
|
|
85
89
|
- [Contribution guide](../CONTRIBUTING.md)
|
|
@@ -80,6 +80,10 @@ Webhook 使用 HTTPS,验证 Telegram webhook secret,将 token 保存到 secr
|
|
|
80
80
|
|
|
81
81
|
- [Runnable examples](../examples/README.md)
|
|
82
82
|
- [完整 API 参考](API.zh-CN.md)
|
|
83
|
-
- [
|
|
84
|
-
- [
|
|
83
|
+
- [文件:上传与下载](FILES.zh-CN.md)
|
|
84
|
+
- [错误处理与限流](ERRORS.zh-CN.md)
|
|
85
|
+
- [Webhook 部署](WEBHOOK.zh-CN.md)
|
|
86
|
+
- [离线测试你的 bot](TESTING.zh-CN.md)
|
|
87
|
+
- [从 Telegraf 迁移](MIGRATION_TELEGRAF.zh-CN.md)
|
|
88
|
+
- [生产实战手册](COOKBOOK.zh-CN.md)
|
|
85
89
|
- [贡献指南](../CONTRIBUTING.md)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Migrasi dari Telegraf (Bahasa Indonesia)
|
|
2
|
+
|
|
3
|
+
telebibz mengimplementasikan surface context dan opsi launch Telegraf secara sengaja, sehingga sebagian besar handler bisa dipindah dengan sedikit atau tanpa perubahan. Panduan ini memetakan setiap bagian bot Telegraf ke padanannya di telebibz.
|
|
4
|
+
|
|
5
|
+
## Daftar isi
|
|
6
|
+
|
|
7
|
+
1. [Side-by-side: satu bot utuh](#1-side-by-side-satu-bot-utuh)
|
|
8
|
+
2. [Peta konsep](#2-peta-konsep)
|
|
9
|
+
3. [Method Context](#3-method-context)
|
|
10
|
+
4. [Opsi launch](#4-opsi-launch)
|
|
11
|
+
5. [Scenes → Wizards](#5-scenes--wizards)
|
|
12
|
+
6. [Penyimpanan session](#6-penyimpanan-session)
|
|
13
|
+
7. [Webhook](#7-webhook)
|
|
14
|
+
8. [Yang tidak punya ekuivalen langsung](#8-yang-tidak-punya-ekuivalen-langsung)
|
|
15
|
+
|
|
16
|
+
## 1. Side-by-side: satu bot utuh
|
|
17
|
+
|
|
18
|
+
**Telegraf**
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Telegraf } from "telegraf";
|
|
22
|
+
|
|
23
|
+
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
|
|
24
|
+
|
|
25
|
+
bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
|
|
26
|
+
bot.start((ctx) => ctx.reply("Welcome!"));
|
|
27
|
+
bot.command("help", (ctx) => ctx.reply("Help"));
|
|
28
|
+
bot.action("menu:open", async (ctx) => { await ctx.answerCbQuery(); await ctx.reply("Menu"); });
|
|
29
|
+
bot.on("message", (ctx) => ctx.reply("got it"));
|
|
30
|
+
bot.catch((error) => console.error(error));
|
|
31
|
+
|
|
32
|
+
bot.launch({ dropPendingUpdates: true });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**telebibz**
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Bot } from "@xbibzlibrary/telebibz";
|
|
39
|
+
|
|
40
|
+
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
|
|
41
|
+
|
|
42
|
+
bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
|
|
43
|
+
bot.command("start", async (ctx) => { await ctx.reply("Welcome!"); }); // command bernama, bukan bot.start()
|
|
44
|
+
bot.command("help", async (ctx) => { await ctx.reply("Help"); });
|
|
45
|
+
bot.action("menu:open", async (ctx) => { await ctx.answerCallbackQuery(); await ctx.reply("Menu"); });
|
|
46
|
+
bot.on("message", async (ctx) => { await ctx.reply("got it"); });
|
|
47
|
+
bot.catch(async (error) => { console.error(error); });
|
|
48
|
+
|
|
49
|
+
await bot.launch({ dropPendingUpdates: true }); // nama opsi sama
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Hanya dua perbedaan mekanis: `bot.start(handler)` menjadi `bot.command("start", handler)`, dan `answerCbQuery()` menjadi `answerCallbackQuery()`.
|
|
53
|
+
|
|
54
|
+
## 2. Peta konsep
|
|
55
|
+
|
|
56
|
+
| Telegraf | telebibz | Catatan |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `new Telegraf(token)` | `new Bot(token)` atau `new Bot({ token, ... })` | |
|
|
59
|
+
| `bot.launch()` | `bot.launch()` / `bot.start()` | `mode: "polling"` eksplisit di `launch` |
|
|
60
|
+
| `bot.stop()` | `bot.stop()` | telebibz men-drain handler yang sedang berjalan lebih dulu |
|
|
61
|
+
| `bot.use(mw)` | `bot.use(mw)` | signature middleware sama `(ctx, next)` |
|
|
62
|
+
| `bot.command(name, h)` | `bot.command(name, h)` | |
|
|
63
|
+
| `bot.on(filter, h)` | `bot.on(filter, h)` | grammar filter sama (`message:photo`, array) |
|
|
64
|
+
| `bot.hears(trigger, h)` | `bot.hears(trigger, h)` | string dan RegExp |
|
|
65
|
+
| `bot.action(pattern, h)` | `bot.action(pattern, h)` | alias drop-in dari `bot.callback` |
|
|
66
|
+
| `bot.catch(handler)` | `bot.catch(handler)` | menerima `(error, ctx)` |
|
|
67
|
+
|
|
68
|
+
## 3. Method Context
|
|
69
|
+
|
|
70
|
+
Setiap shortcut context Telegraf ada — termasuk yang di Telegraf diserahkan ke plugin:
|
|
71
|
+
|
|
72
|
+
- **Balasan**: `reply`, `replyWithPhoto`, `replyWithDocument`, `replyWithVideo`, `replyWithAudio`, `replyWithVoice`, `replyWithAnimation`, `replyWithVideoNote`, `replyWithSticker`, `replyWithMediaGroup`, `replyWithLocation`, `replyWithVenue`, `replyWithContact`, `replyWithPoll`, `replyWithQuiz`, `replyWithDice`, `replyWithGame`, `replyWithInvoice`, `replyWithHTML`, `replyWithMarkdown` (+V2)
|
|
73
|
+
- **Admin/moderasi**: `banChatMember`, `unbanChatMember`, `restrictChatMember`, `promoteChatMember`, `banChatSenderChat`, `unbanChatSenderChat`
|
|
74
|
+
- **Chat**: `setChatTitle`, `setChatDescription`, `setChatPhoto`, `deleteChatPhoto`, `setChatPermissions`, `leaveChat`, `unpinAllChatMessages`, `setChatStickerSet`, `deleteChatStickerSet`
|
|
75
|
+
- **Info**: `getChat`, `getChatAdministrators`, `getChatMemberCount`, `getChatMember`
|
|
76
|
+
- **Invite link/join request**: `exportChatInviteLink`, `createChatInviteLink`, `editChatInviteLink`, `revokeChatInviteLink`, `approveChatJoinRequest`, `declineChatJoinRequest`
|
|
77
|
+
- **Live location/poll/game**: `editMessageLiveLocation`, `stopMessageLiveLocation`, `stopPoll`, `setGameScore`, `getGameHighScores`
|
|
78
|
+
- **Forum**: set topik lengkap (`createForumTopic` … `unhideGeneralForumTopic`)
|
|
79
|
+
- **Baru, melampaui core Telegraf**: `getFile` (typed), `downloadFile`, `edit` (menulis ulang teks pesan saat ini), plus helper mandiri yang diekspor dari root paket — `validateUpload`/`assertValidUpload` — yang bukan method context
|
|
80
|
+
|
|
81
|
+
Perbedaan penamaan yang harus diperbaiki saat porting: `answerCbQuery` → `answerCallbackQuery`; `ctx.telegram` → `ctx.api`; helper keyboard berasal dari root paket (`InlineKeyboard`, `ReplyKeyboard`, `removeKeyboard`, `forceReply`) alih-alih `Markup`.
|
|
82
|
+
|
|
83
|
+
## 4. Opsi launch
|
|
84
|
+
|
|
85
|
+
| Telegraf | telebibz |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `launch({ dropPendingUpdates })` | `launch({ dropPendingUpdates })` — identik |
|
|
88
|
+
| `handlerTimeout` (default 90 000) | `handlerTimeout` (default 90 000; `0` menonaktifkan) |
|
|
89
|
+
| Opsi `contextType` | Opsi `contextType` — subclass `Context` Anda diinstansiasi untuk setiap update |
|
|
90
|
+
| `webhookReply` (per-update) | `webhookReply` pada opsi handler / `handleUpdate` |
|
|
91
|
+
| `telegraf.use(session(...))` | `new Bot({ session: new MemoryStorage() })` (atau JSON/Redis/SQL/Mongo) |
|
|
92
|
+
|
|
93
|
+
## 5. Scenes → Wizards
|
|
94
|
+
|
|
95
|
+
`WizardScene` + `Stage` milik Telegraf menjadi satu `Wizard` dengan step eksplisit dan tanpa kunci session global:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { Bot, Wizard } from "@xbibzlibrary/telebibz";
|
|
99
|
+
|
|
100
|
+
const wizard = new Wizard()
|
|
101
|
+
.step({ id: "ask-name", run: async (flow) => { flow.next(); await flow.ctx.reply("Nama?"); } })
|
|
102
|
+
.step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Hai ${flow.ctx.message?.text}!`); } });
|
|
103
|
+
|
|
104
|
+
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
|
|
105
|
+
bot.useWizard(wizard); // menggantikan middleware Stage
|
|
106
|
+
bot.command("start", async (ctx) => { await wizard.run(ctx); }); // menggantikan scene.enter()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Kunci wizard diturunkan otomatis dari chat + pengirim — tanpa pengelolaan kunci manual.
|
|
110
|
+
- `flow.set(key, value)` / `flow.get(key)` menggantikan `ctx.scene.session`.
|
|
111
|
+
- `/cancel` membatalkan; conversation selesai otomatis setelah step terakhir.
|
|
112
|
+
- Untuk graf non-linear, susun `ConversationManager` dengan router (telebibz sengaja menjadikan orkestrasi scene milik aplikasi; lihat "Design decisions" di FEATURE_MATRIX).
|
|
113
|
+
|
|
114
|
+
## 6. Penyimpanan session
|
|
115
|
+
|
|
116
|
+
Telegraf menyimpan session di memori secara default dan butuh plugin store untuk persistensi. telebibz menerima storage di constructor — ganti adapter, pertahankan kode:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Bot, MemoryStorage, JsonFileStorage, RedisStorage } from "@xbibzlibrary/telebibz";
|
|
120
|
+
|
|
121
|
+
const bot = new Bot({
|
|
122
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
123
|
+
session: new JsonFileStorage("state/sessions.json"), // atau MemoryStorage / RedisStorage / SqlStorage / MongoStorage
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Resep wiring lengkap untuk setiap adapter: [STORAGE.id.md](STORAGE.id.md).
|
|
128
|
+
|
|
129
|
+
## 7. Webhook
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// Telegraf: webhookCallback(bot, app)
|
|
133
|
+
// telebibz: framework eksplisit
|
|
134
|
+
import { webhookCallback } from "@xbibzlibrary/telebibz";
|
|
135
|
+
app.post("/telegram", webhookCallback(bot, "express", { secretToken: process.env.TELEGRAM_WEBHOOK_SECRET }));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`createWebhookHandler()` tambahan menyediakan handler Web-standard `Request → Response` untuk Bun/Deno/edge. Panduan deployment lengkap: [WEBHOOK.id.md](WEBHOOK.id.md).
|
|
139
|
+
|
|
140
|
+
## 8. Yang tidak punya ekuivalen langsung
|
|
141
|
+
|
|
142
|
+
- **Client low-level `bot.telegram`** — pakai `bot.api` (`call`, `raw`, `methods`, `downloadFile`); flood gate dan retry terpasang di transport, bukan dikonfigurasi per panggilan.
|
|
143
|
+
- **Ekosistem plugin Telegraf** — port plugin sebagai objek `Plugin` dengan lifecycle eksplisit (`install`, `onStop`, `dispose`); plugin manager di-restart dengan bersih.
|
|
144
|
+
- **`Composer.mount`/scene dinamis** — bangun dengan nesting `Router` dan `matchMode: "all"`.
|
|
145
|
+
- **Rantai helper Markup** (`Markup.keyboard(...).resize()`) — pakai `new ReplyKeyboard().text("A").resized().build()`; payload sama, gaya builder.
|
|
146
|
+
|
|
147
|
+
English: [MIGRATION_TELEGRAF.md](MIGRATION_TELEGRAF.md) · 简体中文: [MIGRATION_TELEGRAF.zh-CN.md](MIGRATION_TELEGRAF.zh-CN.md)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Migrating from Telegraf (English)
|
|
2
|
+
|
|
3
|
+
telebibz implements the Telegraf context surface and launch options deliberately, so most handlers port with little or no change. This guide maps every part of a Telegraf bot to its telebibz equivalent.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
1. [Side-by-side: a whole bot](#1-side-by-side-a-whole-bot)
|
|
8
|
+
2. [Concept map](#2-concept-map)
|
|
9
|
+
3. [Context methods](#3-context-methods)
|
|
10
|
+
4. [Launch options](#4-launch-options)
|
|
11
|
+
5. [Scenes → Wizards](#5-scenes--wizards)
|
|
12
|
+
6. [Session storage](#6-session-storage)
|
|
13
|
+
7. [Webhooks](#7-webhooks)
|
|
14
|
+
8. [What has no direct equivalent](#8-what-has-no-direct-equivalent)
|
|
15
|
+
|
|
16
|
+
## 1. Side-by-side: a whole bot
|
|
17
|
+
|
|
18
|
+
**Telegraf**
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Telegraf } from "telegraf";
|
|
22
|
+
|
|
23
|
+
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
|
|
24
|
+
|
|
25
|
+
bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
|
|
26
|
+
bot.start((ctx) => ctx.reply("Welcome!"));
|
|
27
|
+
bot.command("help", (ctx) => ctx.reply("Help"));
|
|
28
|
+
bot.action("menu:open", async (ctx) => { await ctx.answerCbQuery(); await ctx.reply("Menu"); });
|
|
29
|
+
bot.on("message", (ctx) => ctx.reply("got it"));
|
|
30
|
+
bot.catch((error) => console.error(error));
|
|
31
|
+
|
|
32
|
+
bot.launch({ dropPendingUpdates: true });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**telebibz**
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Bot } from "@xbibzlibrary/telebibz";
|
|
39
|
+
|
|
40
|
+
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
|
|
41
|
+
|
|
42
|
+
bot.use(async (ctx, next) => { console.time("update"); await next(); console.timeEnd("update"); });
|
|
43
|
+
bot.command("start", async (ctx) => { await ctx.reply("Welcome!"); }); // named command, not bot.start()
|
|
44
|
+
bot.command("help", async (ctx) => { await ctx.reply("Help"); });
|
|
45
|
+
bot.action("menu:open", async (ctx) => { await ctx.answerCallbackQuery(); await ctx.reply("Menu"); });
|
|
46
|
+
bot.on("message", async (ctx) => { await ctx.reply("got it"); });
|
|
47
|
+
bot.catch(async (error) => { console.error(error); });
|
|
48
|
+
|
|
49
|
+
await bot.launch({ dropPendingUpdates: true }); // same option name
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Only two mechanical differences: `bot.start(handler)` becomes `bot.command("start", handler)`, and `answerCbQuery()` becomes `answerCallbackQuery()`.
|
|
53
|
+
|
|
54
|
+
## 2. Concept map
|
|
55
|
+
|
|
56
|
+
| Telegraf | telebibz | Notes |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `new Telegraf(token)` | `new Bot(token)` or `new Bot({ token, ... })` | |
|
|
59
|
+
| `bot.launch()` | `bot.launch()` / `bot.start()` | `mode: "polling"` is explicit on `launch` |
|
|
60
|
+
| `bot.stop()` | `bot.stop()` | telebibz drains in-flight handlers first |
|
|
61
|
+
| `bot.use(mw)` | `bot.use(mw)` | same middleware signature `(ctx, next)` |
|
|
62
|
+
| `bot.command(name, h)` | `bot.command(name, h)` | |
|
|
63
|
+
| `bot.on(filter, h)` | `bot.on(filter, h)` | same filter grammar (`message:photo`, arrays) |
|
|
64
|
+
| `bot.hears(trigger, h)` | `bot.hears(trigger, h)` | strings and RegExp |
|
|
65
|
+
| `bot.action(pattern, h)` | `bot.action(pattern, h)` | drop-in alias of `bot.callback` |
|
|
66
|
+
| `bot.catch(handler)` | `bot.catch(handler)` | receives `(error, ctx)` |
|
|
67
|
+
| `ctx.reply(text, extra)` | `ctx.reply(text, extra)` | |
|
|
68
|
+
| `ctx.telegram.callApi(m, p)` | `ctx.api.call(m, p)` / `ctx.api.raw(m, p)` | `raw` needs no type map entry |
|
|
69
|
+
| `ctx.telegram.api.config` | `transportOptions` on the `Bot` options | timeout, retries, flood gate |
|
|
70
|
+
| `Scenes.WizardScene` + `Stage` | `Wizard` + `bot.useWizard()` | see section 5 |
|
|
71
|
+
| `session` middleware | built-in `session` storage option | see section 6 |
|
|
72
|
+
| `webhookCallback(bot, app)` | `webhookCallback(bot, "express")` | framework is now an argument |
|
|
73
|
+
| Telegraf plugins (`telegraf-i18n`, …) | `bot.usePlugin({ install, onStop, dispose })` | explicit lifecycle |
|
|
74
|
+
|
|
75
|
+
## 3. Context methods
|
|
76
|
+
|
|
77
|
+
Every Telegraf context shortcut exists — including the ones Telegraf leaves to plugins:
|
|
78
|
+
|
|
79
|
+
- **Replies**: `reply`, `replyWithPhoto`, `replyWithDocument`, `replyWithVideo`, `replyWithAudio`, `replyWithVoice`, `replyWithAnimation`, `replyWithVideoNote`, `replyWithSticker`, `replyWithMediaGroup`, `replyWithLocation`, `replyWithVenue`, `replyWithContact`, `replyWithPoll`, `replyWithQuiz`, `replyWithDice`, `replyWithGame`, `replyWithInvoice`, `replyWithHTML`, `replyWithMarkdown` (+V2)
|
|
80
|
+
- **Admin/moderation**: `banChatMember`, `unbanChatMember`, `restrictChatMember`, `promoteChatMember`, `banChatSenderChat`, `unbanChatSenderChat`
|
|
81
|
+
- **Chat**: `setChatTitle`, `setChatDescription`, `setChatPhoto`, `deleteChatPhoto`, `setChatPermissions`, `leaveChat`, `unpinAllChatMessages`, `setChatStickerSet`, `deleteChatStickerSet`
|
|
82
|
+
- **Info**: `getChat`, `getChatAdministrators`, `getChatMemberCount`, `getChatMember`
|
|
83
|
+
- **Invite links/join requests**: `exportChatInviteLink`, `createChatInviteLink`, `editChatInviteLink`, `revokeChatInviteLink`, `approveChatJoinRequest`, `declineChatJoinRequest`
|
|
84
|
+
- **Live location/polls/games**: `editMessageLiveLocation`, `stopMessageLiveLocation`, `stopPoll`, `setGameScore`, `getGameHighScores`
|
|
85
|
+
- **Forum**: full topic set (`createForumTopic` … `unhideGeneralForumTopic`)
|
|
86
|
+
- **New beyond Telegraf core**: `getFile` (typed), `downloadFile`, `edit` (rewrites the current message's text), plus standalone helpers exported from the package root — `validateUpload`/`assertValidUpload` — which are not context methods
|
|
87
|
+
|
|
88
|
+
Naming differences to fix while porting: `answerCbQuery` → `answerCallbackQuery`; `ctx.telegram` → `ctx.api`; keyboard helpers come from the package root (`InlineKeyboard`, `ReplyKeyboard`, `removeKeyboard`, `forceReply`) instead of `Markup`.
|
|
89
|
+
|
|
90
|
+
## 4. Launch options
|
|
91
|
+
|
|
92
|
+
| Telegraf | telebibz |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `launch({ dropPendingUpdates })` | `launch({ dropPendingUpdates })` — identical |
|
|
95
|
+
| `handlerTimeout` (90 000 default) | `handlerTimeout` (90 000 default; `0` disables) |
|
|
96
|
+
| `contextType` option | `contextType` option — your `Context` subclass is instantiated for every update |
|
|
97
|
+
| `webhookReply` (per-update) | `webhookReply` on the handler / `handleUpdate` options |
|
|
98
|
+
| `telegraf.use(session(...))` | `new Bot({ session: new MemoryStorage() })` (or JSON/Redis/SQL/Mongo) |
|
|
99
|
+
|
|
100
|
+
## 5. Scenes → Wizards
|
|
101
|
+
|
|
102
|
+
Telegraf's `WizardScene` + `Stage` becomes a single `Wizard` with explicit steps and no global session keys:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Bot, Wizard } from "@xbibzlibrary/telebibz";
|
|
106
|
+
|
|
107
|
+
const wizard = new Wizard()
|
|
108
|
+
.step({ id: "ask-name", run: async (flow) => { flow.next(); await flow.ctx.reply("Name?"); } })
|
|
109
|
+
.step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Hi ${flow.ctx.message?.text}!`); } });
|
|
110
|
+
|
|
111
|
+
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
|
|
112
|
+
bot.useWizard(wizard); // replaces Stage middleware
|
|
113
|
+
bot.command("start", async (ctx) => { await wizard.run(ctx); }); // replaces scene.enter()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- The wizard key derives automatically from chat + sender — no manual key management.
|
|
117
|
+
- `flow.set(key, value)` / `flow.get(key)` replace `ctx.scene.session`.
|
|
118
|
+
- `/cancel` cancels; the conversation completes automatically after the last step.
|
|
119
|
+
- For non-linear graphs, compose `ConversationManager` with the router (telebibz deliberately keeps scene orchestration application-owned; see FEATURE_MATRIX "Design decisions").
|
|
120
|
+
|
|
121
|
+
## 6. Session storage
|
|
122
|
+
|
|
123
|
+
Telegraf stores sessions in memory by default and needs a store plugin for persistence. telebibz takes storage on the constructor — swap the adapter, keep the code:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { Bot, MemoryStorage, JsonFileStorage, RedisStorage } from "@xbibzlibrary/telebibz";
|
|
127
|
+
|
|
128
|
+
const bot = new Bot({
|
|
129
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
130
|
+
session: new JsonFileStorage("state/sessions.json"), // or MemoryStorage / RedisStorage / SqlStorage / MongoStorage
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Full wiring recipes for every adapter: [STORAGE.md](STORAGE.md).
|
|
135
|
+
|
|
136
|
+
## 7. Webhooks
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// Telegraf: webhookCallback(bot, app)
|
|
140
|
+
// telebibz: framework is explicit
|
|
141
|
+
import { webhookCallback } from "@xbibzlibrary/telebibz";
|
|
142
|
+
app.post("/telegram", webhookCallback(bot, "express", { secretToken: process.env.TELEGRAM_WEBHOOK_SECRET }));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`createWebhookHandler()` additionally provides a Web-standard `Request → Response` handler for Bun/Deno/edge. Full deployment guide: [WEBHOOK.md](WEBHOOK.md).
|
|
146
|
+
|
|
147
|
+
## 8. What has no direct equivalent
|
|
148
|
+
|
|
149
|
+
- **`bot.telegram` low-level client** — use `bot.api` (`call`, `raw`, `methods`, `downloadFile`); the flood gate and retries are built into the transport rather than configurable per call.
|
|
150
|
+
- **Telegraf's plugin ecosystem** — port plugins as `Plugin` objects with an explicit lifecycle (`install`, `onStop`, `dispose`); the plugin manager restarts cleanly.
|
|
151
|
+
- **`Composer.mount`/dynamic scenes** — build with `Router` nesting and `matchMode: "all"` instead.
|
|
152
|
+
- **Markup helper chains** (`Markup.keyboard(...).resize()`) — use `new ReplyKeyboard().text("A").resized().build()`; same payloads, builder style.
|
|
153
|
+
|
|
154
|
+
Bahasa Indonesia: [MIGRATION_TELEGRAF.id.md](MIGRATION_TELEGRAF.id.md) · 简体中文: [MIGRATION_TELEGRAF.zh-CN.md](MIGRATION_TELEGRAF.zh-CN.md)
|