@xbibzlibrary/telebibz 3.1.1 → 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 -3
  11. package/lib/context.js +85 -25
  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
@@ -2,19 +2,24 @@
2
2
  'use strict';
3
3
 
4
4
  const { File } = require('./file');
5
+ const axios = require('axios');
6
+ const fs = require('fs');
5
7
 
6
8
  /** Objek "pesan" dari berbagai field update (gaya ctx.msg). */
7
9
  function msgOf(update) {
8
10
  return update.message || update.edited_message || update.channel_post ||
9
- 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;
10
13
  }
11
14
 
12
15
  const UPDATE_KEYS = [
13
16
  'message', 'edited_message', 'channel_post', 'edited_channel_post',
14
- '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',
15
19
  'chosen_inline_result', 'chat_join_request', 'chat_member', 'my_chat_member',
16
20
  'message_reaction', 'message_reaction_count', 'business_connection',
17
21
  'poll', 'poll_answer', 'shipping_query', 'pre_checkout_query', 'purchased_paid_media',
22
+ 'chat_boost', 'removed_chat_boost', 'stopped_message_generation',
18
23
  ];
19
24
 
20
25
  class Context {
@@ -32,10 +37,13 @@ class Context {
32
37
  if (this.msg) return this.msg.chat;
33
38
  if (this.callback_query) return this.callback_query.message && this.callback_query.message.chat;
34
39
  if (this.inline_query || this.chosen_inline_result) return undefined;
35
- if (this.business_connection) return this.business_connection.user;
36
40
  if (this.chat_join_request) return this.chat_join_request.chat;
37
41
  if (this.chat_member) return this.chat_member.chat;
38
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;
39
47
  return undefined;
40
48
  }
41
49
  get from() {
@@ -48,9 +56,11 @@ class Context {
48
56
  get chatId() { const c = this.chat; return c ? c.id : undefined; }
49
57
  get msgId() { return this.msg ? this.msg.message_id : undefined; }
50
58
  get businessConnectionId() {
59
+ const msg = this.msg || (this.callback_query && this.callback_query.message);
51
60
  return (this.business_connection && this.business_connection.id) ||
52
- (this.msg && this.msg.business_connection_id) || undefined;
61
+ (msg && msg.business_connection_id) || undefined;
53
62
  }
63
+ get guestQueryId() { return this.msg && this.msg.guest_query_id; }
54
64
  get senderChat() { return this.msg ? this.msg.sender_chat : undefined; }
55
65
  get inlineMessageId() { return this.callback_query ? this.callback_query.inline_message_id : undefined; }
56
66
 
@@ -61,13 +71,33 @@ class Context {
61
71
  return c;
62
72
  }
63
73
  _biz(extra = {}) {
64
- // 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);
65
76
  const bc = this.businessConnectionId;
66
- 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;
67
78
  }
68
79
 
69
80
  /* ---------- kirim balasan ---------- */
70
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
+ }
71
101
  replyWithHTML(text, extra = {}) { return this.reply(text, { parse_mode: 'HTML', ...extra }); }
72
102
  replyWithMarkdown(text, extra = {}) { return this.reply(text, { parse_mode: 'MarkdownV2', ...extra }); }
73
103
  replyWithPhoto(p, e = {}) { return this.api.sendPhoto(this._cid(), p, this._biz(e)); }
@@ -75,19 +105,19 @@ class Context {
75
105
  replyWithAudio(a, e = {}) { return this.api.sendAudio(this._cid(), a, this._biz(e)); }
76
106
  replyWithDocument(d, e = {}) { return this.api.sendDocument(this._cid(), d, this._biz(e)); }
77
107
  replyWithAnimation(a, e = {}) { return this.api.sendAnimation(this._cid(), a, this._biz(e)); }
78
- replyWithVoice(v, e = {}) { return this.api.sendVoice(this._cid(), v, { caption: e.caption, ...e }); }
79
- replyWithVideoNote(v, e = {}) { return this.api.sendVideoNote(this._cid(), v, e); }
80
- replyWithSticker(s, e = {}) { return this.api.sendSticker(this._cid(), s, e); }
81
- replyWithMediaGroup(media, e = {}) { return this.api.sendMediaGroup(this._cid(), media, e); }
82
- replyWithLocation(lat, lon, e = {}) { return this.api.sendLocation(this._cid(), lat, lon, e); }
83
- replyWithVenue(lat, lon, title, address, e = {}) { return this.api.sendVenue(this._cid(), lat, lon, title, address, e); }
84
- replyWithContact(phone, first, e = {}) { return this.api.sendContact(this._cid(), phone, first, e); }
85
- replyWithPoll(question, options, e = {}) { return this.api.sendPoll(this._cid(), question, options, e); }
86
- 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)); }
87
117
  replyWithInvoice(title, description, payload, currency, prices, e = {}) {
88
- 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));
89
119
  }
