@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +1 -1
- package/NOTICE.md +15 -8
- package/README.id.md +85 -16
- package/README.md +84 -16
- package/examples/08-rich-message.js +34 -0
- package/index.d.ts +300 -96
- package/index.js +8 -2
- package/lib/api.js +20 -6
- package/lib/composer.js +14 -3
- package/lib/context.js +85 -25
- package/lib/file.js +8 -1
- package/lib/inline-query.js +6 -1
- package/lib/logger.js +44 -14
- package/lib/ratelimit.js +16 -3
- package/lib/rich.js +170 -0
- package/lib/runner.js +14 -5
- package/lib/session.js +12 -4
- package/lib/telebibz.js +40 -24
- package/lib/telegram-methods.js +192 -0
- package/package.json +14 -10
- package/test/audit.test.js +320 -0
- package/test/types.test.ts +40 -0
- package/types/telegram-bot-api/LICENSE +21 -0
- package/types/telegram-bot-api/api.d.ts +22 -0
- package/types/telegram-bot-api/checklist.d.ts +72 -0
- package/types/telegram-bot-api/inline.d.ts +692 -0
- package/types/telegram-bot-api/langs.d.ts +193 -0
- package/types/telegram-bot-api/manage.d.ts +1153 -0
- package/types/telegram-bot-api/markup.d.ts +278 -0
- package/types/telegram-bot-api/message.d.ts +1557 -0
- package/types/telegram-bot-api/methods.d.ts +2860 -0
- package/types/telegram-bot-api/mod.d.ts +14 -0
- package/types/telegram-bot-api/passport.d.ts +163 -0
- package/types/telegram-bot-api/payment.d.ts +576 -0
- package/types/telegram-bot-api/rich.d.ts +1198 -0
- package/types/telegram-bot-api/settings.d.ts +120 -0
- package/types/telegram-bot-api/story.d.ts +89 -0
- 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 ||
|
|
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', '
|
|
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
|
-
(
|
|
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
|
-
//
|
|
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 &&
|
|
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,
|
|
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
|
-
|
|
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 };
|
package/lib/inline-query.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
1
|
+
// lib/logger.js — log terminal rapi, berwarna, dan mudah dibaca.
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const C = {
|
|
5
|
-
reset: '
|
|
6
|
-
red: '
|
|
7
|
-
magenta: '
|
|
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
|
-
|
|
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
|
|
27
|
-
log.banner = (title, rows) => {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
28
|
-
|
|
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],
|
|
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
|
-
|
|
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
|
|