@xbibzlibrary/telebibz 3.0.1 → 3.1.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.
- package/CHANGELOG.md +18 -0
- package/README.id.md +642 -0
- package/README.md +556 -147
- package/examples/03-wizard.js +37 -8
- package/index.d.ts +28 -3
- package/index.js +2 -0
- package/lib/telebibz.js +3 -0
- package/lib/wizard.js +184 -18
- package/package.json +2 -2
- package/test/all.test.js +106 -1
package/examples/03-wizard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// examples/03-wizard.js — form pendaftaran tanya-jawab
|
|
1
|
+
// examples/03-wizard.js — form pendaftaran tanya-jawab + TOMBOL + mode edit/delete.
|
|
2
|
+
// BOT_TOKEN=123:abc node examples/03-wizard.js
|
|
2
3
|
'use strict';
|
|
3
4
|
|
|
4
5
|
const { TeleBibz } = require('..');
|
|
@@ -6,22 +7,50 @@ const { TeleBibz } = require('..');
|
|
|
6
7
|
const bot = new TeleBibz(process.env.BOT_TOKEN);
|
|
7
8
|
|
|
8
9
|
bot.wizard('daftar', {
|
|
10
|
+
// mode tampilan pertanyaan:
|
|
11
|
+
// 'send' → tiap pertanyaan jadi pesan baru (default)
|
|
12
|
+
// 'edit' → SATU pesan diedit terus dari awal sampai akhir
|
|
13
|
+
// 'delete' → pesan lama dihapus dulu, baru tanya berikutnya
|
|
14
|
+
mode: 'edit',
|
|
15
|
+
|
|
9
16
|
steps: [
|
|
10
17
|
{ key: 'nama', ask: '1️⃣ Siapa nama lengkapmu?' },
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
// TOMBOL reply keyboard — user tinggal ketuk, tak perlu mengetik
|
|
21
|
+
key: 'jk',
|
|
22
|
+
ask: '2️⃣ Jenis kelamin?',
|
|
23
|
+
buttons: ['👨 Laki-laki', '👩 Perempuan'],
|
|
24
|
+
onlyButtons: true, // tolak ketikan bebas
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
// TOMBOL inline (callback) — nilai bisa beda dari label,
|
|
29
|
+
// dan pesan langsung diedit ke langkah berikutnya
|
|
30
|
+
key: 'domisili',
|
|
31
|
+
ask: '3️⃣ Domisili pulau mana?',
|
|
32
|
+
inline: true,
|
|
33
|
+
onlyButtons: true,
|
|
34
|
+
buttons: [
|
|
35
|
+
[{ text: '🌋 Jawa', value: 'jawa' }, { text: '🌴 Sumatera', value: 'sumatera' }],
|
|
36
|
+
[{ text: '🏝️ Lainnya', value: 'lainnya' }],
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
11
40
|
{
|
|
12
41
|
key: 'umur',
|
|
13
|
-
ask: '
|
|
42
|
+
ask: '4️⃣ Umur berapa? (ketik "batal" untuk berhenti)',
|
|
14
43
|
parse: (t) => parseInt(t, 10),
|
|
15
44
|
validate: (n) => (Number.isFinite(n) && n > 0 && n < 120 ? null : 'Umur tidak valid — ketik angka saja ya:'),
|
|
16
45
|
},
|
|
17
|
-
{
|
|
18
|
-
key: 'kota',
|
|
19
|
-
ask: '3️⃣ Tinggal di kota mana? (ketik "batal" untuk berhenti)',
|
|
20
|
-
validate: (t) => (t && t.length >= 3 ? null : 'Nama kota minimal 3 huruf:'),
|
|
21
|
-
},
|
|
22
46
|
],
|
|
47
|
+
|
|
48
|
+
// cleanup bawaan: pesan tanya terakhir otomatis dibereskan,
|
|
49
|
+
// reply keyboard otomatis disingkirkan (nonaktif: removeKeyboard: false)
|
|
23
50
|
done: async (ans, ctx) => {
|
|
24
|
-
await ctx.reply(
|
|
51
|
+
await ctx.reply(
|
|
52
|
+
`✅ Terdaftar!\nNama : ${ans.nama}\nJK : ${ans.jk}\nDomisili : ${ans.domisili}\nUmur : ${ans.umur}`,
|
|
53
|
+
);
|
|
25
54
|
},
|
|
26
55
|
onCancel: (ctx) => ctx.reply('👋 Pendaftaran dibatalkan.'),
|
|
27
56
|
});
|
package/index.d.ts
CHANGED
|
@@ -6,18 +6,36 @@ declare module '@xbibzlibrary/telebibz' {
|
|
|
6
6
|
type Color = 'danger' | 'success' | 'primary';
|
|
7
7
|
type Handler = (ctx: Context) => unknown;
|
|
8
8
|
|
|
9
|
+
type WizardMode = 'send' | 'edit' | 'delete';
|
|
10
|
+
type WizardButton = string | { text: string; value?: unknown };
|
|
9
11
|
interface WizardStep {
|
|
10
12
|
key: string;
|
|
11
13
|
ask: string | ((ctx: Context) => string | Promise<string>);
|
|
12
14
|
parse?: (text: string, ctx: Context) => unknown | Promise<unknown>;
|
|
13
15
|
validate?: (value: unknown, ctx: Context) => string | null | Promise<string | null>;
|
|
14
16
|
opts?: Record<string, unknown>;
|
|
17
|
+
/** Tombol pilihan: ['A','B'] | [{text,value}] | baris eksplisit [['A'],['B','C']]. */
|
|
18
|
+
buttons?: Array<WizardButton | WizardButton[]>;
|
|
19
|
+
/** true → tombol inline (callback), default reply keyboard. */
|
|
20
|
+
inline?: boolean;
|
|
21
|
+
/** true/string → tolak ketikan bebas, wajib pilih tombol. */
|
|
22
|
+
onlyButtons?: boolean | string;
|
|
23
|
+
/** Override mode tampilan per langkah. */
|
|
24
|
+
mode?: WizardMode;
|
|
25
|
+
/** Reply keyboard sekali pakai. */
|
|
26
|
+
oneTime?: boolean;
|
|
15
27
|
}
|
|
16
28
|
interface WizardDef {
|
|
17
29
|
steps: WizardStep[];
|
|
18
30
|
done: (answers: Record<string, unknown>, ctx: Context) => unknown;
|
|
19
31
|
cancelWords?: string[];
|
|
20
32
|
onCancel?: (ctx: Context) => unknown;
|
|
33
|
+
/** 'send' (default) | 'edit' satu pesan | 'delete' tanya-hapus per langkah. */
|
|
34
|
+
mode?: WizardMode;
|
|
35
|
+
/** Hapus pesan tanya terakhir saat wizard selesai (default: true bila mode 'delete'). */
|
|
36
|
+
cleanup?: boolean;
|
|
37
|
+
/** Singkirkan reply keyboard saat selesai (default: true bila tombol reply pernah dipakai). */
|
|
38
|
+
removeKeyboard?: boolean;
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
interface TeleBibzOpts {
|
|
@@ -25,13 +43,13 @@ declare module '@xbibzlibrary/telebibz' {
|
|
|
25
43
|
onError?: (err: unknown, ctx?: Context) => unknown;
|
|
26
44
|
silent?: boolean;
|
|
27
45
|
dropPending?: boolean;
|
|
28
|
-
|
|
46
|
+
session?: { initial?: () => unknown; getKey?: (ctx: Context) => string | undefined; storage?: unknown };
|
|
47
|
+
transport?: (method: string, payload?: object) => Promise<unknown>;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
class TeleBibz {
|
|
32
51
|
constructor(token: string, opts?: TeleBibzOpts);
|
|
33
|
-
|
|
34
|
-
api: Bot<Context>['api'];
|
|
52
|
+
api: any;
|
|
35
53
|
botInfo: any;
|
|
36
54
|
use(...mw: any[]): this;
|
|
37
55
|
cmd(names: string | string[], ...mw: Handler[]): this;
|
|
@@ -42,6 +60,9 @@ declare module '@xbibzlibrary/telebibz' {
|
|
|
42
60
|
wizard(id: string, def: WizardDef, bindCommand?: boolean): this;
|
|
43
61
|
wizardStart(ctx: Context, id: string): Promise<unknown>;
|
|
44
62
|
wizardActive(ctx: Context): boolean;
|
|
63
|
+
wizardCancel(ctx: Context): Promise<boolean>;
|
|
64
|
+
wizardEdit(ctx: Context, text: string, extra?: object): Promise<unknown>;
|
|
65
|
+
wizardDelete(ctx: Context): Promise<boolean>;
|
|
45
66
|
broadcast(ids: Array<number | string>, pesan: any, opts?: { delay?: number }): Promise<{ terkirim: number; gagal: number; errors: Array<{ chatId: any; pesan: string }> }>;
|
|
46
67
|
launch(opts?: Record<string, unknown>): Promise<this>;
|
|
47
68
|
handleUpdate(update: any): Promise<void>;
|
|
@@ -63,6 +84,10 @@ declare module '@xbibzlibrary/telebibz' {
|
|
|
63
84
|
define: (id: string, def: WizardDef) => string;
|
|
64
85
|
start: (ctx: Context, id: string) => Promise<unknown>;
|
|
65
86
|
active: (ctx: Context) => boolean;
|
|
87
|
+
cancel: (ctx: Context) => Promise<boolean>;
|
|
88
|
+
editAsk: (ctx: Context, text: string, extra?: object) => Promise<unknown>;
|
|
89
|
+
deleteAsk: (ctx: Context) => Promise<boolean>;
|
|
90
|
+
KEY: string;
|
|
66
91
|
};
|
|
67
92
|
|
|
68
93
|
function humanize(err: unknown): { pesan: string; saran: string | null; method?: string; code?: number };
|
package/index.js
CHANGED
|
@@ -25,6 +25,8 @@ const say = {
|
|
|
25
25
|
module.exports = {
|
|
26
26
|
// kelas utama
|
|
27
27
|
TeleBibz, Context, Composer, BotError, session,
|
|
28
|
+
// wizard (form percakapan + tombol + edit/delete)
|
|
29
|
+
wizard,
|
|
28
30
|
// keyboard & menu
|
|
29
31
|
btn, url, webApp, copy, kb, InlineKeyboard, Keyboard, Menu, MenuContainer,
|
|
30
32
|
// percakapan
|
package/lib/telebibz.js
CHANGED
|
@@ -86,6 +86,9 @@ class TeleBibz {
|
|
|
86
86
|
}
|
|
87
87
|
wizardStart(ctx, id) { return wizard.start(ctx, id); }
|
|
88
88
|
wizardActive(ctx) { return wizard.active(ctx); }
|
|
89
|
+
wizardCancel(ctx) { return wizard.cancel(ctx); }
|
|
90
|
+
wizardEdit(ctx, text, extra) { return wizard.editAsk(ctx, text, extra); }
|
|
91
|
+
wizardDelete(ctx) { return wizard.deleteAsk(ctx); }
|
|
89
92
|
|
|
90
93
|
/* ---------- broadcast ---------- */
|
|
91
94
|
broadcast(ids, pesan, opts) { return broadcast(this.api, ids, pesan, opts); }
|
package/lib/wizard.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// lib/wizard.js — percakapan tanya-jawab berurutan (form) berbasis sesi.
|
|
2
|
+
// v3.1: + TOMBOL pilihan (reply keyboard / inline keyboard), mode tampilan
|
|
3
|
+
// 'send' | 'edit' | 'delete', cleanup pesan & hapus keyboard otomatis.
|
|
2
4
|
'use strict';
|
|
3
5
|
|
|
4
6
|
const KEY = '__telebibz_wizard';
|
|
@@ -6,15 +8,28 @@ const KEY = '__telebibz_wizard';
|
|
|
6
8
|
/**
|
|
7
9
|
* Definisi wizard:
|
|
8
10
|
* {
|
|
11
|
+
* mode: 'send' | 'edit' | 'delete', // ops, default 'send' (per-step bisa override)
|
|
12
|
+
* cleanup: true, // ops: hapus pesan tanya terakhir saat selesai
|
|
13
|
+
* removeKeyboard: true, // ops: singkirkan reply keyboard saat selesai
|
|
9
14
|
* steps: [
|
|
10
15
|
* { key:'nama', ask:'Siapa namamu?' },
|
|
11
|
-
* { key:'
|
|
16
|
+
* { key:'jk', ask:'Jenis kelamin?', buttons: ['Laki-laki', 'Perempuan'] },
|
|
17
|
+
* { key:'setuju', ask:'Setuju?', inline: true,
|
|
18
|
+
* buttons: [[{ text:'✅ Ya', value:'ya' }, { text:'❌ Tidak', value:'tdk' }]] },
|
|
19
|
+
* { key:'umur', ask:'Umur berapa?', parse: Number, mode: 'edit', onlyButtons: false,
|
|
12
20
|
* validate: (n) => (n>0 && n<120) ? null : 'Umur tidak masuk akal, ulangi:' },
|
|
13
21
|
* ],
|
|
14
22
|
* done: async (answers, ctx) => {},
|
|
15
23
|
* cancelWords: ['batal', '/batal', 'cancel'], // ops, default ada
|
|
16
24
|
* onCancel: (ctx) => ctx.reply('Dibatalkan.'), // ops
|
|
17
25
|
* }
|
|
26
|
+
*
|
|
27
|
+
* Format `buttons`:
|
|
28
|
+
* ['A', 'B'] → label polos, nilai = label
|
|
29
|
+
* [{ text:'A', value:'a' }] → label + nilai berbeda (flat, disusun 2 per baris)
|
|
30
|
+
* [['A','B'], ['C']] → baris eksplisit (reply maupun inline)
|
|
31
|
+
* `inline: true` → tombol callback (klik = nilai langsung, tanpa mengetik)
|
|
32
|
+
* `onlyButtons: true` → tolak ketikan bebas, wajib pilih tombol
|
|
18
33
|
*/
|
|
19
34
|
const wizards = new Map();
|
|
20
35
|
|
|
@@ -29,14 +44,149 @@ function define(id, def) {
|
|
|
29
44
|
|
|
30
45
|
function get(id) { return wizards.get(id); }
|
|
31
46
|
|
|
47
|
+
/* ---------- normalisasi tombol ---------- */
|
|
48
|
+
function normalizeButtons(input) {
|
|
49
|
+
if (!Array.isArray(input) || !input.length) return null;
|
|
50
|
+
const norm = (b) => (b && typeof b === 'object' && b.text !== undefined
|
|
51
|
+
? { text: String(b.text), value: b.value !== undefined ? b.value : String(b.text) }
|
|
52
|
+
: { text: String(b), value: String(b) });
|
|
53
|
+
if (Array.isArray(input[0])) return input.map((r) => (Array.isArray(r) ? r : [r]).map(norm));
|
|
54
|
+
// daftar flat → susun rapi maksimal 2 tombol per baris
|
|
55
|
+
const flat = input.map(norm);
|
|
56
|
+
const rows = [];
|
|
57
|
+
for (let i = 0; i < flat.length; i += 2) rows.push(flat.slice(i, i + 2));
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
const flatten = (rows) => rows.reduce((a, r) => a.concat(r), []);
|
|
61
|
+
|
|
62
|
+
/** Susun reply_markup untuk langkah ke-`idx` (jika ada tombolnya). */
|
|
63
|
+
function stepExtra(def, st, idx) {
|
|
64
|
+
const step = def.steps[idx];
|
|
65
|
+
const extra = { ...(step.opts || {}) };
|
|
66
|
+
const rows = normalizeButtons(step.buttons);
|
|
67
|
+
if (!rows) return extra;
|
|
68
|
+
if (step.inline) {
|
|
69
|
+
let n = 0;
|
|
70
|
+
extra.reply_markup = {
|
|
71
|
+
inline_keyboard: rows.map((r) => r.map((b) => ({
|
|
72
|
+
text: b.text,
|
|
73
|
+
callback_data: `wiz:${st.cb}:${idx}:${n++}`,
|
|
74
|
+
}))),
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
st.rk = true; // pernah memakai reply keyboard → perlu dibersihkan saat selesai
|
|
78
|
+
extra.reply_markup = {
|
|
79
|
+
keyboard: rows.map((r) => r.map((b) => ({ text: b.text }))),
|
|
80
|
+
resize_keyboard: true,
|
|
81
|
+
...(step.oneTime ? { one_time_keyboard: true } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return extra;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Tampilkan pertanyaan langkah `idx` sesuai mode ('send' | 'edit' | 'delete'). */
|
|
88
|
+
async function tampil(ctx, st, def, idx, text) {
|
|
89
|
+
const step = def.steps[idx];
|
|
90
|
+
const mode = step.mode || def.mode || 'send';
|
|
91
|
+
const extra = stepExtra(def, st, idx);
|
|
92
|
+
|
|
93
|
+
if (mode === 'edit' && st.msgId) {
|
|
94
|
+
try {
|
|
95
|
+
await ctx.api.editMessageText(st.chatId, st.msgId, text, extra);
|
|
96
|
+
return;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (/not modified/i.test((e && (e.description || e.message)) || '')) return;
|
|
99
|
+
// pesan tak ditemukan (sudah dihapus) → jatuh ke kirim baru
|
|
100
|
+
}
|
|
101
|
+
} else if (mode === 'delete' && st.msgId) {
|
|
102
|
+
try { await ctx.api.deleteMessage(st.chatId, st.msgId); } catch { /* sudah hilang */ }
|
|
103
|
+
st.msgId = undefined;
|
|
104
|
+
}
|
|
105
|
+
const sent = await ctx.reply(text, extra);
|
|
106
|
+
if (sent && sent.message_id) { st.chatId = ctx.chatId; st.msgId = sent.message_id; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Bersihkan jejak wizard: hapus pesan tanya (cleanup) + singkirkan reply keyboard. */
|
|
110
|
+
async function bersih(ctx, st, def) {
|
|
111
|
+
if (!def) return;
|
|
112
|
+
const cleanupOn = def.cleanup !== undefined ? !!def.cleanup : (def.mode === 'delete');
|
|
113
|
+
if (cleanupOn && st.msgId) {
|
|
114
|
+
try { await ctx.api.deleteMessage(st.chatId, st.msgId); } catch { /* sudah hilang */ }
|
|
115
|
+
}
|
|
116
|
+
if (st.rk && def.removeKeyboard !== false) {
|
|
117
|
+
try {
|
|
118
|
+
await ctx.api.sendMessage(st.chatId, '\u200b', { reply_markup: { remove_keyboard: true } });
|
|
119
|
+
} catch { /* chat tak terjangkau */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
32
123
|
/** Mulai wizard dari handler mana pun (command/action/dll). */
|
|
33
124
|
async function start(ctx, id) {
|
|
34
125
|
const def = wizards.get(id);
|
|
35
126
|
if (!def) throw new Error(`wizard "${id}" belum didefinisikan`);
|
|
36
127
|
if (!ctx.session) throw new Error('telebibz: session belum aktif (lib internal)');
|
|
37
|
-
|
|
128
|
+
const st = { id, i: 0, ans: {}, cb: Math.random().toString(36).slice(2, 8) };
|
|
129
|
+
ctx.session[KEY] = st;
|
|
38
130
|
const s0 = def.steps[0];
|
|
39
|
-
|
|
131
|
+
const text = typeof s0.ask === 'function' ? await s0.ask(ctx) : s0.ask;
|
|
132
|
+
return tampil(ctx, st, def, 0, text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Proses satu jawaban (teks ketikan ATAU nilai tombol) untuk langkah aktif. */
|
|
136
|
+
async function jawab(ctx, st, def, input) {
|
|
137
|
+
const step = def.steps[st.i];
|
|
138
|
+
let value = input;
|
|
139
|
+
if (step.parse) value = await step.parse(input, ctx);
|
|
140
|
+
if (step.validate) {
|
|
141
|
+
const errMsg = await step.validate(value, ctx);
|
|
142
|
+
if (errMsg) {
|
|
143
|
+
const mode = step.mode || def.mode || 'send';
|
|
144
|
+
if (mode === 'send') return ctx.reply(errMsg);
|
|
145
|
+
return tampil(ctx, st, def, st.i, errMsg); // edit/delete: ubah pesan yang sama
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
st.ans[step.key] = value;
|
|
149
|
+
st.i += 1;
|
|
150
|
+
|
|
151
|
+
if (st.i >= def.steps.length) {
|
|
152
|
+
await bersih(ctx, st, def);
|
|
153
|
+
delete ctx.session[KEY];
|
|
154
|
+
return def.done(st.ans, ctx);
|
|
155
|
+
}
|
|
156
|
+
const ns = def.steps[st.i];
|
|
157
|
+
return tampil(ctx, st, def, st.i, typeof ns.ask === 'function' ? await ns.ask(ctx) : ns.ask);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Batalkan wizard yang sedang berjalan secara programatis. */
|
|
161
|
+
async function cancel(ctx) {
|
|
162
|
+
const st = ctx.session && ctx.session[KEY];
|
|
163
|
+
if (!st) return false;
|
|
164
|
+
const def = wizards.get(st.id);
|
|
165
|
+
await bersih(ctx, st, def);
|
|
166
|
+
delete ctx.session[KEY];
|
|
167
|
+
if (def && def.onCancel) { await def.onCancel(ctx); return true; }
|
|
168
|
+
await ctx.reply('✖️ Sesi dibatalkan.');
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Edit pesan-tanya wizard yang sedang tampil (fallback: kirim baru). */
|
|
173
|
+
async function editAsk(ctx, text, extra = {}) {
|
|
174
|
+
const st = ctx.session && ctx.session[KEY];
|
|
175
|
+
if (!st || !st.msgId) return ctx.reply(text, extra);
|
|
176
|
+
try { await ctx.api.editMessageText(st.chatId, st.msgId, text, extra); }
|
|
177
|
+
catch {
|
|
178
|
+
const sent = await ctx.reply(text, extra);
|
|
179
|
+
if (sent && sent.message_id) { st.chatId = ctx.chatId; st.msgId = sent.message_id; }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Hapus pesan-tanya wizard yang sedang tampil. */
|
|
184
|
+
async function deleteAsk(ctx) {
|
|
185
|
+
const st = ctx.session && ctx.session[KEY];
|
|
186
|
+
if (!st || !st.msgId) return false;
|
|
187
|
+
try { await ctx.api.deleteMessage(st.chatId, st.msgId); } catch { /* sudah hilang */ }
|
|
188
|
+
st.msgId = undefined;
|
|
189
|
+
return true;
|
|
40
190
|
}
|
|
41
191
|
|
|
42
192
|
/** Middleware global: memproses jawaban wizard yang sedang berjalan. */
|
|
@@ -47,35 +197,51 @@ function middleware() {
|
|
|
47
197
|
const def = wizards.get(st.id);
|
|
48
198
|
if (!def) { delete ctx.session[KEY]; return next(); }
|
|
49
199
|
|
|
200
|
+
/* ---- klik tombol inline (callback_query) ---- */
|
|
201
|
+
const q = ctx.callback_query;
|
|
202
|
+
if (q && typeof q.data === 'string') {
|
|
203
|
+
const m = /^wiz:([a-z0-9]+):(\d+):(\d+)$/i.exec(q.data);
|
|
204
|
+
if (m) {
|
|
205
|
+
const stale = (pesan) => ctx.answerCallbackQuery({ text: pesan, show_alert: true }).catch(() => {});
|
|
206
|
+
if (m[1] !== st.cb || Number(m[2]) !== st.i) return stale('⌛ Tombol usang — formulir sudah berpindah/berakhir.');
|
|
207
|
+
const rows = normalizeButtons(def.steps[st.i].buttons);
|
|
208
|
+
const tombol = rows && flatten(rows)[Number(m[3])];
|
|
209
|
+
if (!tombol) return stale('⌛ Tombol usang — formulir sudah berpindah/berakhir.');
|
|
210
|
+
await ctx.answerCallbackQuery().catch(() => {});
|
|
211
|
+
return jawab(ctx, st, def, tombol.value);
|
|
212
|
+
}
|
|
213
|
+
// callback lain saat wizard aktif → diamkan, wizard menunggu (konsisten non-teks)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ---- jawaban teks ---- */
|
|
50
217
|
const text = ctx.message && typeof ctx.message.text === 'string' ? ctx.message.text.trim() : null;
|
|
51
218
|
if (text === null) return; // pesan non-teks (foto/stiker) — diamkan, wizard menunggu
|
|
52
219
|
|
|
53
220
|
if (def.cancelWords.has(text.toLowerCase())) {
|
|
221
|
+
await bersih(ctx, st, def);
|
|
54
222
|
delete ctx.session[KEY];
|
|
55
223
|
if (def.onCancel) return def.onCancel(ctx);
|
|
56
224
|
return ctx.reply('✖️ Sesi dibatalkan.');
|
|
57
225
|
}
|
|
58
226
|
|
|
59
227
|
const step = def.steps[st.i];
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return def.done(st.ans, ctx);
|
|
228
|
+
let input = text;
|
|
229
|
+
const rows = normalizeButtons(step.buttons);
|
|
230
|
+
if (rows) {
|
|
231
|
+
const hit = flatten(rows).find((b) => b.text.toLowerCase() === text.toLowerCase());
|
|
232
|
+
if (hit) input = hit.value; // tekan tombol reply → pakai nilainya
|
|
233
|
+
else if (step.onlyButtons) {
|
|
234
|
+
const pesan = step.onlyButtons === true
|
|
235
|
+
? '✋ Pilih salah satu tombol yang tersedia ya:'
|
|
236
|
+
: step.onlyButtons;
|
|
237
|
+
return tampil(ctx, st, def, st.i, pesan);
|
|
238
|
+
}
|
|
72
239
|
}
|
|
73
|
-
|
|
74
|
-
return ctx.reply(typeof ns.ask === 'function' ? await ns.ask(ctx) : ns.ask, ns.opts || {});
|
|
240
|
+
return jawab(ctx, st, def, input);
|
|
75
241
|
};
|
|
76
242
|
}
|
|
77
243
|
|
|
78
244
|
/** True kalau user ini sedang di tengah wizard. */
|
|
79
245
|
function active(ctx) { return Boolean(ctx.session && ctx.session[KEY]); }
|
|
80
246
|
|
|
81
|
-
module.exports = { define, get, start, middleware, active, KEY };
|
|
247
|
+
module.exports = { define, get, start, middleware, active, cancel, editAsk, deleteAsk, KEY };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbibzlibrary/telebibz",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Library Telegram Indonesia production-grade — set fitur penuh setara grammY: proxy-api segala metode, transformer, menus, inline query, media group, throttler & auto-retry, session swappable, webhook, polling tahan-409. //—Xbibz Official—//",
|
|
3
|
+
"version": "3.1.1",
|
|
4
|
+
"description": "Library Telegram Indonesia production-grade — set fitur penuh setara grammY: proxy-api segala metode, transformer, menus, inline query, media group, throttler & auto-retry, wizard + tombol + edit/delete, session swappable, webhook, polling tahan-409. //—Xbibz Official—//",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"type": "commonjs",
|
package/test/all.test.js
CHANGED
|
@@ -6,7 +6,7 @@ const { TeleBibz, btn, url, kb, webApp, copy, humanize, File, InlineKeyboard,
|
|
|
6
6
|
Menu, MenuContainer, iq, autoRetry, throttler, limiter, InputMediaBuilder } = require('..');
|
|
7
7
|
|
|
8
8
|
let N = 0, OK = 0;
|
|
9
|
-
const TOTAL =
|
|
9
|
+
const TOTAL = 30;
|
|
10
10
|
const t = (nama, fn) => {
|
|
11
11
|
const done = () => { N++; if (N === TOTAL) { console.log(`\n${OK}/${N} lulus`); process.exit(OK === N ? 0 : 1); } };
|
|
12
12
|
Promise.resolve()
|
|
@@ -332,3 +332,108 @@ t('webhook(): body JSON diproses handleUpdate', async () => {
|
|
|
332
332
|
assert.strictEqual(status, 200);
|
|
333
333
|
assert.ok(texts(calls).some((x) => /hai webhook/.test(x)));
|
|
334
334
|
});
|
|
335
|
+
/* ===== 25–30. wizard v3.1: tombol + edit/delete ===== */
|
|
336
|
+
t('wizard v3.1: tombol reply — label cocok → nilai; onlyButtons menahan ketikan bebas', async () => {
|
|
337
|
+
const { bot, calls } = buatBot(); await boot(bot);
|
|
338
|
+
const jawab = [];
|
|
339
|
+
bot.wizard('survey', {
|
|
340
|
+
steps: [
|
|
341
|
+
{ key: 'jk', ask: 'Jenis kelamin?', buttons: ['Laki-laki', 'Perempuan'], onlyButtons: true },
|
|
342
|
+
{ key: 'kota', ask: 'Kota?' },
|
|
343
|
+
],
|
|
344
|
+
done: async (ans) => { jawab.push(ans); },
|
|
345
|
+
});
|
|
346
|
+
await bot.handleUpdate(uMsg('/survey'));
|
|
347
|
+
const ask = calls.find((c) => c.method === 'sendMessage' && c.payload.text === 'Jenis kelamin?');
|
|
348
|
+
assert.ok(ask && ask.payload.reply_markup.keyboard[0][0].text === 'Laki-laki');
|
|
349
|
+
await bot.handleUpdate(uMsg('terserah')); // bukan tombol → ditolak, wizard tetap di langkah 1
|
|
350
|
+
assert.strictEqual(jawab.length, 0);
|
|
351
|
+
await bot.handleUpdate(uMsg('Perempuan')); // ketuk tombol
|
|
352
|
+
await bot.handleUpdate(uMsg('Magetan'));
|
|
353
|
+
assert.deepStrictEqual(jawab, [{ jk: 'Perempuan', kota: 'Magetan' }]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
t('wizard v3.1: tombol inline + mode edit → editMessageText, bukan pesan baru', async () => {
|
|
357
|
+
const { bot, calls } = buatBot(); await boot(bot);
|
|
358
|
+
const jawab = [];
|
|
359
|
+
bot.wizard('vote', {
|
|
360
|
+
mode: 'edit',
|
|
361
|
+
steps: [
|
|
362
|
+
{ key: 'pilih', ask: 'Pilih salah satu', inline: true, buttons: [[{ text: 'Opsi A', value: 'a' }, { text: 'Opsi B', value: 'b' }]] },
|
|
363
|
+
{ key: 'nama', ask: 'Siapa namamu?' },
|
|
364
|
+
],
|
|
365
|
+
done: async (ans) => { jawab.push(ans); },
|
|
366
|
+
});
|
|
367
|
+
await bot.handleUpdate(uMsg('/vote'));
|
|
368
|
+
const ask = calls.find((c) => c.method === 'sendMessage' && c.payload.text === 'Pilih salah satu');
|
|
369
|
+
const data = ask.payload.reply_markup.inline_keyboard[0][1].callback_data;
|
|
370
|
+
assert.ok(/^wiz:[a-z0-9]+:0:1$/.test(data));
|
|
371
|
+
await bot.handleUpdate({ update_id: ++uid, callback_query: {
|
|
372
|
+
id: 'wq1', from: USER, chat_instance: 'x', data,
|
|
373
|
+
message: { message_id: ask.message_id, from: { id: 99, is_bot: true, first_name: 'B' }, chat: CHAT, date: 1 },
|
|
374
|
+
} });
|
|
375
|
+
assert.ok(calls.some((c) => c.method === 'editMessageText' && c.payload.text === 'Siapa namamu?'), 'langkah 2 harus lewat editMessageText');
|
|
376
|
+
assert.ok(!calls.some((c) => c.method === 'sendMessage' && c.payload.text === 'Siapa namamu?'), 'tidak boleh kirim pesan baru');
|
|
377
|
+
assert.ok(calls.some((c) => c.method === 'answerCallbackQuery' && c.payload.callback_query_id === 'wq1'));
|
|
378
|
+
await bot.handleUpdate(uMsg('Budi'));
|
|
379
|
+
assert.deepStrictEqual(jawab, [{ pilih: 'b', nama: 'Budi' }]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
t('wizard v3.1: mode delete → pesan lama dihapus; cleanup hapus pesan tanya terakhir', async () => {
|
|
383
|
+
const { bot, calls } = buatBot(); await boot(bot);
|
|
384
|
+
bot.wizard('hapus', {
|
|
385
|
+
mode: 'delete',
|
|
386
|
+
steps: [{ key: 'a', ask: 'Pertanyaan A?' }, { key: 'b', ask: 'Pertanyaan B?' }],
|
|
387
|
+
done: () => {},
|
|
388
|
+
});
|
|
389
|
+
await bot.handleUpdate(uMsg('/hapus'));
|
|
390
|
+
const idxA = calls.findIndex((c) => c.method === 'sendMessage' && c.payload.text === 'Pertanyaan A?');
|
|
391
|
+
assert.ok(idxA >= 0);
|
|
392
|
+
const idA = idxA + 1; // stub transport: message_id = calls.length saat push
|
|
393
|
+
await bot.handleUpdate(uMsg('satu'));
|
|
394
|
+
const dihapus = () => calls.filter((c) => c.method === 'deleteMessage').map((c) => c.payload.message_id);
|
|
395
|
+
assert.ok(dihapus().includes(idA), 'pesan A harus dihapus sebelum tanya B');
|
|
396
|
+
const idxB = calls.findIndex((c) => c.method === 'sendMessage' && c.payload.text === 'Pertanyaan B?');
|
|
397
|
+
await bot.handleUpdate(uMsg('dua'));
|
|
398
|
+
assert.ok(dihapus().includes(idxB + 1), 'cleanup: pesan tanya terakhir ikut dihapus');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
t('wizard v3.1: reply keyboard otomatis disingkirkan saat wizard selesai', async () => {
|
|
402
|
+
const { bot, calls } = buatBot(); await boot(bot);
|
|
403
|
+
bot.wizard('kbd', { steps: [{ key: 'x', ask: 'Pilih:', buttons: ['Ok', 'Tidak'] }], done: () => {} });
|
|
404
|
+
await bot.handleUpdate(uMsg('/kbd'));
|
|
405
|
+
await bot.handleUpdate(uMsg('Ok'));
|
|
406
|
+
assert.ok(calls.some((c) => c.method === 'sendMessage' &&
|
|
407
|
+
c.payload.reply_markup && c.payload.reply_markup.remove_keyboard === true));
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
t('wizard v3.1: klik tombol usang → alert aman, wizard tidak rusak', async () => {
|
|
411
|
+
const { bot, calls } = buatBot(); await boot(bot);
|
|
412
|
+
bot.wizard('prog', { steps: [{ key: 'a', ask: 'A?', inline: true, buttons: ['X'] }], done: () => {} });
|
|
413
|
+
await bot.handleUpdate(uMsg('/prog'));
|
|
414
|
+
await bot.handleUpdate({ update_id: ++uid, callback_query: {
|
|
415
|
+
id: 'wq2', from: USER, chat_instance: 'x', data: 'wiz:tokenpalsu:0:0',
|
|
416
|
+
message: { message_id: 1, from: { id: 99, is_bot: true, first_name: 'B' }, chat: CHAT, date: 1 },
|
|
417
|
+
} });
|
|
418
|
+
const alert = calls.find((c) => c.method === 'answerCallbackQuery' && /usang/i.test(c.payload.text || ''));
|
|
419
|
+
assert.ok(alert && alert.payload.show_alert === true);
|
|
420
|
+
await bot.handleUpdate(uMsg('jawaban asli')); // wizard masih berjalan normal
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
t('wizard v3.1: wizard.cancel() membatalkan sesi secara programatis', async () => {
|
|
424
|
+
const wiz = require('../lib/wizard');
|
|
425
|
+
wiz.define('manual', { steps: [{ key: 'a', ask: 'A?' }], done: () => {}, onCancel: () => {} });
|
|
426
|
+
const replies = [];
|
|
427
|
+
const ctx = {
|
|
428
|
+
session: {},
|
|
429
|
+
chatId: 555,
|
|
430
|
+
reply: async (t) => { replies.push(t); return { message_id: 42 }; },
|
|
431
|
+
api: { editMessageText: async () => true, deleteMessage: async () => true, sendMessage: async () => true },
|
|
432
|
+
};
|
|
433
|
+
await wiz.start(ctx, 'manual');
|
|
434
|
+
assert.strictEqual(wiz.active(ctx), true);
|
|
435
|
+
assert.deepStrictEqual(replies, ['A?']);
|
|
436
|
+
assert.strictEqual(await wiz.cancel(ctx), true);
|
|
437
|
+
assert.strictEqual(wiz.active(ctx), false);
|
|
438
|
+
assert.strictEqual(await wiz.cancel(ctx), false); // sudah tidak aktif
|
|
439
|
+
});
|