90
- 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)); }
91
121
 
92
122
  /* ---------- teruskan & salin ---------- */
93
123
  forwardMessage(to, from, messageId, e = {}) {
@@ -103,22 +133,50 @@ class Context {
103
133
  editMessageText(text, e = {}) {
104
134
  if (this.inlineMessageId) return this.api.callApi('editMessageText', { inline_message_id: this.inlineMessageId, text, ...e });
105
135
  const m = this.callback_query && this.callback_query.message;
106
- 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
+ });
107
147
  }
108
148
  editMessageCaption(e = {}) {
109
149
  const m = this.callback_query && this.callback_query.message;
110
150
  if (this.inlineMessageId) return this.api.callApi('editMessageCaption', { inline_message_id: this.inlineMessageId, ...e });
111
- 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));
112
152
  }
113
153
  editMessageMedia(media, e = {}) {
114
154
  const m = this.callback_query && this.callback_query.message;
115
155
  if (this.inlineMessageId) return this.api.callApi('editMessageMedia', { inline_message_id: this.inlineMessageId, media, ...e });
116
- 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));
117
157
  }
118
158
  editMessageReplyMarkup(e = {}) {
119
159
  const m = this.callback_query && this.callback_query.message;
120
160
  if (this.inlineMessageId) return this.api.callApi('editMessageReplyMarkup', { inline_message_id: this.inlineMessageId, ...e });
121
- 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 });
122
180
  }
123
181
  deleteMessage(chatId, messageId) {
124
182
  if (messageId !== undefined) return this.api.deleteMessage(chatId, messageId);
@@ -127,8 +185,9 @@ class Context {
127
185
  }
128
186
  deleteMessages(ids) { return this.api.deleteMessages(this._cid(), ids); }
129
187
  react(emoji = '👍', e = {}) {
188
+ const m = this.callback_query && this.callback_query.message;
130
189
  const reaction = [{ type: 'emoji', emoji }];
131
- 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);
132
191
  }
133
192
 
134
193
  /* ---------- callback & inline ---------- */
@@ -141,6 +200,10 @@ class Context {
141
200
  if (!this.inline_query) return Promise.resolve(false);
142
201
  return this.api.answerInlineQuery(this.inline_query.id, results, e);
143
202
  }
203
+ answerGuestQuery(result) {
204
+ if (!this.guestQueryId) return Promise.resolve(false);
205
+ return this.api.answerGuestQuery({ guest_query_id: this.guestQueryId, result });
206
+ }
144
207
 
145
208
  /* ---------- admin & grup ---------- */
146
209
  banChatMember(cid, uid, e = {}) { return this.api.banChatMember(cid, uid, e); }
@@ -173,9 +236,6 @@ class Context {
173
236
  }
174
237
  async downloadFile(dest) {
175
238
  const f = await this.getFile();
176
- require('fs');
177
- const axios = require('axios');
178
- const fs = require('fs');
179
239
  const url = `https://api.telegram.org/file/bot${this.api.token}/${f.file_path}`;
180
240
  const res = await axios.get(url, { responseType: 'stream' });
181
241
  const out = fs.createWriteStream(dest);
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