@xbibzlibrary/telebibz 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.id.md +14 -0
- package/README.md +14 -0
- package/README.zh-CN.md +14 -0
- package/docs/COOKBOOK.id.md +321 -0
- package/docs/COOKBOOK.md +321 -0
- package/docs/COOKBOOK.zh-CN.md +321 -0
- package/docs/ERRORS.id.md +194 -0
- package/docs/ERRORS.md +194 -0
- package/docs/ERRORS.zh-CN.md +194 -0
- package/docs/FILES.id.md +243 -0
- package/docs/FILES.md +243 -0
- package/docs/FILES.zh-CN.md +243 -0
- package/docs/GETTING_STARTED.id.md +6 -2
- package/docs/GETTING_STARTED.md +6 -2
- package/docs/GETTING_STARTED.zh-CN.md +6 -2
- package/docs/MIGRATION_TELEGRAF.id.md +147 -0
- package/docs/MIGRATION_TELEGRAF.md +154 -0
- package/docs/MIGRATION_TELEGRAF.zh-CN.md +147 -0
- package/docs/README.md +38 -19
- package/docs/TESTING.id.md +203 -0
- package/docs/TESTING.md +203 -0
- package/docs/TESTING.zh-CN.md +203 -0
- package/docs/WEBHOOK.id.md +212 -0
- package/docs/WEBHOOK.md +215 -0
- package/docs/WEBHOOK.zh-CN.md +212 -0
- package/package.json +1 -1
package/README.id.md
CHANGED
|
@@ -19,6 +19,20 @@ Panduan cepat storage (Memory/JSON/Redis/SQL/Mongo): [English](docs/STORAGE.md)
|
|
|
19
19
|
|
|
20
20
|
Panduan mulai: [English](docs/GETTING_STARTED.md) · [Bahasa Indonesia](docs/GETTING_STARTED.id.md) · [简体中文](docs/GETTING_STARTED.zh-CN.md)
|
|
21
21
|
|
|
22
|
+
File (upload & download): [English](docs/FILES.md) · [Bahasa Indonesia](docs/FILES.id.md) · [简体中文](docs/FILES.zh-CN.md)
|
|
23
|
+
|
|
24
|
+
Error & rate limit: [English](docs/ERRORS.md) · [Bahasa Indonesia](docs/ERRORS.id.md) · [简体中文](docs/ERRORS.zh-CN.md)
|
|
25
|
+
|
|
26
|
+
Deployment webhook: [English](docs/WEBHOOK.md) · [Bahasa Indonesia](docs/WEBHOOK.id.md) · [简体中文](docs/WEBHOOK.zh-CN.md)
|
|
27
|
+
|
|
28
|
+
Testing (offline dengan MockTransport): [English](docs/TESTING.md) · [Bahasa Indonesia](docs/TESTING.id.md) · [简体中文](docs/TESTING.zh-CN.md)
|
|
29
|
+
|
|
30
|
+
Migrasi dari Telegraf: [English](docs/MIGRATION_TELEGRAF.md) · [Bahasa Indonesia](docs/MIGRATION_TELEGRAF.id.md) · [简体中文](docs/MIGRATION_TELEGRAF.zh-CN.md)
|
|
31
|
+
|
|
32
|
+
Cookbook produksi (13 resep): [English](docs/COOKBOOK.md) · [Bahasa Indonesia](docs/COOKBOOK.id.md) · [简体中文](docs/COOKBOOK.zh-CN.md)
|
|
33
|
+
|
|
34
|
+
Katalog dokumentasi lengkap: [docs/README.md](docs/README.md)
|
|
35
|
+
|
|
22
36
|
Showcase komunitas: [SHOWCASE.md](SHOWCASE.md)
|
|
23
37
|
|
|
24
38
|

