@xbibzlibrary/telebibz 3.1.2 → 3.1.3

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 (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +1 -1
  3. package/NOTICE.md +15 -8
  4. package/README.id.md +85 -16
  5. package/README.md +84 -16
  6. package/examples/08-rich-message.js +34 -0
  7. package/index.d.ts +300 -96
  8. package/index.js +8 -2
  9. package/lib/api.js +20 -6
  10. package/lib/composer.js +14 -2
  11. package/lib/context.js +83 -21
  12. package/lib/file.js +8 -1
  13. package/lib/inline-query.js +6 -1
  14. package/lib/logger.js +44 -14
  15. package/lib/ratelimit.js +16 -3
  16. package/lib/rich.js +170 -0
  17. package/lib/runner.js +14 -5
  18. package/lib/session.js +12 -4
  19. package/lib/telebibz.js +40 -24
  20. package/lib/telegram-methods.js +192 -0
  21. package/package.json +14 -10
  22. package/test/audit.test.js +320 -0
  23. package/test/types.test.ts +40 -0
  24. package/types/telegram-bot-api/LICENSE +21 -0
  25. package/types/telegram-bot-api/api.d.ts +22 -0
  26. package/types/telegram-bot-api/checklist.d.ts +72 -0
  27. package/types/telegram-bot-api/inline.d.ts +692 -0
  28. package/types/telegram-bot-api/langs.d.ts +193 -0
  29. package/types/telegram-bot-api/manage.d.ts +1153 -0
  30. package/types/telegram-bot-api/markup.d.ts +278 -0
  31. package/types/telegram-bot-api/message.d.ts +1557 -0
  32. package/types/telegram-bot-api/methods.d.ts +2860 -0
  33. package/types/telegram-bot-api/mod.d.ts +14 -0
  34. package/types/telegram-bot-api/passport.d.ts +163 -0
  35. package/types/telegram-bot-api/payment.d.ts +576 -0
  36. package/types/telegram-bot-api/rich.d.ts +1198 -0
  37. package/types/telegram-bot-api/settings.d.ts +120 -0
  38. package/types/telegram-bot-api/story.d.ts +89 -0
  39. package/types/telegram-bot-api/update.d.ts +86 -0
package/lib/context.js CHANGED
@@ -8,15 +8,18 @@ const fs = require('fs');
8
8
  /** Objek "pesan" dari berbagai field update (gaya ctx.msg). */
9
9
  function msgOf(update) {
10
10
  return update.message || update.edited_message || update.channel_post ||
11
- update.edited_channel_post || update.business_message || update.edited_business_message || null;
11
+ update.edited_channel_post || update.business_message || update.edited_business_message ||
12
+ update.guest_message || null;
12
13
  }
13
14
 
14
15
  const UPDATE_KEYS = [
15
16
  'message', 'edited_message', 'channel_post', 'edited_channel_post',
16
- 'business_message', 'edited_business_message', 'callback_query', 'inline_query',
17
+ 'business_message', 'edited_business_message', 'deleted_business_messages', 'guest_message',
18
+ 'callback_query', 'inline_query',
17
19
  'chosen_inline_result', 'chat_join_request', 'chat_member', 'my_chat_member',
18
20
  'message_reaction', 'message_reaction_count', 'business_connection',
19
21
  'poll', 'poll_answer', 'shipping_query', 'pre_checkout_query', 'purchased_paid_media',
22
+ 'chat_boost', 'removed_chat_boost', 'stopped_message_generation',
20
23
  ];
21
24
 
22
25
  class Context {
@@ -37,6 +40,10 @@ class Context {
37
40
  if (this.chat_join_request) return this.chat_join_request.chat;
38
41
  if (this.chat_member) return this.chat_member.chat;
39
42
  if (this.my_chat_member) return this.my_chat_member.chat;
43
+ if (this.deleted_business_messages) return this.deleted_business_messages.chat;
44
+ if (this.chat_boost) return this.chat_boost.chat;
45
+ if (this.removed_chat_boost) return this.removed_chat_boost.chat;
46
+ if (this.stopped_message_generation) return this.stopped_message_generation.chat;
40
47
  return undefined;
41
48
  }
42
49
  get from() {
@@ -49,9 +56,11 @@ class Context {
49
56
  get chatId() { const c = this.chat; return c ? c.id : undefined; }
50
57
  get msgId() { return this.msg ? this.msg.message_id : undefined; }
51
58
  get businessConnectionId() {
59
+ const msg = this.msg || (this.callback_query && this.callback_query.message);
52
60
  return (this.business_connection && this.business_connection.id) ||
53
- (this.msg && this.msg.business_connection_id) || undefined;
61
+ (msg && msg.business_connection_id) || undefined;
54
62
  }
63
+ get guestQueryId() { return this.msg && this.msg.guest_query_id; }
55
64
  get senderChat() { return this.msg ? this.msg.sender_chat : undefined; }
56
65
  get inlineMessageId() { return this.callback_query ? this.callback_query.inline_message_id : undefined; }
57
66
 
@@ -62,13 +71,33 @@ class Context {
62
71
  return c;
63
72
  }
64
73
  _biz(extra = {}) {
65
- // Otomatis sertakan business_connection_id pada pesan business (gaya BizWrapper)
74
+ // Sertakan business_connection_id untuk pesan business, termasuk callback atas pesannya.
75
+ const msg = this.msg || (this.callback_query && this.callback_query.message);
66
76
  const bc = this.businessConnectionId;
67
- return bc && this.msg && this.msg.business_connection_id ? { business_connection_id: bc, ...extra } : extra;
77
+ return bc && msg && msg.business_connection_id ? { business_connection_id: bc, ...extra } : extra;
68
78
  }
69
79
 
70
80
  /* ---------- kirim balasan ---------- */
71
81
  reply(text, extra = {}) { return this.api.sendMessage(this._cid(), text, this._biz(extra)); }
82
+ replyWithRichMessage(richMessage, extra = {}) {
83
+ return this.api.sendRichMessage({ chat_id: this._cid(), rich_message: richMessage, ...this._biz(extra) });
84
+ }
85
+ replyWithLivePhoto(livePhoto, photo, extra = {}) {
86
+ return this.api.sendLivePhoto({ chat_id: this._cid(), live_photo: livePhoto, photo, ...this._biz(extra) });
87
+ }
88
+ replyEphemeral(text, receiverUserId, extra = {}) {
89
+ const { ephemeral_message_parameters = {}, ...other } = extra;
90
+ return this.api.sendMessage(this._cid(), text, {
91
+ ...this._biz(other),
92
+ ephemeral_message_parameters: { ...ephemeral_message_parameters, receiver_user_id: receiverUserId },
93
+ });
94
+ }
95
+ sendMessageDraft(draftId, text, extra = {}) {
96
+ return this.api.sendMessageDraft({ chat_id: this._cid(), draft_id: draftId, text, ...this._biz(extra) });
97
+ }
98
+ sendRichMessageDraft(draftId, richMessage, extra = {}) {
99
+ return this.api.sendRichMessageDraft({ chat_id: this._cid(), draft_id: draftId, rich_message: richMessage, ...this._biz(extra) });
100
+ }
72
101
  replyWithHTML(text, extra = {}) { return this.reply(text, { parse_mode: 'HTML', ...extra }); }
73
102
  replyWithMarkdown(text, extra = {}) { return this.reply(text, { parse_mode: 'MarkdownV2', ...extra }); }
74
103
  replyWithPhoto(p, e = {}) { return this.api.sendPhoto(this._cid(), p, this._biz(e)); }
@@ -76,19 +105,19 @@ class Context {
76
105
  replyWithAudio(a, e = {}) { return this.api.sendAudio(this._cid(), a, this._biz(e)); }
77
106
  replyWithDocument(d, e = {}) { return this.api.sendDocument(this._cid(), d, this._biz(e)); }
78
107
  replyWithAnimation(a, e = {}) { return this.api.sendAnimation(this._cid(), a, this._biz(e)); }
79
- replyWithVoice(v, e = {}) { return this.api.sendVoice(this._cid(), v, { caption: e.caption, ...e }); }
80
- replyWithVideoNote(v, e = {}) { return this.api.sendVideoNote(this._cid(), v, e); }
81
- replyWithSticker(s, e = {}) { return this.api.sendSticker(this._cid(), s, e); }
82
- replyWithMediaGroup(media, e = {}) { return this.api.sendMediaGroup(this._cid(), media, e); }
83
- replyWithLocation(lat, lon, e = {}) { return this.api.sendLocation(this._cid(), lat, lon, e); }
84
- replyWithVenue(lat, lon, title, address, e = {}) { return this.api.sendVenue(this._cid(), lat, lon, title, address, e); }
85
- replyWithContact(phone, first, e = {}) { return this.api.sendContact(this._cid(), phone, first, e); }
86
- replyWithPoll(question, options, e = {}) { return this.api.sendPoll(this._cid(), question, options, e); }
87
- replyWithDice(emoji, e = {}) { return this.api.sendDice(this._cid(), emoji, e); }
108
+ replyWithVoice(v, e = {}) { return this.api.sendVoice(this._cid(), v, this._biz(e)); }
109
+ replyWithVideoNote(v, e = {}) { return this.api.sendVideoNote(this._cid(), v, this._biz(e)); }
110
+ replyWithSticker(s, e = {}) { return this.api.sendSticker(this._cid(), s, this._biz(e)); }
111
+ replyWithMediaGroup(media, e = {}) { return this.api.sendMediaGroup(this._cid(), media, this._biz(e)); }
112
+ replyWithLocation(lat, lon, e = {}) { return this.api.sendLocation(this._cid(), lat, lon, this._biz(e)); }
113
+ replyWithVenue(lat, lon, title, address, e = {}) { return this.api.sendVenue(this._cid(), lat, lon, title, address, this._biz(e)); }
114
+ replyWithContact(phone, first, e = {}) { return this.api.sendContact(this._cid(), phone, first, this._biz(e)); }
115
+ replyWithPoll(question, options, e = {}) { return this.api.sendPoll(this._cid(), question, options, this._biz(e)); }
116
+ replyWithDice(emoji, e = {}) { return this.api.sendDice(this._cid(), emoji, this._biz(e)); }
88
117
  replyWithInvoice(title, description, payload, currency, prices, e = {}) {
89
- return this.api.sendInvoice(this._cid(), title, description, payload, currency, prices, e);
118
+ return this.api.sendInvoice(this._cid(), title, description, payload, currency, prices, this._biz(e));
90
119
  }
91
- replyWithChatAction(action = 'typing', e = {}) { return this.api.sendChatAction(this._cid(), action, e); }
120
+ replyWithChatAction(action = 'typing', e = {}) { return this.api.sendChatAction(this._cid(), action, this._biz(e)); }
92
121
 
93
122
  /* ---------- teruskan & salin ---------- */
94
123
  forwardMessage(to, from, messageId, e = {}) {
@@ -104,22 +133,50 @@ class Context {
104
133
  editMessageText(text, e = {}) {
105
134
  if (this.inlineMessageId) return this.api.callApi('editMessageText', { inline_message_id: this.inlineMessageId, text, ...e });
106
135
  const m = this.callback_query && this.callback_query.message;
107
- return this.api.editMessageText(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, text, e);
136
+ return this.api.editMessageText(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, text, this._biz(e));
137
+ }
138
+ editRichMessage(richMessage, e = {}) {
139
+ const m = this.callback_query && this.callback_query.message;
140
+ if (this.inlineMessageId) return this.api.callApi('editMessageText', { inline_message_id: this.inlineMessageId, rich_message: richMessage, ...e });
141
+ return this.api.callApi('editMessageText', {
142
+ chat_id: this._cid(m ? m.chat.id : undefined),
143
+ message_id: m ? m.message_id : this.msgId,
144
+ rich_message: richMessage,
145
+ ...this._biz(e),
146
+ });
108
147
  }
109
148
  editMessageCaption(e = {}) {
110
149
  const m = this.callback_query && this.callback_query.message;
111
150
  if (this.inlineMessageId) return this.api.callApi('editMessageCaption', { inline_message_id: this.inlineMessageId, ...e });
112
- return this.api.editMessageCaption(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, e);
151
+ return this.api.editMessageCaption(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, this._biz(e));
113
152
  }
114
153
  editMessageMedia(media, e = {}) {
115
154
  const m = this.callback_query && this.callback_query.message;
116
155
  if (this.inlineMessageId) return this.api.callApi('editMessageMedia', { inline_message_id: this.inlineMessageId, media, ...e });
117
- return this.api.editMessageMedia(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, media, e);
156
+ return this.api.editMessageMedia(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, media, this._biz(e));
118
157
  }
119
158
  editMessageReplyMarkup(e = {}) {
120
159
  const m = this.callback_query && this.callback_query.message;
121
160
  if (this.inlineMessageId) return this.api.callApi('editMessageReplyMarkup', { inline_message_id: this.inlineMessageId, ...e });
122
- return this.api.editMessageReplyMarkup(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, e);
161
+ return this.api.editMessageReplyMarkup(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, this._biz(e));
162
+ }
163
+ editEphemeralMessageText(receiverUserId, ephemeralMessageId, text, extra = {}) {
164
+ return this.api.editEphemeralMessageText({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId, text, ...extra });
165
+ }
166
+ editEphemeralRichMessage(receiverUserId, ephemeralMessageId, richMessage, extra = {}) {
167
+ return this.api.editEphemeralMessageText({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId, rich_message: richMessage, ...extra });
168
+ }
169
+ editEphemeralMessageMedia(receiverUserId, ephemeralMessageId, media, extra = {}) {
170
+ return this.api.editEphemeralMessageMedia({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId, media, ...extra });
171
+ }
172
+ editEphemeralMessageCaption(receiverUserId, ephemeralMessageId, extra = {}) {
173
+ return this.api.editEphemeralMessageCaption({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId, ...extra });
174
+ }
175
+ editEphemeralMessageReplyMarkup(receiverUserId, ephemeralMessageId, extra = {}) {
176
+ return this.api.editEphemeralMessageReplyMarkup({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId, ...extra });
177
+ }
178
+ deleteEphemeralMessage(receiverUserId, ephemeralMessageId) {
179
+ return this.api.deleteEphemeralMessage({ chat_id: this._cid(), receiver_user_id: receiverUserId, ephemeral_message_id: ephemeralMessageId });
123
180
  }
124
181
  deleteMessage(chatId, messageId) {
125
182
  if (messageId !== undefined) return this.api.deleteMessage(chatId, messageId);
@@ -128,8 +185,9 @@ class Context {
128
185
  }
129
186
  deleteMessages(ids) { return this.api.deleteMessages(this._cid(), ids); }
130
187
  react(emoji = '👍', e = {}) {
188
+ const m = this.callback_query && this.callback_query.message;
131
189
  const reaction = [{ type: 'emoji', emoji }];
132
- return this.api.setMessageReaction(this._cid(), this.msgId, reaction, e);
190
+ return this.api.setMessageReaction(this._cid(m ? m.chat.id : undefined), m ? m.message_id : this.msgId, reaction, e);
133
191
  }
134
192
 
135
193
  /* ---------- callback & inline ---------- */
@@ -142,6 +200,10 @@ class Context {
142
200
  if (!this.inline_query) return Promise.resolve(false);
143
201
  return this.api.answerInlineQuery(this.inline_query.id, results, e);
144
202
  }
203
+ answerGuestQuery(result) {
204
+ if (!this.guestQueryId) return Promise.resolve(false);
205
+ return this.api.answerGuestQuery({ guest_query_id: this.guestQueryId, result });
206
+ }
145
207
 
146
208
  /* ---------- admin & grup ---------- */
147
209
  banChatMember(cid, uid, e = {}) { return this.api.banChatMember(cid, uid, e); }
package/lib/file.js CHANGED
@@ -33,9 +33,16 @@ class InputFile extends File {}
33
33
  const InputMediaBuilder = {
34
34
  photo: (media, extra = {}) => ({ type: 'photo', media, ...extra }),
35
35
  video: (media, extra = {}) => ({ type: 'video', media, ...extra }),
36
+ livePhoto: (media, photo, extra = {}) => ({ type: 'live_photo', media, photo, ...extra }),
36
37
  document: (media, extra = {}) => ({ type: 'document', media, ...extra }),
37
38
  audio: (media, extra = {}) => ({ type: 'audio', media, ...extra }),
38
39
  animation: (media, extra = {}) => ({ type: 'animation', media, ...extra }),
39
40
  };
40
41
 
41
- module.exports = { File, InputFile, InputMediaBuilder };
42
+ const InputPaidMediaBuilder = {
43
+ photo: (media, extra = {}) => ({ type: 'photo', media, ...extra }),
44
+ video: (media, extra = {}) => ({ type: 'video', media, ...extra }),
45
+ livePhoto: (media, photo, extra = {}) => ({ type: 'live_photo', media, photo, ...extra }),
46
+ };
47
+
48
+ module.exports = { File, InputFile, InputMediaBuilder, InputPaidMediaBuilder };
@@ -9,13 +9,18 @@ function matchInlineQuery(trigger) {
9
9
  if (trigger === undefined || trigger === '*') return true;
10
10
  const list = [].concat(trigger);
11
11
  const text = q.query || '';
12
- return list.some((t) => t instanceof RegExp ? t.test(text) : text.toLowerCase().includes(String(t).toLowerCase()));
12
+ return list.some((t) => {
13
+ if (!(t instanceof RegExp)) return text.toLowerCase().includes(String(t).toLowerCase());
14
+ t.lastIndex = 0;
15
+ return t.test(text);
16
+ });
13
17
  };
14
18
  }
15
19
 
16
20
  /** Builder hasil inline (persis spek Bot API). */
17
21
  const iq = {
18
22
  article: (id, title, extra = {}) => ({ type: 'article', id: String(id), title, input_message_content: { message_text: extra.message_text || title, ...(extra.input_message_content || {}) }, ...omit(extra, ['message_text', 'input_message_content']) }),
23
+ richArticle: (id, title, richMessage, extra = {}) => ({ type: 'article', id: String(id), title, input_message_content: { rich_message: richMessage }, ...omit(extra, ['input_message_content']) }),
19
24
  photo: (id, url, thumb, extra = {}) => ({ type: 'photo', id: String(id), photo_url: url, thumbnail_url: thumb || url, ...extra }),
20
25
  gif: (id, url, thumb, extra = {}) => ({ type: 'gif', id: String(id), gif_url: url, thumbnail_url: thumb || url, ...extra }),
21
26
  video: (id, url, thumb, title, extra = {}) => ({ type: 'video', id: String(id), video_url: url, thumbnail_url: thumb, title, mime_type: 'video/mp4', input_message_content: { message_text: url }, ...extra }),
package/lib/logger.js CHANGED
@@ -1,14 +1,17 @@
1
- // lib/logger.js — log terminal cantik tanpa dependency.
1
+ // lib/logger.js — log terminal rapi, berwarna, dan mudah dibaca.
2
2
  'use strict';
3
3
 
4
4
  const C = {
5
- reset: '', dim: '', bold: '',
6
- red: '', green: '', yellow: '', blue: '',
7
- magenta: '', cyan: '', gray: '',
5
+ reset: '\u001b[0m', dim: '\u001b[2m', bold: '\u001b[1m',
6
+ red: '\u001b[31m', green: '\u001b[32m', yellow: '\u001b[33m', blue: '\u001b[34m',
7
+ magenta: '\u001b[35m', cyan: '\u001b[36m', gray: '\u001b[90m',
8
+ developer: '\u001b[1;35m', border: '\u001b[36m',
9
+ title: '\u001b[1;97;44m', panel: '\u001b[97;44m',
8
10
  };
9
11
 
10
12
  function paint(code, s) {
11
- return process.stdout && process.stdout.isTTY ? `${C[code]}${s}${C.reset}` : String(s);
13
+ const colorsEnabled = process.stdout && process.stdout.isTTY && !process.env.NO_COLOR;
14
+ return colorsEnabled ? `${C[code]}${s}${C.reset}` : String(s);
12
15
  }
13
16
 
14
17
  function stamp() {
@@ -23,16 +26,43 @@ const log = {
23
26
  paint,
24
27
  };
25
28
 
26
- /** Banner ASCII saat bot nyala. */
27
- log.banner = (title, rows) => {
28
- const width = Math.max(title.length, ...rows.map((r) => strip(r).length)) + 2;
29
- const line = '─'.repeat(width + 2);
30
- console.log(`┌${line}┐`);
31
- console.log(`│ ${paint('bold', title)}${' '.repeat(width - title.length)}│`);
32
- for (const r of rows) console.log(`│ ${r}${' '.repeat(Math.max(0, width - strip(r).length))}│`);
33
- console.log(`└${line}┘`);
29
+ /** Banner boot: identitas developer berada di atas panel warna. */
30
+ log.banner = (title, rows = [], developer = 'Xbibz Technology ID') => {
31
+ const innerWidth = Math.max(displayWidth(title), ...rows.map((r) => displayWidth(strip(r)))) + 4;
32
+ const top = `┏${'━'.repeat(innerWidth)}┓`;
33
+ const divider = `┣${'━'.repeat(innerWidth)}┫`;
34
+ const bottom = `┗${'━'.repeat(innerWidth)}┛`;
35
+ const line = (text, color) => {
36
+ const plain = ` ${text}${' '.repeat(Math.max(0, innerWidth - displayWidth(text) - 4))} `;
37
+ console.log(`${paint('border', '┃')}${paint(color, plain)}${paint('border', '┃')}`);
38
+ };
39
+
40
+ console.log(paint('developer', `◆ DEVELOPER ${developer}`));
41
+ console.log(paint('border', top));
42
+ line(title, 'title');
43
+ console.log(paint('border', divider));
44
+ for (const row of rows) line(strip(row), 'panel');
45
+ console.log(paint('border', bottom));
34
46
  };
35
47
 
36
- function strip(s) { return String(s).replace(/\[\d+m/g, ''); }
48
+ function strip(s) { return String(s).replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, ''); }
49
+
50
+ // Hitung lebar tampilan terminal (emoji/CJK = 2 kolom; combining mark = 0).
51
+ function displayWidth(value) {
52
+ let width = 0;
53
+ for (const ch of String(value)) {
54
+ const cp = ch.codePointAt(0);
55
+ if (/\p{Mark}/u.test(ch) || cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f)) continue;
56
+ width += cp >= 0x1100 && (
57
+ cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
58
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
59
+ (cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
60
+ (cp >= 0xfe10 && cp <= 0xfe19) || (cp >= 0xfe30 && cp <= 0xfe6f) ||
61
+ (cp >= 0xff00 && cp <= 0xff60) || (cp >= 0xffe0 && cp <= 0xffe6) ||
62
+ (cp >= 0x1f300 && cp <= 0x1faff) || (cp >= 0x20000 && cp <= 0x3fffd)
63
+ ) ? 2 : 1;
64
+ }
65
+ return width;
66
+ }
37
67
 
38
68
  module.exports = log;
package/lib/ratelimit.js CHANGED
@@ -28,24 +28,31 @@ function autoRetry({ maxRetry = 5, baseDelayMs = 500 } = {}) {
28
28
  /** Transformer antrean: jaga maks `perSecond` panggilan API global.
29
29
  * Pasang: bot.api.config.use(throttler()) */
30
30
  function throttler({ perSecond = 28 } = {}) {
31
+ if (!Number.isFinite(perSecond) || perSecond <= 0) throw new RangeError('throttler: perSecond harus > 0');
31
32
  const interval = 1000 / perSecond;
32
33
  let last = 0; let chain = Promise.resolve();
33
34
  return (prev, method, payload) => {
34
- chain = chain.then(async () => {
35
+ const task = chain.then(async () => {
35
36
  const wait = last + interval - Date.now();
36
37
  if (wait > 0) await new Promise((r) => setTimeout(r, wait));
37
38
  last = Date.now();
38
39
  return prev(method, payload);
39
40
  });
40
- return chain;
41
+ // Satu request gagal tidak boleh meracuni antrean request berikutnya.
42
+ chain = task.then(() => undefined, () => undefined);
43
+ return task;
41
44
  };
42
45
  }
43
46
 
44
47
  /** Middleware anti-spam: maks `limit` update per user per `windowMs`. */
45
48
  function limiter({ windowMs = 2000, limit = 3, onExceeded } = {}) {
49
+ if (!Number.isFinite(windowMs) || windowMs <= 0 || !Number.isInteger(limit) || limit <= 0) {
50
+ throw new RangeError('limiter: windowMs dan limit harus > 0');
51
+ }
46
52
  const bucket = new Map();
53
+ let checks = 0;
47
54
  return async (ctx, next) => {
48
- const key = ctx.from ? ctx.from.id : (ctx.chatId || '?');
55
+ const key = ctx.from && ctx.from.id !== undefined ? `u:${ctx.from.id}` : `c:${ctx.chatId ?? '?'}`;
49
56
  const now = Date.now();
50
57
  const arr = bucket.get(key) || [];
51
58
  const alive = arr.filter((t) => now - t < windowMs);
@@ -54,6 +61,12 @@ function limiter({ windowMs = 2000, limit = 3, onExceeded } = {}) {
54
61
  return; // diamkan spam
55
62
  }
56
63
  alive.push(now); bucket.set(key, alive);
64
+ // Bersihkan bucket user/chat tidak aktif agar Map tidak tumbuh tanpa batas.
65
+ if (++checks % 256 === 0) {
66
+ for (const [k, times] of bucket) {
67
+ if (!times.some((t) => now - t < windowMs)) bucket.delete(k);
68
+ }
69
+ }
57
70
  return next();
58
71
  };
59
72
  }
package/lib/rich.js ADDED
@@ -0,0 +1,170 @@
1
+ // lib/rich.js — builders for Telegram Bot API 10.3 Rich Messages.
2
+ 'use strict';
3
+
4
+ const { File } = require('./file');
5
+ const CONTENT_FIELDS = ['html', 'markdown', 'blocks'];
6
+ const INPUT_BLOCK_TYPES = new Set([
7
+ 'paragraph', 'heading', 'pre', 'footer', 'divider', 'mathematical_expression', 'anchor',
8
+ 'list', 'blockquote', 'expandable_blockquote', 'pullquote', 'collage', 'slideshow',
9
+ 'table', 'details', 'map', 'animation', 'audio', 'document', 'photo', 'video',
10
+ 'voice_note', 'buttons', 'thinking',
11
+ ]);
12
+
13
+ function inputRichMessage(content, options = {}) {
14
+ const fields = CONTENT_FIELDS.filter((key) => Object.hasOwn(content, key));
15
+ if (fields.length !== 1) throw new TypeError('InputRichMessage harus memiliki tepat satu dari html, markdown, atau blocks');
16
+ if (fields[0] === 'blocks' && !Array.isArray(content.blocks)) throw new TypeError('InputRichMessage.blocks harus berupa array');
17
+ for (const key of CONTENT_FIELDS) {
18
+ if (Object.hasOwn(options, key)) throw new TypeError(`Gunakan rich.${key}() untuk menetapkan konten, jangan lewat options`);
19
+ }
20
+ return { ...content, ...options };
21
+ }
22
+
23
+ function assertDraftHasNoFileUpload(value) {
24
+ if (value instanceof File) throw new TypeError('Rich message draft tidak mendukung upload File baru; gunakan file_id Telegram bila perlu');
25
+ if (!value || typeof value !== 'object') return;
26
+ for (const child of Array.isArray(value) ? value : Object.values(value)) assertDraftHasNoFileUpload(child);
27
+ }
28
+
29
+ function draftRichMessage(content, options = {}) {
30
+ const result = inputRichMessage(content, options);
31
+ assertDraftHasNoFileUpload(result);
32
+ return result;
33
+ }
34
+
35
+ function block(type, props = {}) {
36
+ if (!INPUT_BLOCK_TYPES.has(type)) throw new TypeError(`Jenis InputRichBlock tidak dikenal: ${type}`);
37
+ const { type: _ignored, ...rest } = props;
38
+ return { ...rest, type };
39
+ }
40
+
41
+ const textEntity = (type, text, extra = {}) => ({ type, text, ...extra });
42
+ const caption = (text, credit) => ({ text, ...(credit !== undefined ? { credit } : {}) });
43
+
44
+ const rich = {
45
+ html: (html, options = {}) => inputRichMessage({ html: String(html) }, options),
46
+ markdown: (markdown, options = {}) => inputRichMessage({ markdown: String(markdown) }, options),
47
+ blocks: (blocks, options = {}) => inputRichMessage({ blocks: [...blocks] }, options),
48
+ draftHtml: (html, options = {}) => draftRichMessage({ html: String(html) }, options),
49
+ draftMarkdown: (markdown, options = {}) => draftRichMessage({ markdown: String(markdown) }, options),
50
+ draftBlocks: (blocks, options = {}) => draftRichMessage({ blocks: [...blocks] }, options),
51
+ block,
52
+
53
+ // RichText inline entities.
54
+ bold: (text) => textEntity('bold', text),
55
+ italic: (text) => textEntity('italic', text),
56
+ underline: (text) => textEntity('underline', text),
57
+ strikethrough: (text) => textEntity('strikethrough', text),
58
+ spoiler: (text) => textEntity('spoiler', text),
59
+ subscript: (text) => textEntity('subscript', text),
60
+ superscript: (text) => textEntity('superscript', text),
61
+ marked: (text) => textEntity('marked', text),
62
+ code: (text) => textEntity('code', text),
63
+ dateTime: (text, unixTime, format = 'r') => ({ type: 'date_time', text, unix_time: unixTime, date_time_format: format }),
64
+ textMention: (text, user) => ({ type: 'text_mention', text, user }),
65
+ customEmoji: (customEmojiId, alternativeText) => ({ type: 'custom_emoji', custom_emoji_id: String(customEmojiId), alternative_text: alternativeText }),
66
+ mathText: (expression) => ({ type: 'mathematical_expression', expression }),
67
+ url: (text, url) => textEntity('url', text, { url }),
68
+ email: (text, emailAddress) => textEntity('email_address', text, { email_address: emailAddress }),
69
+ phone: (text, phoneNumber) => textEntity('phone_number', text, { phone_number: phoneNumber }),
70
+ bankCard: (text, bankCardNumber) => textEntity('bank_card_number', text, { bank_card_number: bankCardNumber }),
71
+ mention: (text, username) => textEntity('mention', text, { username }),
72
+ hashtag: (text, hashtag) => textEntity('hashtag', text, { hashtag }),
73
+ cashtag: (text, cashtag) => textEntity('cashtag', text, { cashtag }),
74
+ botCommand: (text, botCommand) => textEntity('bot_command', text, { bot_command: botCommand }),
75
+ anchorText: (name) => ({ type: 'anchor', name }),
76
+ anchorLink: (text, anchorName) => textEntity('anchor_link', text, { anchor_name: anchorName }),
77
+ reference: (text, name) => textEntity('reference', text, { name }),
78
+ referenceLink: (text, referenceName) => textEntity('reference_link', text, { reference_name: referenceName }),
79
+ button: (text, action, style) => {
80
+ if (!action || typeof action !== 'object') throw new TypeError('rich.button() membutuhkan satu jenis aksi button');
81
+ const keys = ['url', 'callback_data', 'web_app', 'login_url', 'switch_inline_query', 'switch_inline_query_current_chat', 'switch_inline_query_chosen_chat', 'copy_text', 'disabled'];
82
+ const defined = keys.filter((key) => Object.hasOwn(action, key));
83
+ if (defined.length !== 1) throw new TypeError('RichMessageButton harus memiliki tepat satu field aksi');
84
+ if (style !== undefined && !['danger', 'success', 'primary', 'link'].includes(style)) {
85
+ throw new TypeError('Style rich button harus danger, success, primary, atau link');
86
+ }
87
+ if (style === 'link' && defined[0] !== 'callback_data') {
88
+ throw new TypeError('Style link hanya didukung pada rich callback button');
89
+ }
90
+ return { type: 'button', button: { text, ...action, ...(style ? { style } : {}) } };
91
+ },
92
+
93
+ // InputRichBlock builders.
94
+ paragraph: (text) => block('paragraph', { text }),
95
+ heading: (text, size = 2) => {
96
+ if (!Number.isInteger(size) || size < 1 || size > 6) throw new RangeError('rich.heading(): size harus 1–6');
97
+ return block('heading', { text, size });
98
+ },
99
+ pre: (text, language) => block('pre', { text, ...(language ? { language } : {}) }),
100
+ footer: (text) => block('footer', { text }),
101
+ divider: () => block('divider'),
102
+ mathBlock: (expression) => block('mathematical_expression', { expression }),
103
+ anchor: (name) => block('anchor', { name }),
104
+ list: (items) => block('list', { items: items.map((item) => {
105
+ if (Array.isArray(item)) return { blocks: item };
106
+ if (typeof item === 'string' || typeof item === 'number') return { blocks: [block('paragraph', { text: String(item) })] };
107
+ return item;
108
+ }) }),
109
+ quote: (blocks, credit) => block('blockquote', { blocks, ...(credit !== undefined ? { credit } : {}) }),
110
+ expandableQuote: (text, credit) => block('expandable_blockquote', { text, ...(credit !== undefined ? { credit } : {}) }),
111
+ pullQuote: (text, credit) => block('pullquote', { text, ...(credit !== undefined ? { credit } : {}) }),
112
+ collage: (blocks, captionText, credit) => block('collage', { blocks, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
113
+ slideshow: (blocks, captionText, credit) => block('slideshow', { blocks, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
114
+ table: (cells, options = {}) => block('table', {
115
+ cells,
116
+ ...(options.bordered ? { is_bordered: true } : {}),
117
+ ...(options.striped ? { is_striped: true } : {}),
118
+ ...(options.compact ? { is_compact: true } : {}),
119
+ ...(options.caption !== undefined ? { caption: options.caption } : {}),
120
+ }),
121
+ details: (summary, blocks, open = false) => block('details', { summary, blocks, ...(open ? { is_open: true } : {}) }),
122
+ map: (location, zoom, width, height, captionText, credit) => block('map', {
123
+ location, zoom, width, height,
124
+ ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}),
125
+ }),
126
+ animation: (animation, captionText, credit) => block('animation', { animation, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
127
+ audio: (audio, captionText, credit) => block('audio', { audio, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
128
+ document: (document, captionText, credit) => block('document', { document, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
129
+ photo: (photo, captionText, credit) => block('photo', { photo, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
130
+ video: (video, captionText, credit) => block('video', { video, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
131
+ voiceNote: (voiceNote, captionText, credit) => block('voice_note', { voice_note: voiceNote, ...(captionText !== undefined ? { caption: caption(captionText, credit) } : {}) }),
132
+ buttons: (buttons, align) => {
133
+ if (align !== undefined && !['left', 'center', 'right'].includes(align)) {
134
+ throw new TypeError('rich.buttons(): align harus left, center, atau right');
135
+ }
136
+ return block('buttons', { buttons, ...(align ? { align } : {}) });
137
+ },
138
+ thinking: (text = '') => block('thinking', { text }),
139
+ };
140
+
141
+ class RichMessageBuilder {
142
+ constructor() { this._content = null; this._options = {}; }
143
+ _set(field, value) {
144
+ if (this._content) throw new Error('RichMessageBuilder: tentukan satu mode saja (html, markdown, atau blocks)');
145
+ this._content = { [field]: value };
146
+ return this;
147
+ }
148
+ html(value) { return this._set('html', String(value)); }
149
+ markdown(value) { return this._set('markdown', String(value)); }
150
+ blocks(value = []) { return this._set('blocks', [...value]); }
151
+ add(...items) {
152
+ if (!this._content) this._content = { blocks: [] };
153
+ if (!Array.isArray(this._content.blocks)) throw new Error('RichMessageBuilder.add() hanya bisa dipakai pada mode blocks');
154
+ this._content.blocks.push(...items);
155
+ return this;
156
+ }
157
+ media(items) { this._options.media = [...items]; return this; }
158
+ rtl(value = true) { this._options.is_rtl = Boolean(value); return this; }
159
+ skipEntityDetection(value = true) { this._options.skip_entity_detection = Boolean(value); return this; }
160
+ build() {
161
+ if (!this._content) throw new Error('RichMessageBuilder: konten belum diisi');
162
+ return inputRichMessage(this._content, this._options);
163
+ }
164
+ buildDraft() {
165
+ if (!this._content) throw new Error('RichMessageBuilder: konten belum diisi');
166
+ return draftRichMessage(this._content, this._options);
167
+ }
168
+ }
169
+
170
+ module.exports = { rich, RichMessageBuilder, inputRichMessage };
package/lib/runner.js CHANGED
@@ -16,7 +16,7 @@ async function pollLoop({ api, handleUpdate, allowedUpdates, dropPending, onConf
16
16
  if (dropPending) {
17
17
  try {
18
18
  const u = await api.callApi('getUpdates', { offset: -1, timeout: 0, allowed_updates: allowedUpdates });
19
- if (u.length) offset = u[u.length - 1].update_id + 1;
19
+ if (Array.isArray(u) && u.length) offset = u[u.length - 1].update_id + 1;
20
20
  } catch { /* abaikan — loop utama yang menangani */ }
21
21
  }
22
22
 
@@ -28,10 +28,19 @@ async function pollLoop({ api, handleUpdate, allowedUpdates, dropPending, onConf
28
28
  } catch (err) {
29
29
  const code = err && (err.error_code || err.code);
30
30
  if (shouldStop()) break;
31
- if (code === 409) { if (onConflict) onConflict(err); await sleep(conflictDelay); continue; }
32
- const fatal = code && code !== 429;
33
- if (fatal) { if (onFatal) onFatal(err); break; }
34
- await sleep(1000); continue; // koneksi putus / 429 → napas sebentar
31
+ if (code === 409) { if (onConflict) await onConflict(err); await sleep(conflictDelay); continue; }
32
+ if (code === 429) {
33
+ const retryAfter = Number(err && err.parameters && err.parameters.retry_after);
34
+ await sleep(Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1000 : 1000);
35
+ continue;
36
+ }
37
+ if (Number.isInteger(code) && code >= 500 && code < 600) { await sleep(1000); continue; }
38
+ const networkCode = err && ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EPIPE', 'ENETUNREACH', 'ECONNREFUSED'].includes(err.code);
39
+ if (!err || err.network || networkCode || !err.error_code) {
40
+ await sleep(1000); continue; // jaringan putus/time-out: retry, bukan fatal
41
+ }
42
+ if (onFatal) await onFatal(err);
43
+ break;
35
44
  }
36
45
 
37
46
  for (const u of updates) {
package/lib/session.js CHANGED
@@ -24,12 +24,20 @@ function session(opts = {}) {
24
24
  const key = getKey(ctx);
25
25
  if (key === undefined) { ctx.session = {}; return next(); }
26
26
  let s = await read(key);
27
- if (s === undefined || s === null) { s = initial(); await write(key, s); }
28
- // Proxy: properti __deleted di akhir → hapus dari store
27
+ if (s === undefined || s === null) s = initial();
28
+ if (!s || typeof s !== 'object') throw new TypeError('session: initial/storage harus mengembalikan object');
29
+ // Proxy: simpan ulang setelah middleware agar storage hasil deserialisasi juga awet.
29
30
  ctx.session = new Proxy(s, {
30
- get: (t, p) => t[p], set: (t, p, v) => { t[p] = v; return true; },
31
+ get: (t, p) => t[p],
32
+ set: (t, p, v) => { t[p] = v; return true; },
33
+ deleteProperty: (t, p) => { delete t[p]; return true; },
31
34
  });
32
- return next();
35
+ try {
36
+ return await next();
37
+ } finally {
38
+ if (s.__deleted === true) await del(key);
39
+ else await write(key, s);
40
+ }
33
41
  };
34
42
  }
35
43