@xbibzlibrary/telebibz 0.4.3 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.id.md +20 -3
  3. package/README.md +20 -3
  4. package/README.zh-CN.md +20 -3
  5. package/dist/src/api/client.d.ts +28 -0
  6. package/dist/src/api/client.d.ts.map +1 -1
  7. package/dist/src/api/client.js +28 -0
  8. package/dist/src/api/client.js.map +1 -1
  9. package/dist/src/api/transport.d.ts +14 -0
  10. package/dist/src/api/transport.d.ts.map +1 -1
  11. package/dist/src/api/transport.js +67 -2
  12. package/dist/src/api/transport.js.map +1 -1
  13. package/dist/src/api/types.d.ts +25 -0
  14. package/dist/src/api/types.d.ts.map +1 -1
  15. package/dist/src/context/context.d.ts +13 -3
  16. package/dist/src/context/context.d.ts.map +1 -1
  17. package/dist/src/context/context.js +15 -0
  18. package/dist/src/context/context.js.map +1 -1
  19. package/dist/src/core/bot.d.ts +12 -0
  20. package/dist/src/core/bot.d.ts.map +1 -1
  21. package/dist/src/core/bot.js +16 -0
  22. package/dist/src/core/bot.js.map +1 -1
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +1 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/testing.d.ts +6 -0
  28. package/dist/src/testing.d.ts.map +1 -1
  29. package/dist/src/testing.js +6 -0
  30. package/dist/src/testing.js.map +1 -1
  31. package/dist/src/utils/files.d.ts +45 -0
  32. package/dist/src/utils/files.d.ts.map +1 -0
  33. package/dist/src/utils/files.js +53 -0
  34. package/dist/src/utils/files.js.map +1 -0
  35. package/dist-cjs/src/api/client.js +28 -0
  36. package/dist-cjs/src/api/transport.js +67 -2
  37. package/dist-cjs/src/context/context.js +15 -0
  38. package/dist-cjs/src/core/bot.js +16 -0
  39. package/dist-cjs/src/index.js +1 -0
  40. package/dist-cjs/src/testing.js +6 -0
  41. package/dist-cjs/src/utils/files.js +58 -0
  42. package/docs/API.id.md +70 -0
  43. package/docs/API.md +70 -0
  44. package/docs/API.zh-CN.md +70 -0
  45. package/docs/COOKBOOK.id.md +321 -0
  46. package/docs/COOKBOOK.md +321 -0
  47. package/docs/COOKBOOK.zh-CN.md +321 -0
  48. package/docs/ERRORS.id.md +194 -0
  49. package/docs/ERRORS.md +194 -0
  50. package/docs/ERRORS.zh-CN.md +194 -0
  51. package/docs/FILES.id.md +243 -0
  52. package/docs/FILES.md +243 -0
  53. package/docs/FILES.zh-CN.md +243 -0
  54. package/docs/GETTING_STARTED.id.md +6 -2
  55. package/docs/GETTING_STARTED.md +6 -2
  56. package/docs/GETTING_STARTED.zh-CN.md +6 -2
  57. package/docs/MIGRATION_TELEGRAF.id.md +147 -0
  58. package/docs/MIGRATION_TELEGRAF.md +154 -0
  59. package/docs/MIGRATION_TELEGRAF.zh-CN.md +147 -0
  60. package/docs/README.md +38 -19
  61. package/docs/STORAGE.id.md +105 -0
  62. package/docs/STORAGE.md +105 -0
  63. package/docs/STORAGE.zh-CN.md +105 -0
  64. package/docs/TESTING.id.md +203 -0
  65. package/docs/TESTING.md +203 -0
  66. package/docs/TESTING.zh-CN.md +203 -0
  67. package/docs/WEBHOOK.id.md +212 -0
  68. package/docs/WEBHOOK.md +215 -0
  69. package/docs/WEBHOOK.zh-CN.md +212 -0
  70. package/examples/files.ts +35 -0
  71. package/package.json +1 -1
