@xbibzlibrary/telebibz 0.4.5 → 3.0.1

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 (276) hide show
  1. package/CHANGELOG.md +35 -138
  2. package/LICENSE +1 -1
  3. package/NOTICE.md +9 -4
  4. package/README.md +173 -256
  5. package/examples/01-quickstart.js +13 -0
  6. package/examples/02-menu-tombol.js +22 -0
  7. package/examples/03-wizard.js +29 -0
  8. package/examples/04-broadcast.js +25 -0
  9. package/examples/05-kirim-file.js +19 -0
  10. package/examples/06-menu.js +35 -0
  11. package/examples/07-inline-query.js +18 -0
  12. package/index.d.ts +81 -0
  13. package/index.js +42 -0
  14. package/lib/api.js +154 -0
  15. package/lib/broadcast.js +35 -0
  16. package/lib/composer.js +174 -0
  17. package/lib/context.js +190 -0
  18. package/lib/errors.js +35 -0
  19. package/lib/file.js +41 -0
  20. package/lib/inline-query.js +28 -0
  21. package/lib/keyboard.js +83 -0
  22. package/lib/logger.js +38 -0
  23. package/lib/menus.js +90 -0
  24. package/lib/net.js +115 -0
  25. package/lib/ratelimit.js +61 -0
  26. package/lib/runner.js +45 -0
  27. package/lib/session.js +36 -0
  28. package/lib/telebibz.js +190 -0
  29. package/lib/wizard.js +81 -0
  30. package/package.json +35 -97
  31. package/test/all.test.js +334 -0
  32. package/CODE_OF_CONDUCT.md +0 -37
  33. package/CONTRIBUTING.md +0 -59
  34. package/CONTRIBUTION_RULES.md +0 -41
  35. package/GOVERNANCE.md +0 -47
  36. package/README.id.md +0 -306
  37. package/README.zh-CN.md +0 -306
  38. package/RELEASE_AUTOMATION.md +0 -78
  39. package/RELEASE_POLICY.md +0 -32
  40. package/SECURITY.md +0 -47
  41. package/SHOWCASE.md +0 -29
  42. package/SUPPORT.md +0 -30
  43. package/assets/readme-preview.html +0 -75
  44. package/assets/telebibz-logo.png +0 -0
  45. package/assets/telebibz-readme-preview.png +0 -0
  46. package/bin/telebibz.mjs +0 -3
  47. package/dist/generated/api.d.ts +0 -13
  48. package/dist/generated/api.d.ts.map +0 -1
  49. package/dist/generated/api.js +0 -192
  50. package/dist/generated/api.js.map +0 -1
  51. package/dist/src/api/client.d.ts +0 -62
  52. package/dist/src/api/client.d.ts.map +0 -1
  53. package/dist/src/api/client.js +0 -104
  54. package/dist/src/api/client.js.map +0 -1
  55. package/dist/src/api/errors.d.ts +0 -45
  56. package/dist/src/api/errors.d.ts.map +0 -1
  57. package/dist/src/api/errors.js +0 -65
  58. package/dist/src/api/errors.js.map +0 -1
  59. package/dist/src/api/index.d.ts +0 -6
  60. package/dist/src/api/index.d.ts.map +0 -1
  61. package/dist/src/api/index.js +0 -6
  62. package/dist/src/api/index.js.map +0 -1
  63. package/dist/src/api/telegram-types/LICENSE +0 -21
  64. package/dist/src/api/telegram-types/api.d.ts +0 -22
  65. package/dist/src/api/telegram-types/checklist.d.ts +0 -72
  66. package/dist/src/api/telegram-types/inline.d.ts +0 -692
  67. package/dist/src/api/telegram-types/langs.d.ts +0 -193
  68. package/dist/src/api/telegram-types/manage.d.ts +0 -1144
  69. package/dist/src/api/telegram-types/markup.d.ts +0 -268
  70. package/dist/src/api/telegram-types/message.d.ts +0 -1537
  71. package/dist/src/api/telegram-types/methods.d.ts +0 -2870
  72. package/dist/src/api/telegram-types/mod.d.ts +0 -14
  73. package/dist/src/api/telegram-types/passport.d.ts +0 -163
  74. package/dist/src/api/telegram-types/payment.d.ts +0 -570
  75. package/dist/src/api/telegram-types/rich.d.ts +0 -1010
  76. package/dist/src/api/telegram-types/settings.d.ts +0 -120
  77. package/dist/src/api/telegram-types/story.d.ts +0 -89
  78. package/dist/src/api/telegram-types/update.d.ts +0 -84
  79. package/dist/src/api/telegram.d.ts +0 -7
  80. package/dist/src/api/telegram.d.ts.map +0 -1
  81. package/dist/src/api/telegram.js +0 -2
  82. package/dist/src/api/telegram.js.map +0 -1
  83. package/dist/src/api/transport.d.ts +0 -68
  84. package/dist/src/api/transport.d.ts.map +0 -1
  85. package/dist/src/api/transport.js +0 -264
  86. package/dist/src/api/transport.js.map +0 -1
  87. package/dist/src/api/types.d.ts +0 -466
  88. package/dist/src/api/types.d.ts.map +0 -1
  89. package/dist/src/api/types.js +0 -2
  90. package/dist/src/api/types.js.map +0 -1
  91. package/dist/src/branding/terminal.d.ts +0 -77
  92. package/dist/src/branding/terminal.d.ts.map +0 -1
  93. package/dist/src/branding/terminal.js +0 -328
  94. package/dist/src/branding/terminal.js.map +0 -1
  95. package/dist/src/broadcast/broadcast.d.ts +0 -50
  96. package/dist/src/broadcast/broadcast.d.ts.map +0 -1
  97. package/dist/src/broadcast/broadcast.js +0 -56
  98. package/dist/src/broadcast/broadcast.js.map +0 -1
  99. package/dist/src/cache/cache.d.ts +0 -34
  100. package/dist/src/cache/cache.d.ts.map +0 -1
  101. package/dist/src/cache/cache.js +0 -41
  102. package/dist/src/cache/cache.js.map +0 -1
  103. package/dist/src/cli.d.ts +0 -2
  104. package/dist/src/cli.d.ts.map +0 -1
  105. package/dist/src/cli.js +0 -84
  106. package/dist/src/cli.js.map +0 -1
  107. package/dist/src/context/context.d.ts +0 -124
  108. package/dist/src/context/context.d.ts.map +0 -1
  109. package/dist/src/context/context.js +0 -302
  110. package/dist/src/context/context.js.map +0 -1
  111. package/dist/src/core/bot.d.ts +0 -204
  112. package/dist/src/core/bot.d.ts.map +0 -1
  113. package/dist/src/core/bot.js +0 -506
  114. package/dist/src/core/bot.js.map +0 -1
  115. package/dist/src/core/events.d.ts +0 -75
  116. package/dist/src/core/events.d.ts.map +0 -1
  117. package/dist/src/core/events.js +0 -35
  118. package/dist/src/core/events.js.map +0 -1
  119. package/dist/src/core/webhook-reply.d.ts +0 -34
  120. package/dist/src/core/webhook-reply.d.ts.map +0 -1
  121. package/dist/src/core/webhook-reply.js +0 -37
  122. package/dist/src/core/webhook-reply.js.map +0 -1
  123. package/dist/src/index.d.ts +0 -24
  124. package/dist/src/index.d.ts.map +0 -1
  125. package/dist/src/index.js +0 -24
  126. package/dist/src/index.js.map +0 -1
  127. package/dist/src/keyboard/index.d.ts +0 -46
  128. package/dist/src/keyboard/index.d.ts.map +0 -1
  129. package/dist/src/keyboard/index.js +0 -55
  130. package/dist/src/keyboard/index.js.map +0 -1
  131. package/dist/src/middleware/compose.d.ts +0 -5
  132. package/dist/src/middleware/compose.d.ts.map +0 -1
  133. package/dist/src/middleware/compose.js +0 -17
  134. package/dist/src/middleware/compose.js.map +0 -1
  135. package/dist/src/observability/logger.d.ts +0 -78
  136. package/dist/src/observability/logger.d.ts.map +0 -1
  137. package/dist/src/observability/logger.js +0 -285
  138. package/dist/src/observability/logger.js.map +0 -1
  139. package/dist/src/plugins/plugin.d.ts +0 -38
  140. package/dist/src/plugins/plugin.d.ts.map +0 -1
  141. package/dist/src/plugins/plugin.js +0 -59
  142. package/dist/src/plugins/plugin.js.map +0 -1
  143. package/dist/src/queue/queue.d.ts +0 -77
  144. package/dist/src/queue/queue.d.ts.map +0 -1
  145. package/dist/src/queue/queue.js +0 -213
  146. package/dist/src/queue/queue.js.map +0 -1
  147. package/dist/src/router/router.d.ts +0 -61
  148. package/dist/src/router/router.d.ts.map +0 -1
  149. package/dist/src/router/router.js +0 -183
  150. package/dist/src/router/router.js.map +0 -1
  151. package/dist/src/state/conversation.d.ts +0 -56
  152. package/dist/src/state/conversation.d.ts.map +0 -1
  153. package/dist/src/state/conversation.js +0 -133
  154. package/dist/src/state/conversation.js.map +0 -1
  155. package/dist/src/state/forms.d.ts +0 -34
  156. package/dist/src/state/forms.d.ts.map +0 -1
  157. package/dist/src/state/forms.js +0 -44
  158. package/dist/src/state/forms.js.map +0 -1
  159. package/dist/src/state/menu.d.ts +0 -78
  160. package/dist/src/state/menu.d.ts.map +0 -1
  161. package/dist/src/state/menu.js +0 -127
  162. package/dist/src/state/menu.js.map +0 -1
  163. package/dist/src/storage/storage.d.ts +0 -146
  164. package/dist/src/storage/storage.d.ts.map +0 -1
  165. package/dist/src/storage/storage.js +0 -195
  166. package/dist/src/storage/storage.js.map +0 -1
  167. package/dist/src/telegram-features.d.ts +0 -33
  168. package/dist/src/telegram-features.d.ts.map +0 -1
  169. package/dist/src/telegram-features.js +0 -71
  170. package/dist/src/telegram-features.js.map +0 -1
  171. package/dist/src/testing.d.ts +0 -24
  172. package/dist/src/testing.d.ts.map +0 -1
  173. package/dist/src/testing.js +0 -38
  174. package/dist/src/testing.js.map +0 -1
  175. package/dist/src/utils/concurrency.d.ts +0 -25
  176. package/dist/src/utils/concurrency.d.ts.map +0 -1
  177. package/dist/src/utils/concurrency.js +0 -52
  178. package/dist/src/utils/concurrency.js.map +0 -1
  179. package/dist/src/utils/files.d.ts +0 -45
  180. package/dist/src/utils/files.d.ts.map +0 -1
  181. package/dist/src/utils/files.js +0 -53
  182. package/dist/src/utils/files.js.map +0 -1
  183. package/dist/src/utils/text.d.ts +0 -39
  184. package/dist/src/utils/text.d.ts.map +0 -1
  185. package/dist/src/utils/text.js +0 -56
  186. package/dist/src/utils/text.js.map +0 -1
  187. package/dist/src/webhook/handler.d.ts +0 -19
  188. package/dist/src/webhook/handler.d.ts.map +0 -1
  189. package/dist/src/webhook/handler.js +0 -141
  190. package/dist/src/webhook/handler.js.map +0 -1
  191. package/dist-cjs/generated/api.js +0 -194
  192. package/dist-cjs/package.json +0 -3
  193. package/dist-cjs/src/api/client.js +0 -107
  194. package/dist-cjs/src/api/errors.js +0 -74
  195. package/dist-cjs/src/api/index.js +0 -21
  196. package/dist-cjs/src/api/telegram-types/LICENSE +0 -21
  197. package/dist-cjs/src/api/telegram-types/api.d.ts +0 -22
  198. package/dist-cjs/src/api/telegram-types/checklist.d.ts +0 -72
  199. package/dist-cjs/src/api/telegram-types/inline.d.ts +0 -692
  200. package/dist-cjs/src/api/telegram-types/langs.d.ts +0 -193
  201. package/dist-cjs/src/api/telegram-types/manage.d.ts +0 -1144
  202. package/dist-cjs/src/api/telegram-types/markup.d.ts +0 -268
  203. package/dist-cjs/src/api/telegram-types/message.d.ts +0 -1537
  204. package/dist-cjs/src/api/telegram-types/methods.d.ts +0 -2870
  205. package/dist-cjs/src/api/telegram-types/mod.d.ts +0 -14
  206. package/dist-cjs/src/api/telegram-types/passport.d.ts +0 -163
  207. package/dist-cjs/src/api/telegram-types/payment.d.ts +0 -570
  208. package/dist-cjs/src/api/telegram-types/rich.d.ts +0 -1010
  209. package/dist-cjs/src/api/telegram-types/settings.d.ts +0 -120
  210. package/dist-cjs/src/api/telegram-types/story.d.ts +0 -89
  211. package/dist-cjs/src/api/telegram-types/update.d.ts +0 -84
  212. package/dist-cjs/src/api/telegram.js +0 -2
  213. package/dist-cjs/src/api/transport.js +0 -267
  214. package/dist-cjs/src/api/types.js +0 -2
  215. package/dist-cjs/src/branding/terminal.js +0 -338
  216. package/dist-cjs/src/broadcast/broadcast.js +0 -58
  217. package/dist-cjs/src/cache/cache.js +0 -45
  218. package/dist-cjs/src/cli.js +0 -86
  219. package/dist-cjs/src/context/context.js +0 -305
  220. package/dist-cjs/src/core/bot.js +0 -510
  221. package/dist-cjs/src/core/events.js +0 -38
  222. package/dist-cjs/src/core/webhook-reply.js +0 -42
  223. package/dist-cjs/src/index.js +0 -47
  224. package/dist-cjs/src/keyboard/index.js +0 -61
  225. package/dist-cjs/src/middleware/compose.js +0 -20
  226. package/dist-cjs/src/observability/logger.js +0 -293
  227. package/dist-cjs/src/plugins/plugin.js +0 -63
  228. package/dist-cjs/src/queue/queue.js +0 -219
  229. package/dist-cjs/src/router/router.js +0 -186
  230. package/dist-cjs/src/state/conversation.js +0 -139
  231. package/dist-cjs/src/state/forms.js +0 -47
  232. package/dist-cjs/src/state/menu.js +0 -133
  233. package/dist-cjs/src/storage/storage.js +0 -202
  234. package/dist-cjs/src/telegram-features.js +0 -76
  235. package/dist-cjs/src/testing.js +0 -45
  236. package/dist-cjs/src/utils/concurrency.js +0 -57
  237. package/dist-cjs/src/utils/files.js +0 -58
  238. package/dist-cjs/src/utils/text.js +0 -63
  239. package/dist-cjs/src/webhook/handler.js +0 -144
  240. package/docs/API.id.md +0 -1935
  241. package/docs/API.md +0 -1969
  242. package/docs/API.zh-CN.md +0 -1929
  243. package/docs/COOKBOOK.id.md +0 -321
  244. package/docs/COOKBOOK.md +0 -321
  245. package/docs/COOKBOOK.zh-CN.md +0 -321
  246. package/docs/ERRORS.id.md +0 -194
  247. package/docs/ERRORS.md +0 -194
  248. package/docs/ERRORS.zh-CN.md +0 -194
  249. package/docs/FILES.id.md +0 -243
  250. package/docs/FILES.md +0 -243
  251. package/docs/FILES.zh-CN.md +0 -243
  252. package/docs/GETTING_STARTED.id.md +0 -89
  253. package/docs/GETTING_STARTED.md +0 -89
  254. package/docs/GETTING_STARTED.zh-CN.md +0 -89
  255. package/docs/GITHUB_PACKAGES.id.md +0 -82
  256. package/docs/GITHUB_PACKAGES.md +0 -82
  257. package/docs/GITHUB_PACKAGES.zh-CN.md +0 -82
  258. package/docs/MIGRATION_TELEGRAF.id.md +0 -147
  259. package/docs/MIGRATION_TELEGRAF.md +0 -154
  260. package/docs/MIGRATION_TELEGRAF.zh-CN.md +0 -147
  261. package/docs/README.md +0 -67
  262. package/docs/STORAGE.id.md +0 -105
  263. package/docs/STORAGE.md +0 -105
  264. package/docs/STORAGE.zh-CN.md +0 -105
  265. package/docs/TESTING.id.md +0 -203
  266. package/docs/TESTING.md +0 -203
  267. package/docs/TESTING.zh-CN.md +0 -203
  268. package/docs/WEBHOOK.id.md +0 -212
  269. package/docs/WEBHOOK.md +0 -215
  270. package/docs/WEBHOOK.zh-CN.md +0 -212
  271. package/examples/README.md +0 -37
  272. package/examples/files.ts +0 -35
  273. package/examples/minimal.ts +0 -12
  274. package/examples/tsconfig.json +0 -9
  275. package/examples/webhook.ts +0 -42
  276. package/examples/wizard-registration.ts +0 -42