|
package/README.md
CHANGED
|
@@ -21,6 +21,20 @@ Storage quick start (Memory/JSON/Redis/SQL/Mongo): [English](docs/STORAGE.md) ·
|
|
|
21
21
|
|
|
22
22
|
Getting started: [English](docs/GETTING_STARTED.md) · [Bahasa Indonesia](docs/GETTING_STARTED.id.md) · [简体中文](docs/GETTING_STARTED.zh-CN.md)
|
|
23
23
|
|
|
24
|
+
Files (upload & download): [English](docs/FILES.md) · [Bahasa Indonesia](docs/FILES.id.md) · [简体中文](docs/FILES.zh-CN.md)
|
|
25
|
+
|
|
26
|
+
Errors & rate limits: [English](docs/ERRORS.md) · [Bahasa Indonesia](docs/ERRORS.id.md) · [简体中文](docs/ERRORS.zh-CN.md)
|
|
27
|
+
|
|
28
|
+
Webhook deployment: [English](docs/WEBHOOK.md) · [Bahasa Indonesia](docs/WEBHOOK.id.md) · [简体中文](docs/WEBHOOK.zh-CN.md)
|
|
29
|
+
|
|
30
|
+
Testing (offline with MockTransport): [English](docs/TESTING.md) · [Bahasa Indonesia](docs/TESTING.id.md) · [简体中文](docs/TESTING.zh-CN.md)
|
|
31
|
+
|
|
32
|
+
Migrating from Telegraf: [English](docs/MIGRATION_TELEGRAF.md) · [Bahasa Indonesia](docs/MIGRATION_TELEGRAF.id.md) · [简体中文](docs/MIGRATION_TELEGRAF.zh-CN.md)
|
|
33
|
+
|
|
34
|
+
Production cookbook (13 recipes): [English](docs/COOKBOOK.md) · [Bahasa Indonesia](docs/COOKBOOK.id.md) · [简体中文](docs/COOKBOOK.zh-CN.md)
|
|
35
|
+
|
|
36
|
+
Full documentation catalog: [docs/README.md](docs/README.md)
|
|
37
|
+
|
|
24
38
|
Community showcase: [SHOWCASE.md](SHOWCASE.md)
|
|
25
39
|
|
|
26
40
|

|
package/README.zh-CN.md
CHANGED
|
@@ -19,6 +19,20 @@ GitHub Packages 指南:[English](docs/GITHUB_PACKAGES.md) · [Bahasa Indonesia
|
|
|
19
19
|
|
|
20
20
|
入门指南:[English](docs/GETTING_STARTED.md) · [Bahasa Indonesia](docs/GETTING_STARTED.id.md) · [简体中文](docs/GETTING_STARTED.zh-CN.md)
|
|
21
21
|
|
|
22
|
+
文件(上传与下载):[English](docs/FILES.md) · [Bahasa Indonesia](docs/FILES.id.md) · [简体中文](docs/FILES.zh-CN.md)
|
|
23
|
+
|
|
24
|
+
错误与限流:[English](docs/ERRORS.md) · [Bahasa Indonesia](docs/ERRORS.id.md) · [简体中文](docs/ERRORS.zh-CN.md)
|
|
25
|
+
|
|
26
|
+
Webhook 部署:[English](docs/WEBHOOK.md) · [Bahasa Indonesia](docs/WEBHOOK.id.md) · [简体中文](docs/WEBHOOK.zh-CN.md)
|
|
27
|
+
|
|
28
|
+
测试(用 MockTransport 离线进行):[English](docs/TESTING.md) · [Bahasa Indonesia](docs/TESTING.id.md) · [简体中文](docs/TESTING.zh-CN.md)
|
|
29
|
+
|
|
30
|
+
从 Telegraf 迁移:[English](docs/MIGRATION_TELEGRAF.md) · [Bahasa Indonesia](docs/MIGRATION_TELEGRAF.id.md) · [简体中文](docs/MIGRATION_TELEGRAF.zh-CN.md)
|
|
31
|
+
|
|
32
|
+
生产实战手册(13 个配方):[English](docs/COOKBOOK.md) · [Bahasa Indonesia](docs/COOKBOOK.id.md) · [简体中文](docs/COOKBOOK.zh-CN.md)
|
|
33
|
+
|
|
34
|
+
完整文档目录:[docs/README.md](docs/README.md)
|
|
35
|
+
|
|
22
36
|
社区 showcase:[SHOWCASE.md](SHOWCASE.md)
|
|
23
37
|
|
|
24
38
|

|
|
@@ -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)
|