telegix 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1534 -0
- package/index.d.ts +539 -0
- package/index.js +38 -0
- package/lib/album.js +57 -0
- package/lib/api.js +1840 -0
- package/lib/chataction.js +40 -0
- package/lib/cluster.js +68 -0
- package/lib/composer.js +419 -0
- package/lib/context.js +970 -0
- package/lib/errors.js +67 -0
- package/lib/format.js +115 -0
- package/lib/i18n.js +158 -0
- package/lib/inline-debounce.js +49 -0
- package/lib/inline.js +79 -0
- package/lib/markdownv2.js +29 -0
- package/lib/markup.js +321 -0
- package/lib/payment.js +91 -0
- package/lib/polling.js +101 -0
- package/lib/prompt.js +62 -0
- package/lib/ratelimit.js +59 -0
- package/lib/rich.js +609 -0
- package/lib/scenes.js +206 -0
- package/lib/serialize.js +141 -0
- package/lib/session.js +145 -0
- package/lib/telegix.js +176 -0
- package/lib/webapp.js +65 -0
- package/lib/webhook.js +86 -0
- package/package.json +42 -0
package/lib/markup.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Keyboard & Markup Builder
|
|
3
|
+
* @module telegix/markup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class KeyboardBuilder {
|
|
7
|
+
constructor(buttons = []) {
|
|
8
|
+
this.keyboard = Array.isArray(buttons) ? buttons : [];
|
|
9
|
+
this.is_persistent = false;
|
|
10
|
+
this.resize_keyboard = true;
|
|
11
|
+
this.one_time_keyboard = false;
|
|
12
|
+
this.input_field_placeholder = undefined;
|
|
13
|
+
this.selective = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns reply_markup object directly
|
|
18
|
+
*/
|
|
19
|
+
get reply_markup() {
|
|
20
|
+
return this.toJSON();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resizes keyboard vertically for optimal fit
|
|
25
|
+
* @param {boolean} [resize=true]
|
|
26
|
+
* @returns {this}
|
|
27
|
+
*/
|
|
28
|
+
resize(resize = true) {
|
|
29
|
+
this.resize_keyboard = Boolean(resize);
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Requests clients to always show the keyboard when the regular keyboard is hidden
|
|
35
|
+
* @param {boolean} [persistent=true]
|
|
36
|
+
* @returns {this}
|
|
37
|
+
*/
|
|
38
|
+
persistent(persistent = true) {
|
|
39
|
+
this.is_persistent = Boolean(persistent);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Requests clients to hide the keyboard as soon as it's been used
|
|
45
|
+
* @param {boolean} [oneTime=true]
|
|
46
|
+
* @returns {this}
|
|
47
|
+
*/
|
|
48
|
+
oneTime(oneTime = true) {
|
|
49
|
+
this.one_time_keyboard = Boolean(oneTime);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The placeholder to be shown in the input field when the keyboard is active
|
|
55
|
+
* @param {string} placeholder
|
|
56
|
+
* @returns {this}
|
|
57
|
+
*/
|
|
58
|
+
placeholder(placeholder) {
|
|
59
|
+
this.input_field_placeholder = placeholder;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Use this parameter if you want to show the keyboard to specific users only
|
|
65
|
+
* @param {boolean} [selective=true]
|
|
66
|
+
* @returns {this}
|
|
67
|
+
*/
|
|
68
|
+
selectiveTarget(selective = true) {
|
|
69
|
+
this.selective = Boolean(selective);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns standard Telegram ReplyKeyboardMarkup object
|
|
75
|
+
* @returns {object}
|
|
76
|
+
*/
|
|
77
|
+
toJSON() {
|
|
78
|
+
return {
|
|
79
|
+
keyboard: this.keyboard,
|
|
80
|
+
is_persistent: this.is_persistent,
|
|
81
|
+
resize_keyboard: this.resize_keyboard,
|
|
82
|
+
one_time_keyboard: this.one_time_keyboard,
|
|
83
|
+
input_field_placeholder: this.input_field_placeholder,
|
|
84
|
+
selective: this.selective,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Markup {
|
|
90
|
+
/**
|
|
91
|
+
* Create custom reply keyboard markup
|
|
92
|
+
* @param {Array<Array<object|string>|object|string>} buttons
|
|
93
|
+
* @param {object} [options]
|
|
94
|
+
* @returns {KeyboardBuilder}
|
|
95
|
+
*/
|
|
96
|
+
static keyboard(buttons = [], options = {}) {
|
|
97
|
+
const formatted = Array.isArray(buttons)
|
|
98
|
+
? buttons.map((row) => {
|
|
99
|
+
const rowArr = Array.isArray(row) ? row : [row];
|
|
100
|
+
return rowArr.map((btn) => (typeof btn === 'string' ? { text: btn } : btn));
|
|
101
|
+
})
|
|
102
|
+
: [];
|
|
103
|
+
const builder = new KeyboardBuilder(formatted);
|
|
104
|
+
if (options.resize !== undefined) builder.resize(options.resize);
|
|
105
|
+
if (options.oneTime !== undefined) builder.oneTime(options.oneTime);
|
|
106
|
+
if (options.persistent !== undefined) builder.persistent(options.persistent);
|
|
107
|
+
if (options.placeholder !== undefined) builder.placeholder(options.placeholder);
|
|
108
|
+
if (options.selective !== undefined) builder.selectiveTarget(options.selective);
|
|
109
|
+
return builder;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create inline keyboard markup
|
|
114
|
+
* @param {Array<Array<object>|object>} buttons
|
|
115
|
+
* @returns {{ inline_keyboard: Array<Array<object>>, reply_markup: { inline_keyboard: Array<Array<object>> } }}
|
|
116
|
+
*/
|
|
117
|
+
static inlineKeyboard(buttons = []) {
|
|
118
|
+
const inline_keyboard = Array.isArray(buttons)
|
|
119
|
+
? buttons.map((row) => {
|
|
120
|
+
const rowArr = Array.isArray(row) ? row : [row];
|
|
121
|
+
return rowArr.map((btn) => (typeof btn === 'string' ? { text: btn, callback_data: btn } : btn));
|
|
122
|
+
})
|
|
123
|
+
: [];
|
|
124
|
+
return {
|
|
125
|
+
inline_keyboard,
|
|
126
|
+
reply_markup: { inline_keyboard },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Requests clients to remove the custom keyboard
|
|
132
|
+
* @param {boolean} [selective=false]
|
|
133
|
+
* @returns {{ remove_keyboard: true, selective: boolean, reply_markup: { remove_keyboard: true, selective: boolean } }}
|
|
134
|
+
*/
|
|
135
|
+
static removeKeyboard(selective = false) {
|
|
136
|
+
const res = {
|
|
137
|
+
remove_keyboard: true,
|
|
138
|
+
selective: Boolean(selective),
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
...res,
|
|
142
|
+
reply_markup: res,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Displays a reply interface to the user
|
|
148
|
+
* @param {boolean} [selective=false]
|
|
149
|
+
* @param {string} [placeholder]
|
|
150
|
+
* @returns {{ force_reply: true, selective: boolean, input_field_placeholder?: string, reply_markup: object }}
|
|
151
|
+
*/
|
|
152
|
+
static forceReply(selective = false, placeholder = undefined) {
|
|
153
|
+
const res = {
|
|
154
|
+
force_reply: true,
|
|
155
|
+
selective: Boolean(selective),
|
|
156
|
+
};
|
|
157
|
+
if (placeholder) {
|
|
158
|
+
res.input_field_placeholder = placeholder;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...res,
|
|
162
|
+
reply_markup: res,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Button builders
|
|
168
|
+
*/
|
|
169
|
+
static button = {
|
|
170
|
+
/**
|
|
171
|
+
* Standard text button for reply keyboard
|
|
172
|
+
* @param {string} text
|
|
173
|
+
*/
|
|
174
|
+
text: (text) => ({ text }),
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Inline callback button
|
|
178
|
+
* @param {string} text
|
|
179
|
+
* @param {string} data
|
|
180
|
+
*/
|
|
181
|
+
callback: (text, data) => ({ text, callback_data: String(data) }),
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Inline URL button
|
|
185
|
+
* @param {string} text
|
|
186
|
+
* @param {string} url
|
|
187
|
+
*/
|
|
188
|
+
url: (text, url) => ({ text, url }),
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Web App button
|
|
192
|
+
* @param {string} text
|
|
193
|
+
* @param {string} url
|
|
194
|
+
*/
|
|
195
|
+
webApp: (text, url) => ({ text, web_app: { url } }),
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Request user contact (reply keyboard only)
|
|
199
|
+
* @param {string} text
|
|
200
|
+
*/
|
|
201
|
+
contactRequest: (text) => ({ text, request_contact: true }),
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Request user location (reply keyboard only)
|
|
205
|
+
* @param {string} text
|
|
206
|
+
*/
|
|
207
|
+
locationRequest: (text) => ({ text, request_location: true }),
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Request user poll (reply keyboard only)
|
|
211
|
+
* @param {string} text
|
|
212
|
+
* @param {'quiz'|'regular'|string} [type]
|
|
213
|
+
*/
|
|
214
|
+
pollRequest: (text, type) => ({
|
|
215
|
+
text,
|
|
216
|
+
request_poll: type ? { type } : {},
|
|
217
|
+
}),
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Switch to inline query button
|
|
221
|
+
* @param {string} text
|
|
222
|
+
* @param {string} [query='']
|
|
223
|
+
*/
|
|
224
|
+
switchToChat: (text, query = '') => ({
|
|
225
|
+
text,
|
|
226
|
+
switch_inline_query: query,
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Switch to inline query in current chat button
|
|
231
|
+
* @param {string} text
|
|
232
|
+
* @param {string} [query='']
|
|
233
|
+
*/
|
|
234
|
+
switchToCurrentChat: (text, query = '') => ({
|
|
235
|
+
text,
|
|
236
|
+
switch_inline_query_current_chat: query,
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Login URL button
|
|
241
|
+
* @param {string} text
|
|
242
|
+
* @param {string} url
|
|
243
|
+
* @param {object} [options]
|
|
244
|
+
*/
|
|
245
|
+
login: (text, url, options = {}) => ({
|
|
246
|
+
text,
|
|
247
|
+
login_url: { url, ...options },
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Pay button (must be the very first button in the first row of an invoice inline keyboard)
|
|
252
|
+
* @param {string} [text='Pay']
|
|
253
|
+
*/
|
|
254
|
+
pay: (text = 'Pay') => ({
|
|
255
|
+
text,
|
|
256
|
+
pay: true,
|
|
257
|
+
}),
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Copy text button (copies copyText directly to clipboard on click)
|
|
261
|
+
* @param {string} text
|
|
262
|
+
* @param {string} copyText
|
|
263
|
+
*/
|
|
264
|
+
copyText: (text, copyText) => ({
|
|
265
|
+
text,
|
|
266
|
+
copy_text: { text: String(copyText) },
|
|
267
|
+
}),
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Request users button (reply keyboard only)
|
|
271
|
+
* @param {string} text
|
|
272
|
+
* @param {number} requestId
|
|
273
|
+
* @param {object} [options]
|
|
274
|
+
*/
|
|
275
|
+
requestUsers: (text, requestId, options = {}) => ({
|
|
276
|
+
text,
|
|
277
|
+
request_users: { request_id: requestId, ...options },
|
|
278
|
+
}),
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Request chat button (reply keyboard only)
|
|
282
|
+
* @param {string} text
|
|
283
|
+
* @param {number} requestId
|
|
284
|
+
* @param {boolean} [chatIsChannel=false]
|
|
285
|
+
* @param {object} [options]
|
|
286
|
+
*/
|
|
287
|
+
requestChat: (text, requestId, chatIsChannel = false, options = {}) => ({
|
|
288
|
+
text,
|
|
289
|
+
request_chat: { request_id: requestId, chat_is_channel: Boolean(chatIsChannel), ...options },
|
|
290
|
+
}),
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Switch to inline query chosen chat button
|
|
294
|
+
* @param {string} text
|
|
295
|
+
* @param {string} [query='']
|
|
296
|
+
* @param {object} [options]
|
|
297
|
+
*/
|
|
298
|
+
switchInlineQueryChosenChat: (text, query = '', options = {}) => ({
|
|
299
|
+
text,
|
|
300
|
+
switch_inline_query_chosen_chat: { query, ...options },
|
|
301
|
+
}),
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Play Game button
|
|
305
|
+
* @param {string} [text='Play Game']
|
|
306
|
+
*/
|
|
307
|
+
game: (text = 'Play Game') => ({
|
|
308
|
+
text,
|
|
309
|
+
callback_game: {},
|
|
310
|
+
}),
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Disabled button (Bot API 10.3)
|
|
314
|
+
* @param {string} text
|
|
315
|
+
*/
|
|
316
|
+
disabled: (text) => ({
|
|
317
|
+
text,
|
|
318
|
+
disabled: true,
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
}
|
package/lib/payment.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Telegram Payment & Invoice Builder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class InvoiceBuilder {
|
|
6
|
+
constructor(title, description, payload, currency, prices) {
|
|
7
|
+
this.invoice = {
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
payload,
|
|
11
|
+
currency: currency || 'XTR', // Telegram Stars default or USD, RUB, etc.
|
|
12
|
+
prices: prices || [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
providerToken(token) {
|
|
17
|
+
if (token) this.invoice.provider_token = token;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addPrice(label, amount) {
|
|
22
|
+
this.invoice.prices.push({ label, amount });
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
maxTipAmount(amount) {
|
|
27
|
+
this.invoice.max_tip_amount = amount;
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
suggestedTipAmounts(amounts) {
|
|
32
|
+
this.invoice.suggested_tip_amounts = amounts;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
photo(url, width, height, size) {
|
|
37
|
+
if (url) this.invoice.photo_url = url;
|
|
38
|
+
if (width) this.invoice.photo_width = width;
|
|
39
|
+
if (height) this.invoice.photo_height = height;
|
|
40
|
+
if (size) this.invoice.photo_size = size;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
need(options = {}) {
|
|
45
|
+
if (options.name) this.invoice.need_name = true;
|
|
46
|
+
if (options.phoneNumber) this.invoice.need_phone_number = true;
|
|
47
|
+
if (options.email) this.invoice.need_email = true;
|
|
48
|
+
if (options.shippingAddress) this.invoice.need_shipping_address = true;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
send(ctx, chatId) {
|
|
53
|
+
const targetChatId = chatId || ctx.chat?.id || ctx.chatId;
|
|
54
|
+
if (!targetChatId) {
|
|
55
|
+
throw new Error('Target chat ID is required to send invoice');
|
|
56
|
+
}
|
|
57
|
+
const { title, description, payload, currency, prices, ...extra } = this.invoice;
|
|
58
|
+
return ctx.telegram.sendInvoice(
|
|
59
|
+
targetChatId,
|
|
60
|
+
title,
|
|
61
|
+
description,
|
|
62
|
+
payload,
|
|
63
|
+
currency,
|
|
64
|
+
prices,
|
|
65
|
+
extra
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
build() {
|
|
70
|
+
return { ...this.invoice };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validates shipping query response
|
|
76
|
+
*/
|
|
77
|
+
export function answerShippingQuery(ctx, ok, options = {}) {
|
|
78
|
+
const shippingQueryId = ctx.shippingQuery?.id || options.shippingQueryId || (typeof ctx === 'string' ? ctx : null);
|
|
79
|
+
if (!shippingQueryId) throw new Error('Shipping Query ID is missing');
|
|
80
|
+
return ctx.telegram.answerShippingQuery(shippingQueryId, ok, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates pre-checkout query response
|
|
85
|
+
*/
|
|
86
|
+
export function answerPreCheckoutQuery(ctx, ok, options = {}) {
|
|
87
|
+
const preCheckoutQueryId = ctx.preCheckoutQuery?.id || options.preCheckoutQueryId || (typeof ctx === 'string' ? ctx : null);
|
|
88
|
+
if (!preCheckoutQueryId) throw new Error('Pre-Checkout Query ID is missing');
|
|
89
|
+
const errorMsg = typeof options === 'string' ? options : options.errorMessage;
|
|
90
|
+
return ctx.telegram.answerPreCheckoutQuery(preCheckoutQueryId, ok, errorMsg);
|
|
91
|
+
}
|
package/lib/polling.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Long Polling Engine
|
|
3
|
+
* @module telegix/polling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PollingError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
export class Polling {
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('./api.js').Telegram} telegram
|
|
11
|
+
* @param {Function} updateHandler - (update: object) => Promise<void>
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
*/
|
|
14
|
+
constructor(telegram, updateHandler, options = {}) {
|
|
15
|
+
this.telegram = telegram;
|
|
16
|
+
this.updateHandler = updateHandler;
|
|
17
|
+
this.options = {
|
|
18
|
+
timeout: 30,
|
|
19
|
+
limit: 100,
|
|
20
|
+
allowedUpdates: undefined,
|
|
21
|
+
dropPendingUpdates: false,
|
|
22
|
+
retryInterval: 3000,
|
|
23
|
+
...options,
|
|
24
|
+
};
|
|
25
|
+
this.offset = 0;
|
|
26
|
+
this.isRunning = false;
|
|
27
|
+
this.abortController = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Start long polling loop
|
|
32
|
+
*/
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.isRunning) return;
|
|
35
|
+
this.isRunning = true;
|
|
36
|
+
|
|
37
|
+
// Delete webhook if exists or drop pending updates
|
|
38
|
+
try {
|
|
39
|
+
if (this.options.dropPendingUpdates) {
|
|
40
|
+
await this.telegram.deleteWebhook({ drop_pending_updates: true });
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore initial webhook check error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this._loop();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
async _loop() {
|
|
53
|
+
while (this.isRunning) {
|
|
54
|
+
this.abortController = new AbortController();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const updates = await this.telegram.getUpdates(
|
|
58
|
+
this.offset,
|
|
59
|
+
this.options.limit,
|
|
60
|
+
this.options.timeout,
|
|
61
|
+
this.options.allowedUpdates
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!this.isRunning) break;
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(updates) && updates.length > 0) {
|
|
67
|
+
for (const update of updates) {
|
|
68
|
+
this.offset = update.update_id + 1;
|
|
69
|
+
try {
|
|
70
|
+
await this.updateHandler(update);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (this.options.onError) {
|
|
73
|
+
this.options.onError(new PollingError(err));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (!this.isRunning) break;
|
|
80
|
+
|
|
81
|
+
if (this.options.onError) {
|
|
82
|
+
this.options.onError(new PollingError(err));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Retry delay on error / 429
|
|
86
|
+
const retryDelay = err.retryAfter ? err.retryAfter * 1000 : this.options.retryInterval;
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Stop polling gracefully
|
|
94
|
+
*/
|
|
95
|
+
async stop() {
|
|
96
|
+
this.isRunning = false;
|
|
97
|
+
if (this.abortController) {
|
|
98
|
+
this.abortController.abort();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Context Prompt Helper & Conversation Handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const activePrompts = new Map(); // key: `${chatId}:${userId}` -> callback(text)
|
|
6
|
+
|
|
7
|
+
export function promptMiddleware() {
|
|
8
|
+
return async (ctx, next) => {
|
|
9
|
+
const chatId = ctx.chat?.id || ctx.chatId;
|
|
10
|
+
const userId = ctx.from?.id || ctx.userId;
|
|
11
|
+
const text = ctx.message?.text || ctx.message?.caption || ctx.msg?.text || ctx.msg?.caption;
|
|
12
|
+
|
|
13
|
+
if (chatId && userId && (text !== undefined || ctx.message || ctx.msg)) {
|
|
14
|
+
const key = `${chatId}:${userId}`;
|
|
15
|
+
if (activePrompts.has(key)) {
|
|
16
|
+
const handler = activePrompts.get(key);
|
|
17
|
+
activePrompts.delete(key);
|
|
18
|
+
handler(text, ctx);
|
|
19
|
+
return; // Consume update for the prompt
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Attach prompt helper to context
|
|
24
|
+
ctx.prompt = async (textMessage, options = {}) => {
|
|
25
|
+
const targetChatId = ctx.chat?.id || ctx.chatId;
|
|
26
|
+
const targetUserId = ctx.from?.id || ctx.userId;
|
|
27
|
+
if (!targetChatId || !targetUserId) {
|
|
28
|
+
throw new Error('Cannot prompt without chat ID and user ID');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timeoutMs = options.timeoutMs || 60000; // default 1 minute
|
|
32
|
+
await ctx.reply(textMessage, options.extra);
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const key = `${targetChatId}:${targetUserId}`;
|
|
36
|
+
|
|
37
|
+
// If there's an existing prompt, cancel it
|
|
38
|
+
if (activePrompts.has(key)) {
|
|
39
|
+
activePrompts.delete(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
if (activePrompts.has(key)) {
|
|
44
|
+
activePrompts.delete(key);
|
|
45
|
+
reject(new Error('Prompt timed out'));
|
|
46
|
+
}
|
|
47
|
+
}, timeoutMs);
|
|
48
|
+
|
|
49
|
+
activePrompts.set(key, (responseText, promptCtx) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
if (options.returnContext) {
|
|
52
|
+
resolve({ text: responseText, ctx: promptCtx });
|
|
53
|
+
} else {
|
|
54
|
+
resolve(responseText);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return next();
|
|
61
|
+
};
|
|
62
|
+
}
|
package/lib/ratelimit.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Rate Limiter & Throttling Middleware
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.windowMs = options.windowMs || 3000; // 3 seconds window by default
|
|
8
|
+
this.limit = options.limit || 3; // Max 3 requests per window
|
|
9
|
+
this.keyFn = options.keyFn || ((ctx) => ctx.userId || ctx.chatId);
|
|
10
|
+
this.handler = options.handler || (async (ctx) => {
|
|
11
|
+
await ctx.reply('⚠️ Too many requests. Please slow down.');
|
|
12
|
+
});
|
|
13
|
+
this.storage = new Map();
|
|
14
|
+
|
|
15
|
+
const cleanup = setInterval(() => {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [key, data] of this.storage.entries()) {
|
|
18
|
+
if (now > data.resetTime) {
|
|
19
|
+
this.storage.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}, Math.max(this.windowMs, 10000));
|
|
23
|
+
if (cleanup.unref) cleanup.unref();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
middleware() {
|
|
27
|
+
return async (ctx, next) => {
|
|
28
|
+
const key = this.keyFn(ctx);
|
|
29
|
+
if (!key) return next();
|
|
30
|
+
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
let record = this.storage.get(key);
|
|
33
|
+
|
|
34
|
+
if (!record || now > record.resetTime) {
|
|
35
|
+
record = {
|
|
36
|
+
count: 1,
|
|
37
|
+
resetTime: now + this.windowMs,
|
|
38
|
+
};
|
|
39
|
+
this.storage.set(key, record);
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
record.count++;
|
|
44
|
+
if (record.count > this.limit) {
|
|
45
|
+
return this.handler(ctx, next);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return next();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convenience factory function
|
|
55
|
+
*/
|
|
56
|
+
export function rateLimit(options) {
|
|
57
|
+
const limiter = new RateLimiter(options);
|
|
58
|
+
return limiter.middleware();
|
|
59
|
+
}
|