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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Telegix - Chat Action Auto-Sender Middleware & Helper
3
+ */
4
+
5
+ /**
6
+ * Middleware that automatically sends a chat action (e.g. 'typing') periodically while processing a handler
7
+ * @param {string} [action='typing'] - Chat action type ('typing', 'upload_photo', etc.)
8
+ * @param {object} [options] - { intervalMs?: number } (default 4000ms)
9
+ */
10
+ export function chatActionMiddleware(action = 'typing', options = {}) {
11
+ const intervalMs = options.intervalMs || 4000;
12
+
13
+ return async (ctx, next) => {
14
+ const chatId = ctx.chat?.id;
15
+ if (!chatId) {
16
+ return next();
17
+ }
18
+
19
+ // Attach convenience helper to ctx
20
+ ctx.sendChatAction = (act = action, extra = {}) => {
21
+ return ctx.telegram.sendChatAction(chatId, act, extra);
22
+ };
23
+
24
+ // Send initial action
25
+ let active = true;
26
+ ctx.sendChatAction(action).catch(() => {});
27
+
28
+ const timer = setInterval(() => {
29
+ if (!active) return;
30
+ ctx.sendChatAction(action).catch(() => {});
31
+ }, intervalMs);
32
+
33
+ try {
34
+ await next();
35
+ } finally {
36
+ active = false;
37
+ clearInterval(timer);
38
+ }
39
+ };
40
+ }
package/lib/cluster.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Telegix - Multi-Bot Manager / Cluster Support
3
+ */
4
+
5
+ import { Telegix } from './telegix.js';
6
+
7
+ export class TelegixManager {
8
+ constructor() {
9
+ this.bots = new Map();
10
+ }
11
+
12
+ /**
13
+ * Add a bot instance to the manager
14
+ * @param {string} name - Identifier for the bot
15
+ * @param {string|object} tokenOrOptions - Bot token string or options object
16
+ * @returns {Telegix} Telegix instance
17
+ */
18
+ add(name, tokenOrOptions) {
19
+ if (this.bots.has(name)) {
20
+ return this.bots.get(name);
21
+ }
22
+ const bot = new Telegix(tokenOrOptions);
23
+ this.bots.set(name, bot);
24
+ return bot;
25
+ }
26
+
27
+ get(name) {
28
+ return this.bots.get(name);
29
+ }
30
+
31
+ remove(name) {
32
+ const bot = this.bots.get(name);
33
+ if (bot) {
34
+ bot.stop();
35
+ this.bots.delete(name);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Launch all managed bots
41
+ * @param {object} [options] - Launch options
42
+ */
43
+ async launchAll(options = {}) {
44
+ const promises = [];
45
+ for (const [name, bot] of this.bots.entries()) {
46
+ promises.push(
47
+ bot.launch(options).catch((err) => {
48
+ console.error(`Failed to launch bot "${name}":`, err.message);
49
+ throw err;
50
+ })
51
+ );
52
+ }
53
+ return Promise.all(promises);
54
+ }
55
+
56
+ /**
57
+ * Stop all managed bots
58
+ */
59
+ stopAll() {
60
+ for (const [name, bot] of this.bots.entries()) {
61
+ try {
62
+ bot.stop();
63
+ } catch (err) {
64
+ console.error(`Error stopping bot "${name}":`, err.message);
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Telegix - Middleware Composer and Router
3
+ * @module telegix/composer
4
+ */
5
+
6
+ /**
7
+ * Compose multiple middlewares into a single middleware function
8
+ * @param {Array<Function>} middlewares
9
+ * @returns {Function}
10
+ */
11
+ export function compose(middlewares) {
12
+ if (!Array.isArray(middlewares)) {
13
+ throw new TypeError('Middleware stack must be an array of functions');
14
+ }
15
+ for (const fn of middlewares) {
16
+ if (typeof fn !== 'function') {
17
+ throw new TypeError('Middleware must be a function');
18
+ }
19
+ }
20
+
21
+ return function (context, next) {
22
+ let index = -1;
23
+ function dispatch(i) {
24
+ if (i <= index) {
25
+ return Promise.reject(new Error('next() called multiple times in middleware'));
26
+ }
27
+ index = i;
28
+ let fn = middlewares[i];
29
+ if (i === middlewares.length) fn = next;
30
+ if (!fn) return Promise.resolve();
31
+ try {
32
+ return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
33
+ } catch (err) {
34
+ return Promise.reject(err);
35
+ }
36
+ }
37
+ return dispatch(0);
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Check if trigger (string, RegExp, or Function) matches text
43
+ * @param {string|RegExp|Function} trigger
44
+ * @param {string} text
45
+ * @returns {any} Match result or boolean
46
+ */
47
+ function matchTrigger(trigger, text) {
48
+ if (!text && text !== '') return null;
49
+ if (typeof trigger === 'string') {
50
+ return text === trigger ? [text] : null;
51
+ }
52
+ if (trigger instanceof RegExp) {
53
+ return text.match(trigger);
54
+ }
55
+ if (typeof trigger === 'function') {
56
+ return trigger(text);
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export class Composer {
62
+ constructor(...middlewares) {
63
+ this.middlewares = [];
64
+ this.use(...middlewares);
65
+ }
66
+
67
+ /**
68
+ * Register one or more middleware functions
69
+ * @param {...Function} middlewares
70
+ * @returns {this}
71
+ */
72
+ use(...middlewares) {
73
+ for (const mw of middlewares) {
74
+ if (mw instanceof Composer) {
75
+ this.middlewares.push(mw.middleware());
76
+ } else if (typeof mw === 'function') {
77
+ this.middlewares.push(mw);
78
+ } else {
79
+ throw new TypeError('Composer.use() expects functions or Composer instances');
80
+ }
81
+ }
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Returns composed middleware function
87
+ * @returns {Function}
88
+ */
89
+ middleware() {
90
+ return (ctx, next) => {
91
+ const fn = compose(this.middlewares);
92
+ return fn(ctx, next);
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Filter updates based on updateType or sub-filters (e.g. 'text', 'photo', 'callback_query')
98
+ * @param {string|Array<string>} updateTypes
99
+ * @param {...Function} middlewares
100
+ * @returns {this}
101
+ */
102
+ on(updateTypes, ...middlewares) {
103
+ const types = Array.isArray(updateTypes) ? updateTypes : [updateTypes];
104
+ const handler = compose(middlewares);
105
+
106
+ return this.use((ctx, next) => {
107
+ for (const type of types) {
108
+ if (this._matchesUpdateType(ctx, type)) {
109
+ return handler(ctx, next);
110
+ }
111
+ }
112
+ return next();
113
+ });
114
+ }
115
+
116
+ /**
117
+ * @private
118
+ */
119
+ _matchesUpdateType(ctx, type) {
120
+ if (!type) return false;
121
+
122
+ // Check top-level updateType
123
+ if (ctx.updateType === type || ctx.update[type]) return true;
124
+
125
+ const [mainType, subType] = type.split(':');
126
+
127
+ // If matching shorthand like 'text', 'photo', 'document', 'sticker', 'video', etc.
128
+ if (!subType) {
129
+ if (ctx.message && mainType in ctx.message) return true;
130
+ if (mainType === 'text' && typeof ctx.message?.text === 'string') return true;
131
+ return false;
132
+ }
133
+
134
+ // Match sub-filter like 'message:text', 'message:photo', 'channel_post:text'
135
+ const targetObj = ctx.update[mainType];
136
+ if (targetObj && typeof targetObj === 'object') {
137
+ if (subType in targetObj) return true;
138
+ if (subType === 'text' && typeof targetObj.text === 'string') return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Handle Telegram slash commands (e.g. /start, /help, /settings)
146
+ * @param {string|RegExp|Array<string|RegExp>} commands
147
+ * @param {...Function} middlewares
148
+ * @returns {this}
149
+ */
150
+ command(commands, ...middlewares) {
151
+ const list = Array.isArray(commands) ? commands : [commands];
152
+ const handler = compose(middlewares);
153
+
154
+ return this.use((ctx, next) => {
155
+ const text = ctx.message?.text || ctx.channelPost?.text;
156
+ if (!text || !text.startsWith('/')) return next();
157
+
158
+ const [rawCommandWithEntity, ...args] = text.trim().split(/\s+/);
159
+ const rawCommand = rawCommandWithEntity.slice(1);
160
+ const [cmdName, botUsername] = rawCommand.split('@');
161
+
162
+ // Check if command is addressed to another bot specifically
163
+ if (botUsername && ctx.botInfo?.username) {
164
+ if (botUsername.toLowerCase() !== ctx.botInfo.username.toLowerCase()) {
165
+ return next();
166
+ }
167
+ }
168
+
169
+ for (const trigger of list) {
170
+ let isMatch = false;
171
+ let matchResult = null;
172
+
173
+ if (typeof trigger === 'string') {
174
+ const cleanTrigger = trigger.startsWith('/') ? trigger.slice(1) : trigger;
175
+ if (cmdName.toLowerCase() === cleanTrigger.toLowerCase()) {
176
+ isMatch = true;
177
+ matchResult = [rawCommandWithEntity, args.join(' ')];
178
+ }
179
+ } else if (trigger instanceof RegExp) {
180
+ matchResult = cmdName.match(trigger);
181
+ if (matchResult) isMatch = true;
182
+ }
183
+
184
+ if (isMatch) {
185
+ ctx.command = cmdName;
186
+ ctx.payload = args.join(' ');
187
+ ctx.match = matchResult;
188
+ return handler(ctx, next);
189
+ }
190
+ }
191
+
192
+ return next();
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Match string or RegExp against message text or caption
198
+ * @param {string|RegExp|Function|Array<string|RegExp>} triggers
199
+ * @param {...Function} middlewares
200
+ * @returns {this}
201
+ */
202
+ hears(triggers, ...middlewares) {
203
+ const list = Array.isArray(triggers) ? triggers : [triggers];
204
+ const handler = compose(middlewares);
205
+
206
+ return this.use((ctx, next) => {
207
+ const text = ctx.message?.text || ctx.message?.caption;
208
+ if (!text) return next();
209
+
210
+ for (const trigger of list) {
211
+ const match = matchTrigger(trigger, text);
212
+ if (match) {
213
+ ctx.match = match;
214
+ return handler(ctx, next);
215
+ }
216
+ }
217
+
218
+ return next();
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Match string or RegExp against callbackQuery.data
224
+ * @param {string|RegExp|Function|Array<string|RegExp>} triggers
225
+ * @param {...Function} middlewares
226
+ * @returns {this}
227
+ */
228
+ action(triggers, ...middlewares) {
229
+ const list = Array.isArray(triggers) ? triggers : [triggers];
230
+ const handler = compose(middlewares);
231
+
232
+ return this.use((ctx, next) => {
233
+ const data = ctx.callbackQuery?.data;
234
+ if (data === undefined || data === null) return next();
235
+
236
+ for (const trigger of list) {
237
+ const match = matchTrigger(trigger, data);
238
+ if (match) {
239
+ ctx.match = match;
240
+ return handler(ctx, next);
241
+ }
242
+ }
243
+
244
+ return next();
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Match string or RegExp against inlineQuery.query
250
+ * @param {string|RegExp|Function|Array<string|RegExp>} triggers
251
+ * @param {...Function} middlewares
252
+ * @returns {this}
253
+ */
254
+ inlineQuery(triggers, ...middlewares) {
255
+ const list = Array.isArray(triggers) ? triggers : [triggers];
256
+ const handler = compose(middlewares);
257
+
258
+ return this.use((ctx, next) => {
259
+ const query = ctx.inlineQuery?.query;
260
+ if (query === undefined || query === null) return next();
261
+
262
+ for (const trigger of list) {
263
+ const match = matchTrigger(trigger, query);
264
+ if (match) {
265
+ ctx.match = match;
266
+ return handler(ctx, next);
267
+ }
268
+ }
269
+
270
+ return next();
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Conditional middleware execution
276
+ * @param {Function} predicate
277
+ * @param {...Function} middlewares
278
+ */
279
+ filter(predicate, ...middlewares) {
280
+ const handler = compose(middlewares);
281
+ return this.use(async (ctx, next) => {
282
+ const pass = await predicate(ctx);
283
+ if (pass) {
284
+ return handler(ctx, next);
285
+ }
286
+ return next();
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Drop updates that match predicate
292
+ * @param {Function} predicate
293
+ * @param {...Function} middlewares
294
+ */
295
+ drop(predicate, ...middlewares) {
296
+ return this.filter(async (ctx) => !(await predicate(ctx)), ...middlewares);
297
+ }
298
+
299
+ /**
300
+ * Branch middleware based on predicate
301
+ * @param {Function} predicate
302
+ * @param {Function} trueMiddleware
303
+ * @param {Function} [falseMiddleware]
304
+ */
305
+ branch(predicate, trueMiddleware, falseMiddleware = (ctx, next) => next()) {
306
+ return this.use(async (ctx, next) => {
307
+ const pass = await predicate(ctx);
308
+ return pass ? trueMiddleware(ctx, next) : falseMiddleware(ctx, next);
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Filter updates by chat type ('private', 'group', 'supergroup', 'channel')
314
+ * @param {string|Array<string>} types
315
+ * @param {...Function} middlewares
316
+ */
317
+ chatType(types, ...middlewares) {
318
+ const list = Array.isArray(types) ? types : [types];
319
+ return this.filter((ctx) => {
320
+ const chatType = ctx.chat?.type;
321
+ return chatType && list.includes(chatType);
322
+ }, ...middlewares);
323
+ }
324
+
325
+ /**
326
+ * Filter Telegram Business updates (business_connection, business_message, edited_business_message)
327
+ * @param {...Function} middlewares
328
+ */
329
+ business(...middlewares) {
330
+ return this.filter((ctx) => {
331
+ return Boolean(
332
+ ctx.businessConnection ||
333
+ ctx.businessMessage ||
334
+ ctx.editedBusinessMessage ||
335
+ ctx.deletedBusinessMessages
336
+ );
337
+ }, ...middlewares);
338
+ }
339
+
340
+ /**
341
+ * Filter emoji reaction updates (message_reaction, message_reaction_count)
342
+ * @param {...Function} middlewares
343
+ */
344
+ reaction(...middlewares) {
345
+ return this.filter((ctx) => {
346
+ return Boolean(ctx.messageReaction || ctx.messageReactionCount);
347
+ }, ...middlewares);
348
+ }
349
+
350
+ /**
351
+ * Filter chat boost updates (chat_boost, removed_chat_boost)
352
+ * @param {...Function} middlewares
353
+ */
354
+ boost(...middlewares) {
355
+ return this.filter((ctx) => {
356
+ return Boolean(ctx.chatBoost || ctx.removedChatBoost);
357
+ }, ...middlewares);
358
+ }
359
+
360
+ /**
361
+ * Filter forum topic updates or messages inside forum topics
362
+ * @param {...Function} middlewares
363
+ */
364
+ forumTopic(...middlewares) {
365
+ return this.filter((ctx) => {
366
+ return Boolean(
367
+ ctx.topicId ||
368
+ ctx.message?.forum_topic_created ||
369
+ ctx.message?.forum_topic_edited ||
370
+ ctx.message?.forum_topic_closed ||
371
+ ctx.message?.forum_topic_reopened ||
372
+ ctx.message?.general_forum_topic_hidden ||
373
+ ctx.message?.general_forum_topic_unhidden
374
+ );
375
+ }, ...middlewares);
376
+ }
377
+
378
+ /**
379
+ * Filter paid media updates (purchased_paid_media or messages with paid_media)
380
+ * @param {...Function} middlewares
381
+ */
382
+ paidMedia(...middlewares) {
383
+ return this.filter((ctx) => {
384
+ return Boolean(
385
+ ctx.purchasedPaidMedia ||
386
+ ctx.message?.paid_media ||
387
+ ctx.channelPost?.paid_media
388
+ );
389
+ }, ...middlewares);
390
+ }
391
+
392
+ /**
393
+ * Filter messages containing specific entity types (e.g. 'url', 'mention', 'hashtag', 'email', 'custom_emoji')
394
+ * @param {string|Array<string>} entityTypes
395
+ * @param {...Function} middlewares
396
+ */
397
+ entity(entityTypes, ...middlewares) {
398
+ const list = Array.isArray(entityTypes) ? entityTypes : [entityTypes];
399
+ const handler = compose(middlewares);
400
+
401
+ return this.use((ctx, next) => {
402
+ const entities = ctx.entities;
403
+ if (!entities || entities.length === 0) return next();
404
+
405
+ const matched = entities.filter((e) => list.includes(e.type));
406
+ if (matched.length > 0) {
407
+ ctx.matchedEntities = matched;
408
+ return handler(ctx, next);
409
+ }
410
+
411
+ return next();
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Static helper to compose middlewares without creating a Composer instance
417
+ */
418
+ static compose = compose;
419
+ }