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/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
+ }
@@ -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
+ }