package/docs/FILES.md ADDED
@@ -0,0 +1,243 @@
1
+ # Working with files: upload, download, validation (English)
2
+
3
+ Complete guide to every file flow in telebibz: downloading files users send you, uploading files to Telegram, sending by `file_id`, validating uploads, and the limits and gotchas that come with the Telegram Bot API.
4
+
5
+ ## Contents
6
+
7
+ 1. [Download: one call with `downloadFile()`](#1-download-one-call-with-downloadfile)
8
+ 2. [Download: manual flow with `getFile()`](#2-download-manual-flow-with-getfile)
9
+ 3. [Property naming: `file_path` vs `filePath`](#3-property-naming-file_path-vs-filepath)
10
+ 4. [Upload: every source type](#4-upload-every-source-type)
11
+ 5. [Upload: validation before sending](#5-upload-validation-before-sending)
12
+ 6. [Media groups with `attach://`](#6-media-groups-with-attach)
13
+ 7. [Limits and lifetimes](#7-limits-and-lifetimes)
14
+ 8. [Local Bot API server](#8-local-bot-api-server)
15
+ 9. [Testing file flows without network](#9-testing-file-flows-without-network)
16
+ 10. [Troubleshooting](#10-troubleshooting)
17
+
18
+ ## 1. Download: one call with `downloadFile()`
19
+
20
+ `bot.downloadFile()` / `ctx.downloadFile()` resolves the `file_id` through `getFile` and downloads the raw bytes in a single call:
21
+
22
+ ```ts
23
+ bot.on("message:document", async (ctx) => {
24
+ const fileId = ctx.message.document.file_id;
25
+
26
+ // Downloads to memory…
27
+ const file = await ctx.downloadFile(fileId);
28
+ console.log(file.fileName, file.sizeBytes, file.url);
29
+ // file.bytes is a Uint8Array
30
+
31
+ // …or straight to disk
32
+ const saved = await ctx.downloadFile(fileId, { destination: "downloads/report.pdf" });
33
+ console.log(`Saved to ${saved.savedTo}`);
34
+ });
35
+ ```
36
+
37
+ The result (`DownloadedFile`) carries everything:
38
+
39
+ | Field | Meaning |
40
+ |---|---|
41
+ | `file` | The Telegram `File` object returned by `getFile` |
42
+ | `bytes` | Raw file bytes (`Uint8Array`) |
43
+ | `filePath` | The `file_path` used for the download |
44
+ | `url` | Direct download URL — valid for **at least 1 hour** |
45
+ | `fileName` | Last path segment of `filePath` (e.g. `report.pdf`) |
46
+ | `sizeBytes` | Byte length of `bytes` |
47
+ | `savedTo` | Local path, set when you passed `destination` |
48
+
49
+ Errors are precise:
50
+ - Telegram returns no `file_path` → `TelegramError` with `kind: "validation"`
51
+ - The HTTP download itself fails → `TelegramNetworkError` (with status)
52
+ - `getFile` fails (bad `file_id`, file too big) → the original `TelegramError` from Telegram
53
+
54
+ Both variants accept an `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. Download: manual flow with `getFile()`
63
+
64
+ If you want to build the URL yourself (e.g. to hand it to another HTTP client):
65
+
66
+ ```ts
67
+ const file = await ctx.getFile(fileId); // Telegram File object
68
+ if (!file.file_path) throw new Error("File unavailable (over 20 MB or expired)");
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 — NEVER createReadStream (it cannot open URLs)
72
+ if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
73
+ const bytes = new Uint8Array(await response.arrayBuffer());
74
+ ```
75
+
76
+ Three rules this snippet encodes:
77
+ 1. The URL prefix is `/file/bot<TOKEN>/` — **the word `bot` is required**. Forgetting it is the single most common mistake and yields `404 Not Found`.
78
+ 2. `fetch` downloads the bytes; `fs.createReadStream()` only opens **local paths** — passing it a URL crashes the process with `ENOENT` on an unhandled stream error.
79
+ 3. The URL is guaranteed valid for **at least 1 hour**. Never cache it long-term; call `getFile` again when it expires.
80
+
81
+ ## 3. Property naming: `file_path` vs `filePath`
82
+
83
+ This trips everyone once. The two naming styles belong to different layers:
84
+
85
+ | Naming | Belongs to | Examples |
86
+ |---|---|---|
87
+ | `snake_case` (`file_path`) | **Raw Telegram objects** — the result of `getFile()`, `ctx.message.document`, `ctx.message.photo` | `file.file_path`, `document.file_id`, `photo.file_unique_id` |
88
+ | `camelCase` (`filePath`) | **telebibz result types** — `DownloadedFile` and library options | `downloaded.filePath`, `downloaded.fileName`, `downloaded.sizeBytes` |
89
+
90
+ ```ts
91
+ const file = await ctx.getFile(fileId);
92
+ file.file_path; // ✅ Telegram object → snake_case
93
+ file.filePath; // ❌ undefined — this is the DownloadedFile name
94
+
95
+ const downloaded = await ctx.downloadFile(fileId);
96
+ downloaded.filePath; // ✅ library result → camelCase
97
+ downloaded.file_path; // ❌ undefined
98
+ ```
99
+
100
+ If your "no file path" check fails while the logged response clearly contains `file_path`, you are reading a camelCase property from a snake_case object.
101
+
102
+ ## 4. Upload: every source type
103
+
104
+ All `replyWith*` senders and raw API calls accept the `InputFile` type. Pass uploads as `{ source, filename? }`:
105
+
106
+ ```ts
107
+ // From a path on disk (absolute, ./, or ../)
108
+ await ctx.replyWithDocument({ source: "reports/q3.pdf", filename: "Q3-report.pdf" });
109
+
110
+ // From raw bytes
111
+ const bytes = new Uint8Array(await someFile.bytes());
112
+ await ctx.replyWithDocument({ source: bytes, filename: "data.bin" });
113
+
114
+ // From a Blob or File (File carries its own name)
115
+ await ctx.replyWithDocument({ source: new File([bytes], "photo.png") });
116
+
117
+ // From a web ReadableStream or a Node.js stream (drained automatically)
118
+ import { createReadStream } from "node:fs";
119
+ await ctx.replyWithVideo({ source: createReadStream("clip.mp4"), filename: "clip.mp4" });
120
+ ```
121
+
122
+ Notes:
123
+ - `filename` overrides whatever name the source would otherwise have (for paths, the basename is used by default).
124
+ - **You never build `FormData` yourself.** The transport detects upload payloads and switches to multipart automatically. Hand-rolled `FormData` with Node streams fails (`append` requires a `Blob`) — always pass the stream/bytes to the library instead.
125
+ - Bare values also work: `await ctx.replyWithDocument(bytes)` (no filename) or an existing Telegram file id:
126
+
127
+ ```ts
128
+ // Re-send by file_id — no download, no upload, no size limit
129
+ await ctx.replyWithDocument(ctx.message.document.file_id);
130
+ ```
131
+
132
+ ## 5. Upload: validation before sending
133
+
134
+ `validateUpload()` / `assertValidUpload()` enforce your own rules before a byte leaves the process:
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("Usage: /doc <path>"));
142
+
143
+ const info = await stat(filePath);
144
+ try {
145
+ assertValidUpload(
146
+ { sizeBytes: info.size, fileName: filePath },
147
+ {
148
+ maxBytes: 50 * 1024 * 1024, // Telegram document cap
149
+ allowedExtensions: [".pdf", ".docx", ".pptx"], // case-insensitive
150
+ },
151
+ );
152
+ } catch (error) {
153
+ if (error instanceof UploadValidationError) {
154
+ return void (await ctx.reply(`❌ ${error.message}`)); // lists every violation
155
+ }
156
+ throw error;
157
+ }
158
+
159
+ await ctx.replyWithDocument({ source: filePath });
160
+ });
161
+ ```
162
+
163
+ `validateUpload()` returns issues instead of throwing (empty array = valid). MIME rules support wildcards:
164
+
165
+ ```ts
166
+ validateUpload({ mimeType: "image/png" }, { allowedMimeTypes: ["image/*"] }); // []
167
+ ```
168
+
169
+ Available rules: `maxBytes`, `allowedMimeTypes` (exact or `image/*` wildcards), `allowedExtensions` (dot optional, case-insensitive).
170
+
171
+ ## 6. Media groups with `attach://`
172
+
173
+ `sendMediaGroup` takes a JSON array of input media; binary files ride along as **separate form parts** referenced by `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
+ The transport detects the binary parts and switches to multipart automatically; the `media` array itself is serialized as one JSON form field, exactly as Telegram requires.
183
+
184
+ ## 7. Limits and lifetimes
185
+
186
+ | Limit | Value | Notes |
187
+ |---|---|---|
188
+ | Download via `getFile` | **20 MB** | Larger files: `getFile` fails (HTTP 400 "file is too big") — not an empty `file_path` |
189
+ | Upload photos | 10 MB | |
190
+ | Upload other files | 50 MB | |
191
+ | Re-send by `file_id` | **No limit** | Telegram already has the file |
192
+ | Send by URL | 5 MB photos / 20 MB other | Telegram fetches the URL itself |
193
+ | Download URL validity | **≥ 1 hour** | Re-run `getFile` after expiry |
194
+ | `file_path` presence | Optional in the schema | Always check before using it |
195
+
196
+ Upload limits are Telegram's, not the library's — `validateUpload()` is how you reject early with a friendly message.
197
+
198
+ ## 8. Local Bot API server
199
+
200
+ Running your own [local Bot API server](https://core.telegram.org/bots/api#using-a-local-bot-api-server) removes the 20 MB download cap and allows 2000 MB uploads:
201
+
202
+ ```ts
203
+ const bot = new Bot({
204
+ token: process.env.TELEGRAM_BOT_TOKEN!,
205
+ apiBaseUrl: "http://localhost:8081", // Bot API option
206
+ transportOptions: { timeoutMs: 600_000 },
207
+ });
208
+ ```
209
+
210
+ `downloadFile()` and `fileUrl()` map `/bot<token>` to `/file/bot<token>` on any base URL, so downloads work against local servers too. Note: with a local server, `file_path` is an **absolute path on the server's disk** — fetch the URL only if the server is remote; read the path directly when your bot runs on the same machine.
211
+
212
+ ## 9. Testing file flows without network
213
+
214
+ `MockTransport` (from `@xbibzlibrary/telebibz/testing`) implements the download members:
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"] — the recorded download
227
+ ```
228
+
229
+ See [TESTING.md](TESTING.md) for the full testing guide.
230
+
231
+ ## 10. Troubleshooting
232
+
233
+ | Symptom | Cause | Fix |
234
+ |---|---|---|
235
+ | "Gagal mendapatkan path file" while the logged response contains `file_path` | Reading `file.filePath`/`file.path` from a raw Telegram object | Use `file.file_path` (snake_case) — or skip manual handling entirely with `ctx.downloadFile()` |
236
+ | `ENOENT … open 'https://…'` on a ReadStream | `createReadStream()` only opens local paths | Use `fetch(url)` for URLs, or `ctx.downloadFile()` |
237
+ | Download URL returns 404 | Missing `bot` prefix in `/file/bot<TOKEN>/` | Use the `url` field from `downloadFile()` — it is always built correctly |
238
+ | `getFile` returns HTTP 400 "file is too big" | File over 20 MB | Use a local Bot API server, or re-send by `file_id` |
239
+ | `FormData append: parameter 2 is not of type 'Blob'` | Hand-rolled `FormData` with a Node stream | Pass `{ source: stream, filename }` to `replyWith*` instead; the library handles multipart |
240
+ | `file_path` was there, now it's gone | URL expired (>1 hour) | Call `getFile` again |
241
+ | Downloaded file is 0 bytes / wrong | `file_id` belongs to another bot | `file_id`s are bot-scoped; use the id from your own bot's updates |
242
+
243
+ Bahasa Indonesia: [FILES.id.md](FILES.id.md) · 简体中文: [FILES.zh-CN.md](FILES.zh-CN.md)
@@ -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
- - [Webhook API](API.id.md#10-webhook)
84
- - [Conversation dan wizard](API.id.md#8-state-session-and-conversations)
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)
@@ -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
- - [Webhook API](API.md#10-webhook)
84
- - [Conversations and wizards](API.md#8-state-session-and-conversations)
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
- - [Webhook API](API.zh-CN.md#10-webhook)
84
- - [Conversation 和 wizard](API.zh-CN.md#8-state-session-and-conversations)
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)