@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.
- package/CHANGELOG.md +22 -0
- package/README.id.md +20 -3
- package/README.md +20 -3
- package/README.zh-CN.md +20 -3
- package/dist/src/api/client.d.ts +28 -0
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +28 -0
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/transport.d.ts +14 -0
- package/dist/src/api/transport.d.ts.map +1 -1
- package/dist/src/api/transport.js +67 -2
- package/dist/src/api/transport.js.map +1 -1
- package/dist/src/api/types.d.ts +25 -0
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/context/context.d.ts +13 -3
- package/dist/src/context/context.d.ts.map +1 -1
- package/dist/src/context/context.js +15 -0
- package/dist/src/context/context.js.map +1 -1
- package/dist/src/core/bot.d.ts +12 -0
- package/dist/src/core/bot.d.ts.map +1 -1
- package/dist/src/core/bot.js +16 -0
- package/dist/src/core/bot.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/testing.d.ts +6 -0
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +6 -0
- package/dist/src/testing.js.map +1 -1
- package/dist/src/utils/files.d.ts +45 -0
- package/dist/src/utils/files.d.ts.map +1 -0
- package/dist/src/utils/files.js +53 -0
- package/dist/src/utils/files.js.map +1 -0
- package/dist-cjs/src/api/client.js +28 -0
- package/dist-cjs/src/api/transport.js +67 -2
- package/dist-cjs/src/context/context.js +15 -0
- package/dist-cjs/src/core/bot.js +16 -0
- package/dist-cjs/src/index.js +1 -0
- package/dist-cjs/src/testing.js +6 -0
- package/dist-cjs/src/utils/files.js +58 -0
- package/docs/API.id.md +70 -0
- package/docs/API.md +70 -0
- package/docs/API.zh-CN.md +70 -0
- package/docs/COOKBOOK.id.md +321 -0
- package/docs/COOKBOOK.md +321 -0
- package/docs/COOKBOOK.zh-CN.md +321 -0
- package/docs/ERRORS.id.md +194 -0
- package/docs/ERRORS.md +194 -0
- package/docs/ERRORS.zh-CN.md +194 -0
- package/docs/FILES.id.md +243 -0
- package/docs/FILES.md +243 -0
- package/docs/FILES.zh-CN.md +243 -0
- package/docs/GETTING_STARTED.id.md +6 -2
- package/docs/GETTING_STARTED.md +6 -2
- package/docs/GETTING_STARTED.zh-CN.md +6 -2
- package/docs/MIGRATION_TELEGRAF.id.md +147 -0
- package/docs/MIGRATION_TELEGRAF.md +154 -0
- package/docs/MIGRATION_TELEGRAF.zh-CN.md +147 -0
- package/docs/README.md +38 -19
- package/docs/STORAGE.id.md +105 -0
- package/docs/STORAGE.md +105 -0
- package/docs/STORAGE.zh-CN.md +105 -0
- package/docs/TESTING.id.md +203 -0
- package/docs/TESTING.md +203 -0
- package/docs/TESTING.zh-CN.md +203 -0
- package/docs/WEBHOOK.id.md +212 -0
- package/docs/WEBHOOK.md +215 -0
- package/docs/WEBHOOK.zh-CN.md +212 -0
- package/examples/files.ts +35 -0
- 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)
|
package/docs/COOKBOOK.md
ADDED
|
@@ -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)
|