@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
@@ -0,0 +1,321 @@
1
+ # Cookbook produksi (Bahasa Indonesia)
2
+
3
+ Resep lengkap dan terverifikasi untuk kebutuhan bot sungguhan: rate limit per user, middleware auth, broadcast, tugas terjadwal, antrian background, menu berpaginasi, form, cache, validasi Mini App, dan pembayaran. Setiap resep berdiri sendiri โ€” salin ke bot Anda dan sesuaikan namanya.
4
+
5
+ ## Daftar isi
6
+
7
+ 1. [Rate limit per user](#1-rate-limit-per-user)
8
+ 2. [Middleware auth (allowlist / khusus admin)](#2-middleware-auth-allowlist--khusus-admin)
9
+ 3. [Broadcast ke ribuan user](#3-broadcast-ke-ribuan-user)
10
+ 4. [Pesan terjadwal (interval, sekali, cron)](#4-pesan-terjadwal-interval-sekali-cron)
11
+ 5. [Job background dengan retry](#5-job-background-dengan-retry)
12
+ 6. [Menu berpaginasi](#6-menu-berpaginasi)
13
+ 7. [Menu dengan permission](#7-menu-dengan-permission)
14
+ 8. [Form multi-step dengan validasi](#8-form-multi-step-dengan-validasi)
15
+ 9. [Mengedit pesan dan inline keyboard](#9-mengedit-pesan-dan-inline-keyboard)
16
+ 10. [Cache hasil mahal](#10-cache-hasil-mahal)
17
+ 11. [Validasi initData Mini App](#11-validasi-initdata-mini-app)
18
+ 12. [Pembayaran dengan Telegram Stars / invoice](#12-pembayaran-dengan-telegram-stars--invoice)
19
+ 13. [Logging terstruktur dan hook metrik](#13-logging-terstruktur-dan-hook-metrik)
20
+
21
+ ## 1. Rate limit per user
22
+
23
+ `TokenBucketLimiter` menjaga bucket independen per kunci โ€” kuncikan berdasarkan user atau chat:
24
+
25
+ ```ts
26
+ import { Bot, TokenBucketLimiter } from "@xbibzlibrary/telebibz";
27
+
28
+ const limiter = new TokenBucketLimiter(5, 0.5); // burst 5, isi ulang 0.5 token/s (= 1 pesan per 2s berkelanjutan)
29
+
30
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
31
+ bot.use(async (ctx, next) => {
32
+ const key = `user:${ctx.from?.id ?? "anon"}`;
33
+ const result = limiter.consume(key);
34
+ if (!result.allowed) {
35
+ const seconds = Math.ceil((result.retryAfterMs ?? 1000) / 1000);
36
+ await ctx.reply(`โณ Terlalu banyak permintaan. Coba lagi dalam ${seconds} detik.`);
37
+ return; // jangan panggil next(): update dijatuhkan
38
+ }
39
+ await next();
40
+ });
41
+ ```
42
+
43
+ `consume(key, cost)` mendukung aksi berbobot (mis. unggahan biaya 5, teks biaya 1). `limiter.clear(key?)` mereset state. Kombinasikan dengan flood gate transport โ€” limiter ini membentuk *user Anda*; flood gate mematuhi *Telegram*.
44
+
45
+ ## 2. Middleware auth (allowlist / khusus admin)
46
+
47
+ ```ts
48
+ const ADMINS = new Set([Number(process.env.ADMIN_ID)]);
49
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
50
+
51
+ bot.use(async (ctx, next) => {
52
+ if (ADMINS.has(ctx.from?.id ?? 0)) return await next(); // admin: semua boleh
53
+ if (ctx.chat?.type === "private") return await next(); // DM: diizinkan
54
+ return undefined; // grup: jatuhkan diam-diam
55
+ });
56
+
57
+ bot.command("stats", async (ctx) => { // route khusus admin
58
+ if (!ADMINS.has(ctx.from?.id ?? 0)) return;
59
+ await ctx.reply("Statistik rahasia");
60
+ });
61
+ ```
62
+
63
+ ## 3. Broadcast ke ribuan user
64
+
65
+ ```ts
66
+ const report = await bot.broadcast(
67
+ subscriberIds,
68
+ (chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "๐Ÿ“ฐ Newsletter #42" }),
69
+ {
70
+ concurrency: 64, // batasi bila downstream Anda butuh (default: paralel penuh)
71
+ onProgress: (p) => console.log(`${p.delivered}/${p.total}`),
72
+ },
73
+ );
74
+
75
+ console.log(`Terkirim ${report.delivered}/${report.total} dalam ${report.durationMs}ms`);
76
+ for (const failure of report.failures) {
77
+ console.error(`chat ${failure.chatId}: ${failure.error}`);
78
+ }
79
+ ```
80
+
81
+ Setiap chat tetap dicoba; 429 di-retry tepat selama `retry_after` yang diperintahkan Telegram. Kegagalan tidak pernah membatalkan keseluruhan proses โ€” semuanya masuk report.
82
+
83
+ ## 4. Pesan terjadwal (interval, sekali, cron)
84
+
85
+ ```ts
86
+ import { Scheduler, parseCronExpression, nextCronOccurrence } from "@xbibzlibrary/telebibz";
87
+
88
+ const scheduler = new Scheduler({ onError: (error, id) => console.error(`job ${id} gagal`, error) });
89
+
90
+ // Setiap 6 jam
91
+ scheduler.every("digest", 6 * 60 * 60 * 1000, async () => {
92
+ await bot.api.methods.sendMessage({ chat_id: ADMIN_CHAT, text: "Ringkasan terjadwal" });
93
+ });
94
+
95
+ // Sekali, setelah 5 menit (pola pengingat)
96
+ scheduler.after("remind-42", 5 * 60 * 1000, async () => {
97
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "โฐ Pengingat!" });
98
+ });
99
+
100
+ // Cron: hari kerja 09:00 (ekspresi lima field)
101
+ scheduler.cron("morning", "0 9 * * 1-5", async () => {
102
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "Selamat pagi!" });
103
+ });
104
+
105
+ scheduler.cancel("digest"); // hentikan satu job
106
+ scheduler.clear(); // hentikan semua
107
+ ```
108
+
109
+ Helper murni untuk test dan pratinjau โ€” tanpa timer:
110
+
111
+ ```ts
112
+ parseCronExpression("*/15 * * * *"); // field tervalidasi
113
+ nextCronOccurrence("0 9 * * 1", new Date()); // eksekusi berikutnya sebagai Date
114
+ ```
115
+
116
+ ## 5. Job background dengan retry
117
+
118
+ ```ts
119
+ import { TaskQueue } from "@xbibzlibrary/telebibz";
120
+
121
+ const queue = new TaskQueue(
122
+ async (job) => {
123
+ await fetch(`https://api.example.com/process`, { method: "POST", body: JSON.stringify(job.data) });
124
+ },
125
+ { concurrency: 8, retries: 3, backoffMs: 500, maxBackoffMs: 30_000, onError: (error, job) => log.error("job gagal", { job: job.id, error }) },
126
+ );
127
+
128
+ bot.command("process", async (ctx) => {
129
+ const job = queue.add({ url: ctx.message?.text?.split(" ")[1] }, { priority: 10 }); // lebih tinggi lebih dulu
130
+ await ctx.reply(`Job ${job.id} masuk antrian`);
131
+ });
132
+
133
+ bot.command("cancel", async (ctx) => {
134
+ const id = ctx.message?.text?.split(" ")[1];
135
+ if (id && queue.cancel(id)) await ctx.reply("Dibatalkan");
136
+ });
137
+ ```
138
+
139
+ ## 6. Menu berpaginasi
140
+
141
+ `MenuController` merender satu halaman sekaligus dan me-routing callback navigasi:
142
+
143
+ ```ts
144
+ import { Bot, MenuController, InlineKeyboard } from "@xbibzlibrary/telebibz";
145
+
146
+ const products = Array.from({ length: 57 }, (_v, i) => ({ id: i + 1, name: `Produk ${i + 1}` }));
147
+
148
+ const menu = new MenuController({
149
+ id: "products",
150
+ items: () => products, // atau async () => await db.products()
151
+ pageSize: 10,
152
+ label: (item) => item.name,
153
+ callback: async (item) => { /* user memilih sebuah produk */ },
154
+ labels: { previous: "โ—€", next: "โ–ถ" },
155
+ });
156
+
157
+ bot.callback("products:*", async (ctx) => {
158
+ // Lewatkan data callback LENGKAP โ€” controller mengharapkan prefix "products:"-nya sendiri.
159
+ const result = await menu.handle(ctx.callbackQuery?.data ?? "");
160
+ if (result === undefined) return void (await ctx.answerCallbackQuery());
161
+ if (result.type === "noop") return void (await ctx.answerCallbackQuery());
162
+ if (result.type === "page") {
163
+ await ctx.reply(`Halaman ${result.page.page + 1}/${result.page.pageCount}`, { reply_markup: result.keyboard });
164
+ } else {
165
+ await ctx.answerCallbackQuery(`Dipilih: ${result.item.name}`);
166
+ }
167
+ });
168
+ bot.command("shop", async (ctx) => {
169
+ const result = await menu.handle("products:page:0"); // "<id>:page:<n>"
170
+ if (result?.type === "page") await ctx.reply("Produk:", { reply_markup: result.keyboard });
171
+ });
172
+ ```
173
+
174
+ ## 7. Menu dengan permission
175
+
176
+ ```ts
177
+ import { Menu } from "@xbibzlibrary/telebibz";
178
+
179
+ const menu = new Menu("main")
180
+ .breadcrumb("Beranda")
181
+ .item({ id: "profile", label: "๐Ÿ‘ค Profil", callbackData: "open:profile" })
182
+ .item({ id: "stats", label: "๐Ÿ“Š Statistik", permission: (context) => context.permissions?.includes("admin") ?? false })
183
+ .item({ id: "help", label: "โ“ Bantuan", url: "https://example.com/help" });
184
+
185
+ // build() async: ia mengevaluasi visibilitas/permission untuk context yang diberikan.
186
+ const keyboard = await menu.build({ permissions: ["admin"] }, { columns: 1, includeBreadcrumbs: true });
187
+ await ctx.reply("Menu utama:", { reply_markup: keyboard.build() });
188
+ ```
189
+
190
+ `visible` menyembunyikan item sepenuhnya; `permission` menerima `MenuContext` Anda (`{ userId, permissions }`).
191
+
192
+ ## 8. Form multi-step dengan validasi
193
+
194
+ ```ts
195
+ import { Bot, Form, validators } from "@xbibzlibrary/telebibz";
196
+
197
+ const registration = new Form({
198
+ name: { parse: validators.string, required: true },
199
+ age: { parse: validators.integer, validate: (age) => (age >= 13 ? undefined : "Minimal 13 tahun") },
200
+ email: { parse: validators.email },
201
+ });
202
+ ```
203
+
204
+ Sambungkan lewat step `Wizard` atau `ConversationManager` โ€” beri satu field per pesan; `validators` mencakup `string`, `number`, `integer`, `email`, `url`, dan cek kustom mengembalikan pesan errornya.
205
+
206
+ ## 9. Mengedit pesan dan inline keyboard
207
+
208
+ ```ts
209
+ import { InlineKeyboard } from "@xbibzlibrary/telebibz";
210
+
211
+ bot.action("vote:up", async (ctx) => {
212
+ votes += 1;
213
+ const keyboard = new InlineKeyboard()
214
+ .text(`๐Ÿ‘ ${votes}`, "vote:up")
215
+ .text("๐Ÿ‘Ž 0", "vote:down")
216
+ .build();
217
+ // Tukar keyboard pesan tombol tersebut di tempat
218
+ await ctx.api.methods.editMessageReplyMarkup({
219
+ chat_id: ctx.chat!.id,
220
+ message_id: ctx.callbackQuery!.message!.message_id,
221
+ reply_markup: keyboard,
222
+ });
223
+ await ctx.answerCallbackQuery(); // hentikan spinner
224
+ });
225
+ ```
226
+
227
+ `ctx.edit(text, extra)` menulis ulang teks pesan saat ini (keyboard ikut lewat `reply_markup` di `extra`); `editMessageLiveLocation`, `stopPoll`, dan seluruh surface method tersedia di `ctx.api.methods`. Data tombol dibatasi **64 byte** โ€” builder memvalidasi saat konstruksi, bukan crash saat runtime.
228
+
229
+ ## 10. Cache hasil mahal
230
+
231
+ ```ts
232
+ import { MemoryCache } from "@xbibzlibrary/telebibz";
233
+
234
+ const weather = new MemoryCache<string>("weather"); // namespace; TTL di-set per penulisan
235
+ bot.command("weather", async (ctx) => {
236
+ const city = ctx.message?.text?.split(" ")[1] ?? "Jakarta";
237
+ let text = await weather.get(city);
238
+ if (text === undefined) {
239
+ text = await fetchWeather(city);
240
+ await weather.set(city, text, 5 * 60 * 1000); // cache 5 menit
241
+ }
242
+ await ctx.reply(text);
243
+ });
244
+ ```
245
+
246
+ ## 11. Validasi initData Mini App
247
+
248
+ ```ts
249
+ import { validateWebAppInitData } from "@xbibzlibrary/telebibz";
250
+
251
+ bot.command("app", async (ctx) => {
252
+ await ctx.reply("Buka aplikasinya:", {
253
+ reply_markup: new InlineKeyboard().webApp("๐Ÿš€ Buka", "https://app.example.com").build(),
254
+ });
255
+ });
256
+
257
+ // Di endpoint backend aplikasi Anda โ€” verifikasi yang dikirim Mini App:
258
+ app.post("/api/data", express.json(), (req, res) => {
259
+ try {
260
+ const initData = validateWebAppInitData(req.body.initData, process.env.TELEGRAM_BOT_TOKEN!, 3600);
261
+ res.json({ user: initData.user, ok: true }); // signature + kesegaran terverifikasi
262
+ } catch {
263
+ res.status(401).json({ ok: false });
264
+ }
265
+ });
266
+ ```
267
+
268
+ `validateWebAppInitData` memeriksa signature HMAC dan jendela kesegaran `auth_date` (default 24 jam; di sini 1 jam).
269
+
270
+ ## 12. Pembayaran dengan Telegram Stars / invoice
271
+
272
+ ```ts
273
+ import { Bot, PaymentsClient, InlineKeyboard } from "@xbibzlibrary/telebibz";
274
+
275
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
276
+ const payments = new PaymentsClient(bot.api);
277
+
278
+ // Link yang berfungsi di mana saja (bio, website, chat)
279
+ const link = await payments.createInvoiceLink({
280
+ title: "Premium",
281
+ description: "30 hari premium",
282
+ payload: "premium-30d",
283
+ currency: "XTR",
284
+ prices: [{ label: "Premium", amount: 100 }],
285
+ });
286
+ await ctx.reply(`Bayar di sini: ${link}`);
287
+
288
+ // Invoice dalam chat + pre-checkout + pembayaran sukses
289
+ bot.on("pre_checkout_query", async (ctx) => {
290
+ const query = ctx.update.pre_checkout_query;
291
+ if (!query) return;
292
+ await ctx.api.methods.answerPreCheckoutQuery({ pre_checkout_query_id: query.id, ok: true });
293
+ });
294
+ bot.on("message:successful_payment", async (ctx) => {
295
+ await ctx.reply("โœ… Pembayaran diterima. Terima kasih!");
296
+ });
297
+
298
+ // Riwayat Stars & refund
299
+ const history = await payments.getStarTransactions({ limit: 50 });
300
+ await payments.refundStarPayment({ user_id: userId, telegram_payment_charge_id: "charge-id" });
301
+ ```
302
+
303
+ ## 13. Logging terstruktur dan hook metrik
304
+
305
+ ```ts
306
+ const bot = new Bot({
307
+ token: process.env.TELEGRAM_BOT_TOKEN!,
308
+ logger: { level: "info", format: "json" }, // baris machine-readable untuk ingestion
309
+ });
310
+
311
+ bot.events.on("api:response", ({ method, durationMs }) => {
312
+ if (durationMs > 3_000) console.warn(JSON.stringify({ event: "slow_api", method, durationMs }));
313
+ });
314
+ bot.events.on("update:error", ({ error }) => {
315
+ console.error(JSON.stringify({ event: "handler_error", error: String(error) }));
316
+ });
317
+ ```
318
+
319
+ Nilai sensitif (token, nomor telepon) otomatis di-redact; `includeUpdateContent: true` mengaktifkan pencatatan teks pesan bila benar-benar diperlukan.
320
+
321
+ English: [COOKBOOK.md](COOKBOOK.md) ยท ็ฎ€ไฝ“ไธญๆ–‡: [COOKBOOK.zh-CN.md](COOKBOOK.zh-CN.md)
@@ -0,0 +1,321 @@
1
+ # Production cookbook (English)
2
+
3
+ Complete, verified recipes for the things real bots need: per-user rate limiting, auth middleware, broadcasts, scheduled jobs, background queues, menus with pagination, forms, caching, Mini App validation, and payments. Each recipe is self-contained โ€” copy it into your bot and adjust names.
4
+
5
+ ## Contents
6
+
7
+ 1. [Per-user rate limiting](#1-per-user-rate-limiting)
8
+ 2. [Auth middleware (allowlist / admin only)](#2-auth-middleware-allowlist--admin-only)
9
+ 3. [Broadcast to thousands of users](#3-broadcast-to-thousands-of-users)
10
+ 4. [Scheduled messages (interval, one-shot, cron)](#4-scheduled-messages-interval-one-shot-cron)
11
+ 5. [Background jobs with retries](#5-background-jobs-with-retries)
12
+ 6. [Paginated menus](#6-paginated-menus)
13
+ 7. [Permission-aware menus](#7-permission-aware-menus)
14
+ 8. [Multi-step forms with validation](#8-multi-step-forms-with-validation)
15
+ 9. [Editing messages and inline keyboards](#9-editing-messages-and-inline-keyboards)
16
+ 10. [Caching expensive results](#10-caching-expensive-results)
17
+ 11. [Mini App initData validation](#11-mini-app-initdata-validation)
18
+ 12. [Payments with Telegram Stars / invoices](#12-payments-with-telegram-stars--invoices)
19
+ 13. [Structured logging and metrics hooks](#13-structured-logging-and-metrics-hooks)
20
+
21
+ ## 1. Per-user rate limiting
22
+
23
+ `TokenBucketLimiter` keeps an independent bucket per key โ€” key it by user or chat:
24
+
25
+ ```ts
26
+ import { Bot, TokenBucketLimiter } from "@xbibzlibrary/telebibz";
27
+
28
+ const limiter = new TokenBucketLimiter(5, 0.5); // burst of 5, refill 0.5 token/s (= 1 msg per 2s sustained)
29
+
30
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
31
+ bot.use(async (ctx, next) => {
32
+ const key = `user:${ctx.from?.id ?? "anon"}`;
33
+ const result = limiter.consume(key);
34
+ if (!result.allowed) {
35
+ const seconds = Math.ceil((result.retryAfterMs ?? 1000) / 1000);
36
+ await ctx.reply(`โณ Terlalu banyak permintaan. Coba lagi dalam ${seconds} detik.`);
37
+ return; // do not call next(): update is dropped
38
+ }
39
+ await next();
40
+ });
41
+ ```
42
+
43
+ `consume(key, cost)` supports weighted actions (e.g. uploads cost 5, text costs 1). `limiter.clear(key?)` resets state. Combine with the transport flood gate โ€” this limiter shapes *your users*; the flood gate obeys *Telegram*.
44
+
45
+ ## 2. Auth middleware (allowlist / admin only)
46
+
47
+ ```ts
48
+ const ADMINS = new Set([Number(process.env.ADMIN_ID)]);
49
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
50
+
51
+ bot.use(async (ctx, next) => {
52
+ if (ADMINS.has(ctx.from?.id ?? 0)) return await next(); // admins: everything
53
+ if (ctx.chat?.type === "private") return await next(); // DMs: allowed
54
+ return undefined; // groups: silent drop
55
+ });
56
+
57
+ bot.command("stats", async (ctx) => { // admin-only route
58
+ if (!ADMINS.has(ctx.from?.id ?? 0)) return;
59
+ await ctx.reply("Secret stats");
60
+ });
61
+ ```
62
+
63
+ ## 3. Broadcast to thousands of users
64
+
65
+ ```ts
66
+ const report = await bot.broadcast(
67
+ subscriberIds,
68
+ (chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "๐Ÿ“ฐ Newsletter #42" }),
69
+ {
70
+ concurrency: 64, // cap when your downstream needs it (default: fully parallel)
71
+ onProgress: (p) => console.log(`${p.delivered}/${p.total}`),
72
+ },
73
+ );
74
+
75
+ console.log(`Delivered ${report.delivered}/${report.total} in ${report.durationMs}ms`);
76
+ for (const failure of report.failures) {
77
+ console.error(`chat ${failure.chatId}: ${failure.error}`);
78
+ }
79
+ ```
80
+
81
+ Every chat is attempted; 429s are retried after exactly the `retry_after` Telegram orders. Failures never abort the run โ€” they land in the report.
82
+
83
+ ## 4. Scheduled messages (interval, one-shot, cron)
84
+
85
+ ```ts
86
+ import { Scheduler, parseCronExpression, nextCronOccurrence } from "@xbibzlibrary/telebibz";
87
+
88
+ const scheduler = new Scheduler({ onError: (error, id) => console.error(`job ${id} failed`, error) });
89
+
90
+ // Every 6 hours
91
+ scheduler.every("digest", 6 * 60 * 60 * 1000, async () => {
92
+ await bot.api.methods.sendMessage({ chat_id: ADMIN_CHAT, text: "Scheduled digest" });
93
+ });
94
+
95
+ // Once, after 5 minutes (reminder pattern)
96
+ scheduler.after("remind-42", 5 * 60 * 1000, async () => {
97
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "โฐ Reminder!" });
98
+ });
99
+
100
+ // Cron: weekdays 09:00 (five-field expression)
101
+ scheduler.cron("morning", "0 9 * * 1-5", async () => {
102
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "Good morning!" });
103
+ });
104
+
105
+ scheduler.cancel("digest"); // stop one job
106
+ scheduler.clear(); // stop all
107
+ ```
108
+
109
+ Pure helpers for tests and previews โ€” no timers involved:
110
+
111
+ ```ts
112
+ parseCronExpression("*/15 * * * *"); // validated fields
113
+ nextCronOccurrence("0 9 * * 1", new Date()); // the next run as a Date
114
+ ```
115
+
116
+ ## 5. Background jobs with retries
117
+
118
+ ```ts
119
+ import { TaskQueue } from "@xbibzlibrary/telebibz";
120
+
121
+ const queue = new TaskQueue(
122
+ async (job) => {
123
+ await fetch(`https://api.example.com/process`, { method: "POST", body: JSON.stringify(job.data) });
124
+ },
125
+ { concurrency: 8, retries: 3, backoffMs: 500, maxBackoffMs: 30_000, onError: (error, job) => log.error("job failed", { job: job.id, error }) },
126
+ );
127
+
128
+ bot.command("process", async (ctx) => {
129
+ const job = queue.add({ url: ctx.message?.text?.split(" ")[1] }, { priority: 10 }); // higher runs first
130
+ await ctx.reply(`Queued job ${job.id}`);
131
+ });
132
+
133
+ bot.command("cancel", async (ctx) => {
134
+ const id = ctx.message?.text?.split(" ")[1];
135
+ if (id && queue.cancel(id)) await ctx.reply("Cancelled");
136
+ });
137
+ ```
138
+
139
+ ## 6. Paginated menus
140
+
141
+ `MenuController` renders one page at a time and routes `prev:`/`next:` callbacks:
142
+
143
+ ```ts
144
+ import { Bot, MenuController, InlineKeyboard } from "@xbibzlibrary/telebibz";
145
+
146
+ const products = Array.from({ length: 57 }, (_v, i) => ({ id: i + 1, name: `Product ${i + 1}` }));
147
+
148
+ const menu = new MenuController({
149
+ id: "products",
150
+ items: () => products, // or an async () => await db.products()
151
+ pageSize: 10,
152
+ label: (item) => item.name,
153
+ callback: async (item) => { /* user picked a product */ },
154
+ labels: { previous: "โ—€", next: "โ–ถ" },
155
+ });
156
+
157
+ bot.callback("products:*", async (ctx) => {
158
+ // Pass the FULL callback data โ€” the controller expects its own "products:" prefix.
159
+ const result = await menu.handle(ctx.callbackQuery?.data ?? "");
160
+ if (result === undefined) return void (await ctx.answerCallbackQuery());
161
+ if (result.type === "noop") return void (await ctx.answerCallbackQuery());
162
+ if (result.type === "page") {
163
+ await ctx.reply(`Halaman ${result.page.page + 1}/${result.page.pageCount}`, { reply_markup: result.keyboard });
164
+ } else {
165
+ await ctx.answerCallbackQuery(`Picked: ${result.item.name}`);
166
+ }
167
+ });
168
+ bot.command("shop", async (ctx) => {
169
+ const result = await menu.handle("products:page:0"); // "<id>:page:<n>"
170
+ if (result?.type === "page") await ctx.reply("Products:", { reply_markup: result.keyboard });
171
+ });
172
+ ```
173
+
174
+ ## 7. Permission-aware menus
175
+
176
+ ```ts
177
+ import { Menu } from "@xbibzlibrary/telebibz";
178
+
179
+ const menu = new Menu("main")
180
+ .breadcrumb("Home")
181
+ .item({ id: "profile", label: "๐Ÿ‘ค Profile", callbackData: "open:profile" })
182
+ .item({ id: "stats", label: "๐Ÿ“Š Stats", permission: (context) => context.permissions?.includes("admin") ?? false })
183
+ .item({ id: "help", label: "โ“ Help", url: "https://example.com/help" });
184
+
185
+ // build() is async: it evaluates visibility/permissions for the given context.
186
+ const keyboard = await menu.build({ permissions: ["admin"] }, { columns: 1, includeBreadcrumbs: true });
187
+ await ctx.reply("Main menu:", { reply_markup: keyboard.build() });
188
+ ```
189
+
190
+ `visible` hides items entirely; `permission` receives your `MenuContext` (`{ userId, permissions }`).
191
+
192
+ ## 8. Multi-step forms with validation
193
+
194
+ ```ts
195
+ import { Bot, Form, validators } from "@xbibzlibrary/telebibz";
196
+
197
+ const registration = new Form({
198
+ name: { parse: validators.string, required: true },
199
+ age: { parse: validators.integer, validate: (age) => (age >= 13 ? undefined : "Must be 13+") },
200
+ email: { parse: validators.email },
201
+ });
202
+ ```
203
+
204
+ Wire it through a `Wizard` step or `ConversationManager` โ€” feed one field per message; `validators` covers `string`, `number`, `integer`, `email`, `url`, and custom checks return the error message.
205
+
206
+ ## 9. Editing messages and inline keyboards
207
+
208
+ ```ts
209
+ import { InlineKeyboard } from "@xbibzlibrary/telebibz";
210
+
211
+ bot.action("vote:up", async (ctx) => {
212
+ votes += 1;
213
+ const keyboard = new InlineKeyboard()
214
+ .text(`๐Ÿ‘ ${votes}`, "vote:up")
215
+ .text("๐Ÿ‘Ž 0", "vote:down")
216
+ .build();
217
+ // Swap the keyboard of the button's message in place
218
+ await ctx.api.methods.editMessageReplyMarkup({
219
+ chat_id: ctx.chat!.id,
220
+ message_id: ctx.callbackQuery!.message!.message_id,
221
+ reply_markup: keyboard,
222
+ });
223
+ await ctx.answerCallbackQuery(); // stop the spinner
224
+ });
225
+ ```
226
+
227
+ `ctx.edit(text, extra)` rewrites the current message's text (keyboard included via `reply_markup` in `extra`); `editMessageLiveLocation`, `stopPoll`, and the full method surface are available on `ctx.api.methods`. Button data is limited to **64 bytes** โ€” the builder validates at construction time, not at runtime crash time.
228
+
229
+ ## 10. Caching expensive results
230
+
231
+ ```ts
232
+ import { MemoryCache } from "@xbibzlibrary/telebibz";
233
+
234
+ const weather = new MemoryCache<string>("weather"); // namespace; TTL is set per write
235
+ bot.command("weather", async (ctx) => {
236
+ const city = ctx.message?.text?.split(" ")[1] ?? "Jakarta";
237
+ let text = await weather.get(city);
238
+ if (text === undefined) {
239
+ text = await fetchWeather(city);
240
+ await weather.set(city, text, 5 * 60 * 1000); // cache for 5 minutes
241
+ }
242
+ await ctx.reply(text);
243
+ });
244
+ ```
245
+
246
+ ## 11. Mini App initData validation
247
+
248
+ ```ts
249
+ import { validateWebAppInitData } from "@xbibzlibrary/telebibz";
250
+
251
+ bot.command("app", async (ctx) => {
252
+ await ctx.reply("Open the app:", {
253
+ reply_markup: new InlineKeyboard().webApp("๐Ÿš€ Open", "https://app.example.com").build(),
254
+ });
255
+ });
256
+
257
+ // In your app's backend endpoint โ€” verify what the Mini App sends:
258
+ app.post("/api/data", express.json(), (req, res) => {
259
+ try {
260
+ const initData = validateWebAppInitData(req.body.initData, process.env.TELEGRAM_BOT_TOKEN!, 3600);
261
+ res.json({ user: initData.user, ok: true }); // signature + freshness verified
262
+ } catch {
263
+ res.status(401).json({ ok: false });
264
+ }
265
+ });
266
+ ```
267
+
268
+ `validateWebAppInitData` checks the HMAC signature and the `auth_date` freshness window (default 24 h; here 1 h).
269
+
270
+ ## 12. Payments with Telegram Stars / invoices
271
+
272
+ ```ts
273
+ import { Bot, PaymentsClient, InlineKeyboard } from "@xbibzlibrary/telebibz";
274
+
275
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
276
+ const payments = new PaymentsClient(bot.api);
277
+
278
+ // Link that works anywhere (bio, website, chat)
279
+ const link = await payments.createInvoiceLink({
280
+ title: "Premium",
281
+ description: "30 days of premium",
282
+ payload: "premium-30d",
283
+ currency: "XTR",
284
+ prices: [{ label: "Premium", amount: 100 }],
285
+ });
286
+ await ctx.reply(`Pay here: ${link}`);
287
+
288
+ // In-chat invoice + pre-checkout + successful payment
289
+ bot.on("pre_checkout_query", async (ctx) => {
290
+ const query = ctx.update.pre_checkout_query;
291
+ if (!query) return;
292
+ await ctx.api.methods.answerPreCheckoutQuery({ pre_checkout_query_id: query.id, ok: true });
293
+ });
294
+ bot.on("message:successful_payment", async (ctx) => {
295
+ await ctx.reply("โœ… Payment received. Thank you!");
296
+ });
297
+
298
+ // Stars history & refunds
299
+ const history = await payments.getStarTransactions({ limit: 50 });
300
+ await payments.refundStarPayment({ user_id: userId, telegram_payment_charge_id: "charge-id" });
301
+ ```
302
+
303
+ ## 13. Structured logging and metrics hooks
304
+
305
+ ```ts
306
+ const bot = new Bot({
307
+ token: process.env.TELEGRAM_BOT_TOKEN!,
308
+ logger: { level: "info", format: "json" }, // machine-readable lines for ingestion
309
+ });
310
+
311
+ bot.events.on("api:response", ({ method, durationMs }) => {
312
+ if (durationMs > 3_000) console.warn(JSON.stringify({ event: "slow_api", method, durationMs }));
313
+ });
314
+ bot.events.on("update:error", ({ error }) => {
315
+ console.error(JSON.stringify({ event: "handler_error", error: String(error) }));
316
+ });
317
+ ```
318
+
319
+ Sensitive values (tokens, phone numbers) are redacted automatically; `includeUpdateContent: true` opts into logging message text when you truly need it.
320
+
321
+ Bahasa Indonesia: [COOKBOOK.id.md](COOKBOOK.id.md) ยท ็ฎ€ไฝ“ไธญๆ–‡: [COOKBOOK.zh-CN.md](COOKBOOK.zh-CN.md)