package/docs/ERRORS.md DELETED
@@ -1,194 +0,0 @@
1
- # Error handling guide (English)
2
-
3
- Every error path in telebibz, what it means, and how to handle it — from Telegram API errors to handler failures, timeouts, and shutdown.
4
-
5
- ## Contents
6
-
7
- 1. [The error taxonomy](#1-the-error-taxonomy)
8
- 2. [TelegramError anatomy](#2-telegramerror-anatomy)
9
- 3. [429 rate limits and the flood gate](#3-429-rate-limits-and-the-flood-gate)
10
- 4. [Handler errors and `bot.catch()`](#4-handler-errors-and-botcatch)
11
- 5. [handlerTimeout and `UpdateTimeoutError`](#5-handlertimeout-and-updatetimeouterror)
12
- 6. [Event-based error observation](#6-event-based-error-observation)
13
- 7. [Transport retries and network errors](#7-transport-retries-and-network-errors)
14
- 8. [Error handling recipes](#8-error-handling-recipes)
15
-
16
- ## 1. The error taxonomy
17
-
18
- Every Telegram API failure is a `TelegramError` with a `kind`:
19
-
20
- | `kind` | Subclass | Meaning | Typical trigger |
21
- |---|---|---|---|
22
- | `rate-limit` | `TelegramRateLimitError` | Telegram answered 429 | Sending too fast; `retryAfter` is set |
23
- | `authentication` | `TelegramAuthError` | Token invalid/revoked (401) | Wrong token, revoked bot, logout |
24
- | `validation` | `TelegramValidationError` | Bad request parameters (400) | Unknown `file_id`, malformed payload |
25
- | `network` | `TelegramNetworkError` | Transport-level failure | DNS, socket, non-JSON response, download failure |
26
- | `server` | — | Telegram server error (5xx) | Transient; retried automatically |
27
- | `retryable` | — | Other retryable Telegram error | Flood-wait variants |
28
- | `unknown` | — | Anything else | — |
29
-
30
- Check `kind` when the reaction should differ:
31
-
32
- ```ts
33
- import { TelegramError, TelegramRateLimitError } from "@xbibzlibrary/telebibz";
34
-
35
- try {
36
- await ctx.reply("hello");
37
- } catch (error) {
38
- if (error instanceof TelegramError) {
39
- switch (error.kind) {
40
- case "rate-limit": console.log(`slow down ${error.retryAfter}s`); break;
41
- case "authentication": console.error("token problem — stopping"); break;
42
- case "validation": console.warn(error.message); break;
43
- default: console.error(error.message);
44
- }
45
- }
46
- }
47
- ```
48
-
49
- `instanceof TelegramRateLimitError` works too when you only care about 429s.
50
-
51
- ## 2. TelegramError anatomy
52
-
53
- ```ts
54
- try {
55
- await bot.api.methods.getChatMember({ chat_id: -100123, user_id: 42 });
56
- } catch (error) {
57
- if (error instanceof TelegramError) {
58
- error.kind; // one of the seven kinds above
59
- error.errorCode; // Telegram error_code (400, 401, 429, …) when present
60
- error.method; // "getChatMember" — the failing call
61
- error.payload; // the payload that was sent
62
- error.retryAfter; // seconds, only for 429 (from parameters.retry_after)
63
- error.message; // Telegram's description
64
- }
65
- }
66
- ```
67
-
68
- ## 3. 429 rate limits and the flood gate
69
-
70
- You normally never see a 429, because the transport handles them for you:
71
-
72
- 1. Telegram answers 429 with `parameters.retry_after`.
73
- 2. The **flood gate** pauses *new* outgoing requests for exactly that window — protecting all in-flight traffic, not just the request that was rejected.
74
- 3. The failed request is retried automatically, then the failure (if it persists) surfaces as `TelegramRateLimitError`.
75
-
76
- The flood gate is the **only** delay the library ever introduces — it is never a proactive cooldown. Tune or disable it per transport:
77
-
78
- ```ts
79
- const bot = new Bot({
80
- token: process.env.TELEGRAM_BOT_TOKEN!,
81
- transportOptions: {
82
- floodGate: false, // handle 429s entirely yourself
83
- retries: 3, // retry count for 429/5xx/network errors
84
- backoffMs: 250, // exponential backoff base
85
- maxBackoffMs: 8_000,
86
- timeoutMs: 30_000, // per-request timeout
87
- },
88
- });
89
- ```
90
-
91
- If you disable the gate, catch `TelegramRateLimitError` and honor `retryAfter` yourself — Telegram's word is final:
92
-
93
- ```ts
94
- catch (error) {
95
- if (error instanceof TelegramRateLimitError) {
96
- await sleep((error.retryAfter ?? 1) * 1000);
97
- return retry();
98
- }
99
- throw error;
100
- }
101
- ```
102
-
103
- ## 4. Handler errors and `bot.catch()`
104
-
105
- Without an error boundary, a throwing handler rejects `handleUpdate()` (and a webhook answers 500). With `bot.catch()`, failures are routed to one place:
106
-
107
- ```ts
108
- bot.catch(async (error, ctx) => {
109
- console.error("handler failed:", error);
110
-
111
- if (error instanceof TelegramError && error.kind === "authentication") {
112
- process.exitCode = 1; // unrecoverable — let the supervisor restart
113
- await bot.stop();
114
- return;
115
- }
116
-
117
- await ctx.reply("❌ Terjadi kesalahan. Coba lagi."); // ctx is the failing update's context
118
- });
119
- ```
120
-
121
- Key behaviors:
122
- - Only the failing update is affected — other chats keep processing concurrently.
123
- - Per-chat ordering is preserved: the next update of the same chat still waits for this one.
124
- - `broadcast()` failures are collected in the report instead of throwing.
125
-
126
- ## 5. handlerTimeout and `UpdateTimeoutError`
127
-
128
- `handlerTimeout` (default **90 000 ms**, matching Telegraf) protects the pipeline from hung handlers:
129
-
130
- ```ts
131
- const bot = new Bot({ token, handlerTimeout: 30_000 }); // 0 or Infinity disables
132
- ```
133
-
134
- - The hung update's `handleUpdate()` promise rejects with `UpdateTimeoutError` and flows through `update:error` → `bot:error` → `bot.catch()`.
135
- - **The handler keeps running in the background** — sessions and conversations still complete their writes; the timeout only releases the pipeline.
136
- - Per-chat ordering is unaffected.
137
-
138
- ## 6. Event-based error observation
139
-
140
- For metrics/ logging independent of the boundary, listen on the event bus:
141
-
142
- ```ts
143
- bot.events.on("update:error", ({ update, error }) => {
144
- metrics.increment("handler_errors", { updateId: (update as { update_id?: number }).update_id });
145
- });
146
- bot.events.on("bot:error", ({ error }) => log.error("bot error", { error }));
147
- bot.events.on("api:response", ({ method, durationMs }) => {
148
- if (durationMs > 3_000) log.warn("slow api call", { method, durationMs });
149
- });
150
- ```
151
-
152
- ## 7. Transport retries and network errors
153
-
154
- `FetchTransport` retries automatically on: 429 (with Telegram's `retry_after`), 5xx, network errors, and non-JSON responses — with exponential backoff and jitter. After `retries` attempts the last error surfaces as `TelegramNetworkError` (with `status` and a truncated cause). Authentication errors (401) and validation errors (400) are **never** retried — retrying cannot fix them.
155
-
156
- ## 8. Error handling recipes
157
-
158
- **Retry with backoff around a single call** (beyond transport retries):
159
-
160
- ```ts
161
- async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
162
- for (let i = 0; i < attempts; i++) {
163
- try { return await run(); }
164
- catch (error) {
165
- if (error instanceof TelegramError && ["authentication", "validation"].includes(error.kind)) throw error;
166
- if (i === attempts - 1) throw error;
167
- await new Promise((r) => setTimeout(r, 500 * 2 ** i));
168
- }
169
- }
170
- throw new Error("unreachable");
171
- }
172
- ```
173
-
174
- **Fail fast on bad tokens** (polling with an invalid token):
175
-
176
- ```ts
177
- try {
178
- await bot.start();
179
- } catch (error) {
180
- if (error instanceof TelegramError && error.kind === "authentication") {
181
- console.error("TELEGRAM_BOT_TOKEN is invalid or revoked");
182
- }
183
- throw error;
184
- }
185
- ```
186
-
187
- **Graceful shutdown** — `stop()` drains in-flight handlers first:
188
-
189
- ```ts
190
- process.on("SIGINT", () => { void bot.stop().then(() => process.exit(0)); });
191
- process.on("SIGTERM", () => { void bot.stop().then(() => process.exit(0)); });
192
- ```
193
-
194
- Bahasa Indonesia: [ERRORS.id.md](ERRORS.id.md) · 简体中文: [ERRORS.zh-CN.md](ERRORS.zh-CN.md)
@@ -1,194 +0,0 @@
1
- # 错误处理指南(简体中文)
2
-
3
- telebibz 中每一条错误路径、其含义与处理方式 —— 从 Telegram API 错误到 handler 失败、超时与关机。
4
-
5
- ## 目录
6
-
7
- 1. [错误分类](#1-错误分类)
8
- 2. [TelegramError 解剖](#2-telegramerror-解剖)
9
- 3. [429 限流与 flood gate](#3-429-限流与-flood-gate)
10
- 4. [错误处理器与 `bot.catch()`](#4-错误处理器与-botcatch)
11
- 5. [handlerTimeout 与 `UpdateTimeoutError`](#5-handlertimeout-与-updatetimeouterror)
12
- 6. [基于事件的错误观测](#6-基于事件的错误观测)
13
- 7. [传输层重试与网络错误](#7-传输层重试与网络错误)
14
- 8. [错误处理配方](#8-错误处理配方)
15
-
16
- ## 1. 错误分类
17
-
18
- 每个 Telegram API 失败都是带 `kind` 的 `TelegramError`:
19
-
20
- | `kind` | 子类 | 含义 | 常见诱因 |
21
- |---|---|---|---|
22
- | `rate-limit` | `TelegramRateLimitError` | Telegram 返回 429 | 发送过快;`retryAfter` 有值 |
23
- | `authentication` | `TelegramAuthError` | token 无效/被吊销(401) | token 错误、bot 被吊销、logout |
24
- | `validation` | `TelegramValidationError` | 请求参数错误(400) | 未知的 `file_id`、损坏的负载 |
25
- | `network` | `TelegramNetworkError` | 传输层失败 | DNS、socket、非 JSON 响应、下载失败 |
26
- | `server` | — | Telegram 服务器错误(5xx) | 瞬时故障;自动重试 |
27
- | `retryable` | — | 其他可重试的 Telegram 错误 | flood-wait 变体 |
28
- | `unknown` | — | 其他 | — |
29
-
30
- 当不同错误需要不同反应时,检查 `kind`:
31
-
32
- ```ts
33
- import { TelegramError, TelegramRateLimitError } from "@xbibzlibrary/telebibz";
34
-
35
- try {
36
- await ctx.reply("hello");
37
- } catch (error) {
38
- if (error instanceof TelegramError) {
39
- switch (error.kind) {
40
- case "rate-limit": console.log(`放慢 ${error.retryAfter}s`); break;
41
- case "authentication": console.error("token 问题 —— 停机"); break;
42
- case "validation": console.warn(error.message); break;
43
- default: console.error(error.message);
44
- }
45
- }
46
- }
47
- ```
48
-
49
- 如果只关心 429,也可以用 `instanceof TelegramRateLimitError`。
50
-
51
- ## 2. TelegramError 解剖
52
-
53
- ```ts
54
- try {
55
- await bot.api.methods.getChatMember({ chat_id: -100123, user_id: 42 });
56
- } catch (error) {
57
- if (error instanceof TelegramError) {
58
- error.kind; // 上表七种 kind 之一
59
- error.errorCode; // Telegram 的 error_code(400、401、429……),如存在
60
- error.method; // "getChatMember" —— 失败的调用
61
- error.payload; // 发出的负载
62
- error.retryAfter; // 秒数,仅 429(来自 parameters.retry_after)
63
- error.message; // Telegram 的描述
64
- }
65
- }
66
- ```
67
-
68
- ## 3. 429 限流与 flood gate
69
-
70
- 通常你根本见不到 429,因为传输层已经处理了:
71
-
72
- 1. Telegram 返回带 `parameters.retry_after` 的 429。
73
- 2. **flood gate** 在该时间窗内暂停*新的*外发请求 —— 保护整个流量,而不只是被拒绝的那个请求。
74
- 3. 被拒请求自动重试;若仍失败,最终以 `TelegramRateLimitError` 抛出。
75
-
76
- flood gate 是本库引入的唯一暂停机制 —— 从不做主动冷却。可按传输层调整或关闭:
77
-
78
- ```ts
79
- const bot = new Bot({
80
- token: process.env.TELEGRAM_BOT_TOKEN!,
81
- transportOptions: {
82
- floodGate: false, // 完全自行处理 429
83
- retries: 3, // 429/5xx/网络错误的重试次数
84
- backoffMs: 250, // 指数退避基数
85
- maxBackoffMs: 8_000,
86
- timeoutMs: 30_000, // 单个请求超时
87
- },
88
- });
89
- ```
90
-
91
- 关闭 gate 后,请捕获 `TelegramRateLimitError` 并遵守 `retryAfter` —— Telegram 说了算:
92
-
93
- ```ts
94
- catch (error) {
95
- if (error instanceof TelegramRateLimitError) {
96
- await sleep((error.retryAfter ?? 1) * 1000);
97
- return retry();
98
- }
99
- throw error;
100
- }
101
- ```
102
-
103
- ## 4. 错误处理器与 `bot.catch()`
104
-
105
- 没有错误边界时,抛异常的 handler 会让 `handleUpdate()` 拒绝(webhook 则返回 500)。使用 `bot.catch()` 把失败引到一处:
106
-
107
- ```ts
108
- bot.catch(async (error, ctx) => {
109
- console.error("handler 失败:", error);
110
-
111
- if (error instanceof TelegramError && error.kind === "authentication") {
112
- process.exitCode = 1; // 不可恢复 —— 交给监督进程重启
113
- await bot.stop();
114
- return;
115
- }
116
-
117
- await ctx.reply("❌ 出错了,请稍后再试。"); // ctx = 失败 update 的上下文
118
- });
119
- ```
120
-
121
- 关键行为:
122
- - 只有失败的 update 受影响 —— 其他 chat 继续并发处理。
123
- - 每 chat 顺序保持不变:同一 chat 的后续 update 仍会排队等待。
124
- - `broadcast()` 的失败收集在报告中,而不是抛异常。
125
-
126
- ## 5. handlerTimeout 与 `UpdateTimeoutError`
127
-
128
- `handlerTimeout`(默认 **90 000 毫秒**,与 Telegraf 一致)保护流水线免受挂起 handler 的拖累:
129
-
130
- ```ts
131
- const bot = new Bot({ token, handlerTimeout: 30_000 }); // 0 或 Infinity 表示禁用
132
- ```
133
-
134
- - 挂起 update 的 `handleUpdate()` promise 抛出 `UpdateTimeoutError`,并依次流经 `update:error` → `bot:error` → `bot.catch()`。
135
- - **handler 仍在后台运行** —— session 与 conversation 照常完成写入;超时只释放流水线。
136
- - 每 chat 顺序不受影响。
137
-
138
- ## 6. 基于事件的错误观测
139
-
140
- 要独立于错误边界的指标/日志,监听事件总线:
141
-
142
- ```ts
143
- bot.events.on("update:error", ({ update, error }) => {
144
- metrics.increment("handler_errors", { updateId: (update as { update_id?: number }).update_id });
145
- });
146
- bot.events.on("bot:error", ({ error }) => log.error("bot error", { error }));
147
- bot.events.on("api:response", ({ method, durationMs }) => {
148
- if (durationMs > 3_000) log.warn("慢速 API 调用", { method, durationMs });
149
- });
150
- ```
151
-
152
- ## 7. 传输层重试与网络错误
153
-
154
- `FetchTransport` 自动重试:429(遵守 Telegram 的 `retry_after`)、5xx、网络错误、非 JSON 响应 —— 采用指数退避加抖动。重试 `retries` 次后,最后一个错误以 `TelegramNetworkError` 抛出(含 `status` 与截断的 cause)。认证错误(401)和校验错误(400)**从不**重试 —— 重试无济于事。
155
-
156
- ## 8. 错误处理配方
157
-
158
- **围绕单次调用的退避重试**(在传输层重试之外):
159
-
160
- ```ts
161
- async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
162
- for (let i = 0; i < attempts; i++) {
163
- try { return await run(); }
164
- catch (error) {
165
- if (error instanceof TelegramError && ["authentication", "validation"].includes(error.kind)) throw error;
166
- if (i === attempts - 1) throw error;
167
- await new Promise((r) => setTimeout(r, 500 * 2 ** i));
168
- }
169
- }
170
- throw new Error("unreachable");
171
- }
172
- ```
173
-
174
- **token 无效时快速失败**(用错误 token 轮询):
175
-
176
- ```ts
177
- try {
178
- await bot.start();
179
- } catch (error) {
180
- if (error instanceof TelegramError && error.kind === "authentication") {
181
- console.error("TELEGRAM_BOT_TOKEN 无效或已吊销");
182
- }
183
- throw error;
184
- }
185
- ```
186
-
187
- **优雅关机** —— `stop()` 先排空在途 handler:
188
-
189
- ```ts
190
- process.on("SIGINT", () => { void bot.stop().then(() => process.exit(0)); });
191
- process.on("SIGTERM", () => { void bot.stop().then(() => process.exit(0)); });
192
- ```
193
-
194
- English: [ERRORS.md](ERRORS.md) · Bahasa Indonesia: [ERRORS.id.md](ERRORS.id.md)
package/docs/FILES.id.md DELETED
@@ -1,243 +0,0 @@
1
- # Bekerja dengan file: upload, download, validasi (Bahasa Indonesia)
2
-
3
- Panduan lengkap setiap alur file di telebibz: mengunduh file yang dikirim user, mengunggah file ke Telegram, mengirim ulang via `file_id`, memvalidasi unggahan, serta batasan dan jebakan Telegram Bot API.
4
-
5
- ## Daftar isi
6
-
7
- 1. [Download: satu panggilan dengan `downloadFile()`](#1-download-satu-panggilan-dengan-downloadfile)
8
- 2. [Download: alur manual dengan `getFile()`](#2-download-alur-manual-dengan-getfile)
9
- 3. [Penamaan property: `file_path` vs `filePath`](#3-penamaan-property-file_path-vs-filepath)
10
- 4. [Upload: semua tipe sumber](#4-upload-semua-tipe-sumber)
11
- 5. [Upload: validasi sebelum mengirim](#5-upload-validasi-sebelum-mengirim)
12
- 6. [Media group dengan `attach://`](#6-media-group-dengan-attach)
13
- 7. [Batasan dan masa berlaku](#7-batasan-dan-masa-berlaku)
14
- 8. [Local Bot API server](#8-local-bot-api-server)
15
- 9. [Menguji alur file tanpa jaringan](#9-menguji-alur-file-tanpa-jaringan)
16
- 10. [Troubleshooting](#10-troubleshooting)
17
-
18
- ## 1. Download: satu panggilan dengan `downloadFile()`
19
-
20
- `bot.downloadFile()` / `ctx.downloadFile()` me-resolve `file_id` lewat `getFile` lalu mengunduh byte mentahnya dalam satu panggilan:
21
-
22
- ```ts
23
- bot.on("message:document", async (ctx) => {
24
- const fileId = ctx.message.document.file_id;
25
-
26
- // Unduh ke memori…
27
- const file = await ctx.downloadFile(fileId);
28
- console.log(file.fileName, file.sizeBytes, file.url);
29
- // file.bytes adalah Uint8Array
30
-
31
- // …atau langsung ke disk
32
- const saved = await ctx.downloadFile(fileId, { destination: "downloads/report.pdf" });
33
- console.log(`Tersimpan di ${saved.savedTo}`);
34
- });
35
- ```
36
-
37
- Hasilnya (`DownloadedFile`) membawa semua yang dibutuhkan:
38
-
39
- | Field | Arti |
40
- |---|---|
41
- | `file` | Objek `File` Telegram yang dikembalikan `getFile` |
42
- | `bytes` | Byte mentah file (`Uint8Array`) |
43
- | `filePath` | `file_path` yang dipakai untuk unduhan |
44
- | `url` | URL unduhan langsung — valid **minimal 1 jam** |
45
- | `fileName` | Segmen path terakhir `filePath` (mis. `report.pdf`) |
46
- | `sizeBytes` | Panjang byte `bytes` |
47
- | `savedTo` | Path lokal, terisi saat Anda memberi `destination` |
48
-
49
- Error-nya presisi:
50
- - Telegram tidak mengembalikan `file_path` → `TelegramError` dengan `kind: "validation"`
51
- - Unduhan HTTP-nya sendiri gagal → `TelegramNetworkError` (lengkap dengan status)
52
- - `getFile` gagal (file_id salah, file terlalu besar) → `TelegramError` asli dari Telegram
53
-
54
- Keduanya menerima `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: alur manual dengan `getFile()`
63
-
64
- Kalau ingin membangun URL sendiri (mis. untuk HTTP client lain):
65
-
66
- ```ts
67
- const file = await ctx.getFile(fileId); // objek File Telegram
68
- if (!file.file_path) throw new Error("File tidak tersedia (di atas 20 MB atau kedaluwarsa)");
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 — JANGAN createReadStream (tidak bisa membuka URL)
72
- if (!response.ok) throw new Error(`Unduhan gagal: HTTP ${response.status}`);
73
- const bytes = new Uint8Array(await response.arrayBuffer());
74
- ```
75
-
76
- Tiga aturan yang terkandung di snippet ini:
77
- 1. Prefix URL-nya `/file/bot<TOKEN>/` — **kata `bot` wajib ada**. Lupa menuliskannya adalah kesalahan paling umum dan hasilnya `404 Not Found`.
78
- 2. `fetch` yang mengunduh byte; `fs.createReadStream()` hanya membuka **path lokal** — diberi URL ia akan crash dengan `ENOENT` pada stream error yang tidak tertangani.
79
- 3. URL dijamin valid **minimal 1 jam**. Jangan pernah cache lama; panggil `getFile` lagi setelah kedaluwarsa.
80
-
81
- ## 3. Penamaan property: `file_path` vs `filePath`
82
-
83
- Ini menjegal semua orang minimal sekali. Dua gaya penamaan ini milik layer yang berbeda:
84
-
85
- | Penamaan | Milik | Contoh |
86
- |---|---|---|
87
- | `snake_case` (`file_path`) | **Objek mentah Telegram** — hasil `getFile()`, `ctx.message.document`, `ctx.message.photo` | `file.file_path`, `document.file_id`, `photo.file_unique_id` |
88
- | `camelCase` (`filePath`) | **Tipe hasil telebibz** — `DownloadedFile` dan opsi library | `downloaded.filePath`, `downloaded.fileName`, `downloaded.sizeBytes` |
89
-
90
- ```ts
91
- const file = await ctx.getFile(fileId);
92
- file.file_path; // ✅ objek Telegram → snake_case
93
- file.filePath; // ❌ undefined — itu nama milik DownloadedFile
94
-
95
- const downloaded = await ctx.downloadFile(fileId);
96
- downloaded.filePath; // ✅ hasil library → camelCase
97
- downloaded.file_path; // ❌ undefined
98
- ```
99
-
100
- Kalau cek "tidak ada path" Anda gagal padahal response yang di-log jelas memuat `file_path`, berarti Anda membaca property camelCase dari objek snake_case.
101
-
102
- ## 4. Upload: semua tipe sumber
103
-
104
- Semua pengirim `replyWith*` dan panggilan API mentah menerima tipe `InputFile`. Kirim unggahan sebagai `{ source, filename? }`:
105
-
106
- ```ts
107
- // Dari path di disk (absolut, ./, atau ../)
108
- await ctx.replyWithDocument({ source: "reports/q3.pdf", filename: "Q3-report.pdf" });
109
-
110
- // Dari byte mentah
111
- const bytes = new Uint8Array(await someFile.bytes());
112
- await ctx.replyWithDocument({ source: bytes, filename: "data.bin" });
113
-
114
- // Dari Blob atau File (File membawa namanya sendiri)
115
- await ctx.replyWithDocument({ source: new File([bytes], "photo.png") });
116
-
117
- // Dari web ReadableStream atau stream Node (dikuras otomatis)
118
- import { createReadStream } from "node:fs";
119
- await ctx.replyWithVideo({ source: createReadStream("clip.mp4"), filename: "clip.mp4" });
120
- ```
121
-
122
- Catatan:
123
- - `filename` menimpa nama apa pun yang dimiliki sumber (untuk path, basename dipakai secara default).
124
- - **Anda tidak pernah merakit `FormData` sendiri.** Transport mendeteksi payload unggahan dan beralih ke multipart otomatis. `FormData` buatan tangan dengan stream Node pasti gagal (`append` butuh `Blob`) — selalu serahkan stream/byte ke library.
125
- - Nilai telanjang juga bisa: `await ctx.replyWithDocument(bytes)` (tanpa nama) atau file id Telegram yang sudah ada:
126
-
127
- ```ts
128
- // Kirim ulang via file_id — tanpa unduh, tanpa upload, tanpa batas ukuran
129
- await ctx.replyWithDocument(ctx.message.document.file_id);
130
- ```
131
-
132
- ## 5. Upload: validasi sebelum mengirim
133
-
134
- `validateUpload()` / `assertValidUpload()` menegakkan aturan Anda sendiri sebelum satu byte pun keluar dari proses:
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("Penggunaan: /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, // batas dokumen Telegram
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}`)); // mencantumkan semua pelanggaran
155
- }
156
- throw error;
157
- }
158
-
159
- await ctx.replyWithDocument({ source: filePath });
160
- });
161
- ```
162
-
163
- `validateUpload()` mengembalikan daftar issue alih-alih melempar (array kosong = valid). Aturan MIME mendukung wildcard:
164
-
165
- ```ts
166
- validateUpload({ mimeType: "image/png" }, { allowedMimeTypes: ["image/*"] }); // []
167
- ```
168
-
169
- Aturan yang tersedia: `maxBytes`, `allowedMimeTypes` (persis atau wildcard `image/*`), `allowedExtensions` (titik opsional, case-insensitive).
170
-
171
- ## 6. Media group dengan `attach://`
172
-
173
- `sendMediaGroup` menerima array JSON input media; file biner ikut sebagai **bagian form terpisah** yang direferensikan lewat `attach://<nama>`:
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
- Transport mendeteksi bagian biner dan beralih ke multipart otomatis; array `media` sendiri diserialisasi sebagai satu field JSON — persis kontrak Telegram.
183
-
184
- ## 7. Batasan dan masa berlaku
185
-
186
- | Batasan | Nilai | Catatan |
187
- |---|---|---|
188
- | Unduhan via `getFile` | **20 MB** | File lebih besar: `getFile` gagal (HTTP 400 "file is too big") — bukan `file_path` kosong |
189
- | Upload foto | 10 MB | |
190
- | Upload file lain | 50 MB | |
191
- | Kirim ulang via `file_id` | **Tanpa batas** | File sudah ada di Telegram |
192
- | Kirim via URL | 5 MB foto / 20 MB lainnya | Telegram yang mengambil URL-nya |
193
- | Validitas URL unduhan | **≥ 1 jam** | Jalankan `getFile` lagi setelah kedaluwarsa |
194
- | Kehadiran `file_path` | Opsional di skema | Selalu cek sebelum dipakai |
195
-
196
- Batas upload milik Telegram, bukan library — `validateUpload()` adalah cara Anda menolak lebih awal dengan pesan yang ramah.
197
-
198
- ## 8. Local Bot API server
199
-
200
- Menjalankan [local Bot API server](https://core.telegram.org/bots/api#using-a-local-bot-api-server) sendiri menghapus batas unduhan 20 MB dan mengizinkan upload hingga 2000 MB:
201
-
202
- ```ts
203
- const bot = new Bot({
204
- token: process.env.TELEGRAM_BOT_TOKEN!,
205
- apiBaseUrl: "http://localhost:8081", // opsi Bot API
206
- transportOptions: { timeoutMs: 600_000 },
207
- });
208
- ```
209
-
210
- `downloadFile()` dan `fileUrl()` memetakan `/bot<token>` ke `/file/bot<token>` di base URL mana pun, jadi unduhan juga bekerja lewat server lokal. Catatan: pada server lokal, `file_path` berupa **path absolut di disk server** — fetch URL-nya hanya bila server jarak jauh; baca path-nya langsung bila bot berjalan di mesin yang sama.
211
-
212
- ## 9. Menguji alur file tanpa jaringan
213
-
214
- `MockTransport` (dari `@xbibzlibrary/telebibz/testing`) mengimplementasikan member download:
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"] — unduhan yang tercatat
227
- ```
228
-
229
- Lihat [TESTING.id.md](TESTING.id.md) untuk panduan testing lengkap.
230
-
231
- ## 10. Troubleshooting
232
-
233
- | Gejala | Penyebab | Perbaikan |
234
- |---|---|---|
235
- | "Gagal mendapatkan path file" padahal response yang di-log memuat `file_path` | Membaca `file.filePath`/`file.path` dari objek mentah Telegram | Pakai `file.file_path` (snake_case) — atau lewati manual sepenuhnya dengan `ctx.downloadFile()` |
236
- | `ENOENT … open 'https://…'` pada ReadStream | `createReadStream()` hanya membuka path lokal | Pakai `fetch(url)` untuk URL, atau `ctx.downloadFile()` |
237
- | URL unduhan mengembalikan 404 | Prefix `bot` hilang di `/file/bot<TOKEN>/` | Pakai field `url` dari `downloadFile()` — selalu dibangun dengan benar |
238
- | `getFile` mengembalikan HTTP 400 "file is too big" | File di atas 20 MB | Pakai local Bot API server, atau kirim ulang via `file_id` |
239
- | `FormData append: parameter 2 is not of type 'Blob'` | `FormData` buatan tangan berisi stream Node | Serahkan `{ source: stream, filename }` ke `replyWith*`; library yang mengurus multipart |
240
- | `file_path` tadinya ada, sekarang hilang | URL kedaluwarsa (>1 jam) | Panggil `getFile` lagi |
241
- | File terunduh 0 byte / salah | `file_id` milik bot lain | `file_id` bersifat per-bot; pakai id dari update bot Anda sendiri |
242
-
243
- English: [FILES.md](FILES.md) · 简体中文: [FILES.zh-CN.md](FILES.zh-CN.md)