@2byte/tgbot-framework 1.0.11 → 1.0.13
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/README.md +300 -300
- package/bin/2byte-cli.ts +97 -97
- package/package.json +55 -55
- package/src/cli/CreateBotCommand.ts +181 -181
- package/src/cli/GenerateCommand.ts +195 -195
- package/src/cli/InitCommand.ts +107 -107
- package/src/cli/TgAccountManager.ts +50 -50
- package/src/console/migrate.ts +82 -82
- package/src/core/ApiService.ts +20 -20
- package/src/core/ApiServiceManager.ts +63 -63
- package/src/core/App.ts +1242 -1178
- package/src/core/BotArtisan.ts +79 -79
- package/src/core/BotMigration.ts +30 -30
- package/src/core/BotSeeder.ts +66 -66
- package/src/core/Model.ts +84 -84
- package/src/core/utils.ts +2 -2
- package/src/illumination/Artisan.ts +149 -149
- package/src/illumination/InlineKeyboard.ts +79 -61
- package/src/illumination/Message2Byte.ts +256 -256
- package/src/illumination/Message2ByteLiveProgressive.ts +278 -278
- package/src/illumination/Message2bytePool.ts +113 -113
- package/src/illumination/Migration.ts +186 -186
- package/src/illumination/RunSectionRoute.ts +109 -95
- package/src/illumination/Section.ts +429 -420
- package/src/illumination/SectionComponent.ts +64 -64
- package/src/illumination/Telegraf2byteContext.ts +32 -32
- package/src/index.ts +42 -42
- package/src/libs/TelegramAccountControl.ts +1140 -1140
- package/src/libs/TgSender.ts +53 -53
- package/src/models/Model.ts +67 -67
- package/src/models/Proxy.ts +217 -217
- package/src/models/TgAccount.ts +362 -362
- package/src/models/index.ts +2 -2
- package/src/types.ts +198 -191
- package/src/user/UserModel.ts +297 -297
- package/src/user/UserStore.ts +119 -119
- package/src/workflow/services/MassSendApiService.ts +83 -83
- package/templates/bot/.env.example +33 -33
- package/templates/bot/artisan.ts +8 -8
- package/templates/bot/bot.ts +82 -82
- package/templates/bot/database/dbConnector.ts +4 -4
- package/templates/bot/database/migrate.ts +9 -9
- package/templates/bot/database/migrations/001_create_users.sql +18 -18
- package/templates/bot/database/migrations/007_proxy.sql +27 -27
- package/templates/bot/database/migrations/008_tg_accounts.sql +32 -32
- package/templates/bot/database/seed.ts +14 -14
- package/templates/bot/docs/CLI_SERVICES.md +536 -536
- package/templates/bot/docs/INPUT_SYSTEM.md +211 -211
- package/templates/bot/docs/MASS_SEND_SERVICE.md +327 -327
- package/templates/bot/docs/SERVICE_EXAMPLES.md +384 -384
- package/templates/bot/docs/TASK_SYSTEM.md +156 -156
- package/templates/bot/models/Model.ts +8 -8
- package/templates/bot/models/index.ts +1 -1
- package/templates/bot/package.json +30 -30
- package/templates/bot/sectionList.ts +9 -9
- package/templates/bot/sections/ExampleInputSection.ts +85 -85
- package/templates/bot/sections/ExampleLiveTaskerSection.ts +60 -60
- package/templates/bot/sections/HomeSection.ts +63 -63
- package/templates/bot/tsconfig.json +17 -0
- package/templates/bot/workflow/services/ExampleService.ts +23 -23
package/src/core/App.ts
CHANGED
|
@@ -1,1178 +1,1242 @@
|
|
|
1
|
-
import { Telegraf, Markup } from "telegraf";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { access } from "fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
Telegraf2byteContext,
|
|
6
|
-
Telegraf2byteContextExtraMethods,
|
|
7
|
-
} from "../illumination/Telegraf2byteContext";
|
|
8
|
-
import { Section } from "../illumination/Section";
|
|
9
|
-
import { RunSectionRoute } from "../illumination/RunSectionRoute";
|
|
10
|
-
import { UserModel } from "../user/UserModel";
|
|
11
|
-
import { UserStore } from "../user/UserStore";
|
|
12
|
-
import {
|
|
13
|
-
AppConfig,
|
|
14
|
-
EnvVars,
|
|
15
|
-
RunnedSection,
|
|
16
|
-
SectionEntityConfig,
|
|
17
|
-
SectionList,
|
|
18
|
-
SectionOptions,
|
|
19
|
-
UserRegistrationData,
|
|
20
|
-
} from "../types";
|
|
21
|
-
import { nameToCapitalize } from "./utils";
|
|
22
|
-
import { ApiServiceManager } from "./ApiServiceManager";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
private middlewares: CallableFunction[] = [];
|
|
52
|
-
private apiServiceManager!: ApiServiceManager;
|
|
53
|
-
|
|
54
|
-
// Система управления фоновыми задачами
|
|
55
|
-
private runningTasks: Map<
|
|
56
|
-
string,
|
|
57
|
-
{
|
|
58
|
-
task: Promise<any>;
|
|
59
|
-
cancel?: () => void;
|
|
60
|
-
status: "running" | "completed" | "failed" | "cancelled";
|
|
61
|
-
startTime: number;
|
|
62
|
-
endTime?: number;
|
|
63
|
-
error?: Error;
|
|
64
|
-
ctx: Telegraf2byteContext;
|
|
65
|
-
controller?: {
|
|
66
|
-
signal: AbortSignal;
|
|
67
|
-
sendMessage: (message: string) => Promise<void>;
|
|
68
|
-
onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
|
|
69
|
-
receiveMessage: (message: string) => Promise<void>;
|
|
70
|
-
};
|
|
71
|
-
messageQueue?: Array<{ message: string; source: "task" | "external" }>;
|
|
72
|
-
}
|
|
73
|
-
> = new Map();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
this.debugLog("
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
return
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1
|
+
import { Telegraf, Markup } from "telegraf";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { access } from "fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
Telegraf2byteContext,
|
|
6
|
+
Telegraf2byteContextExtraMethods,
|
|
7
|
+
} from "../illumination/Telegraf2byteContext";
|
|
8
|
+
import { Section } from "../illumination/Section";
|
|
9
|
+
import { RunSectionRoute } from "../illumination/RunSectionRoute";
|
|
10
|
+
import { UserModel } from "../user/UserModel";
|
|
11
|
+
import { UserStore } from "../user/UserStore";
|
|
12
|
+
import {
|
|
13
|
+
AppConfig,
|
|
14
|
+
EnvVars,
|
|
15
|
+
RunnedSection,
|
|
16
|
+
SectionEntityConfig,
|
|
17
|
+
SectionList,
|
|
18
|
+
SectionOptions,
|
|
19
|
+
UserRegistrationData,
|
|
20
|
+
} from "../types";
|
|
21
|
+
import { nameToCapitalize } from "./utils";
|
|
22
|
+
import { ApiServiceManager } from "./ApiServiceManager";
|
|
23
|
+
import { message } from "telegraf/filters";
|
|
24
|
+
|
|
25
|
+
export class App {
|
|
26
|
+
private config: AppConfig = {
|
|
27
|
+
accessPublic: true,
|
|
28
|
+
apiUrl: null,
|
|
29
|
+
envConfig: {},
|
|
30
|
+
botToken: null,
|
|
31
|
+
telegrafConfigLaunch: null,
|
|
32
|
+
settings: null,
|
|
33
|
+
userStorage: null,
|
|
34
|
+
builderPromises: [],
|
|
35
|
+
sections: {},
|
|
36
|
+
components: {},
|
|
37
|
+
debug: false,
|
|
38
|
+
devHotReloadSections: false,
|
|
39
|
+
telegrafLog: false,
|
|
40
|
+
mainMenuKeyboard: [],
|
|
41
|
+
hears: {},
|
|
42
|
+
terminateSigInt: true,
|
|
43
|
+
terminateSigTerm: true,
|
|
44
|
+
keepSectionInstances: false,
|
|
45
|
+
botCwd: process.cwd(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
public bot!: Telegraf<Telegraf2byteContext>;
|
|
49
|
+
private sectionClasses: Map<string, typeof Section> = new Map();
|
|
50
|
+
private runnedSections: WeakMap<UserModel, RunnedSection[]> = new WeakMap();
|
|
51
|
+
private middlewares: CallableFunction[] = [];
|
|
52
|
+
private apiServiceManager!: ApiServiceManager;
|
|
53
|
+
|
|
54
|
+
// Система управления фоновыми задачами
|
|
55
|
+
private runningTasks: Map<
|
|
56
|
+
string,
|
|
57
|
+
{
|
|
58
|
+
task: Promise<any>;
|
|
59
|
+
cancel?: () => void;
|
|
60
|
+
status: "running" | "completed" | "failed" | "cancelled";
|
|
61
|
+
startTime: number;
|
|
62
|
+
endTime?: number;
|
|
63
|
+
error?: Error;
|
|
64
|
+
ctx: Telegraf2byteContext;
|
|
65
|
+
controller?: {
|
|
66
|
+
signal: AbortSignal;
|
|
67
|
+
sendMessage: (message: string) => Promise<void>;
|
|
68
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
|
|
69
|
+
receiveMessage: (message: string) => Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
messageQueue?: Array<{ message: string; source: "task" | "external" }>;
|
|
72
|
+
}
|
|
73
|
+
> = new Map();
|
|
74
|
+
|
|
75
|
+
private messageHandlers: any[] = [];
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
this.middlewares.push(this.mainMiddleware.bind(this));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static Builder = class {
|
|
82
|
+
public app: App;
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
this.app = new App();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
accessPublic(isPublic: boolean = true): this {
|
|
89
|
+
this.app.config.accessPublic = isPublic;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
accessPrivate(isPrivate: boolean = true): this {
|
|
94
|
+
this.app.config.accessPublic = !isPrivate;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
apiUrl(url: string): this {
|
|
99
|
+
this.app.config.apiUrl = url;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
botToken(token: string): this {
|
|
104
|
+
this.app.config.botToken = token;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
telegrafConfigLaunch(config: Record<string, any>): this {
|
|
109
|
+
this.app.config.telegrafConfigLaunch = config;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
settings(settings: Record<string, any>): this {
|
|
114
|
+
this.app.config.settings = settings;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
userStorage(storage: UserStore): this {
|
|
119
|
+
this.app.config.userStorage = storage;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
debug(isDebug: boolean = true): this {
|
|
124
|
+
this.app.config.debug = isDebug;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
devHotReloadSections(isReload: boolean = true): this {
|
|
129
|
+
this.app.config.devHotReloadSections = isReload;
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
telegrafLog(isLog: boolean = true): this {
|
|
134
|
+
this.app.config.telegrafLog = isLog;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
mainMenuKeyboard(keyboard: any[][]): this {
|
|
139
|
+
this.app.config.mainMenuKeyboard = keyboard;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hears(hearsMap: Record<string, string>): this {
|
|
144
|
+
this.app.config.hears = hearsMap;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
terminateSigInt(isTerminate: boolean = true): this {
|
|
149
|
+
this.app.config.terminateSigInt = isTerminate;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
terminateSigTerm(isTerminate: boolean = true): this {
|
|
154
|
+
this.app.config.terminateSigTerm = isTerminate;
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sections(sectionsList: SectionList): this {
|
|
159
|
+
this.app.config.sections = sectionsList;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
messageHandlers(handlers: any[]): this {
|
|
164
|
+
this.app.messageHandlers = handlers;
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
*
|
|
170
|
+
* @param keep Whether to keep section instances in memory after they are run.
|
|
171
|
+
* If true, sections will not be reloaded on each request, improving performance for frequently accessed sections.
|
|
172
|
+
* If false, sections will be reloaded each time they are accessed, ensuring the latest version is used.
|
|
173
|
+
* Default is true.
|
|
174
|
+
* @returns
|
|
175
|
+
*/
|
|
176
|
+
keepSectionInstances(keep: boolean = true): this {
|
|
177
|
+
this.app.config.keepSectionInstances = keep;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
envConfig(config: EnvVars): this {
|
|
182
|
+
this.app.config.envConfig = config;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
botCwd(cwdPath: string): this {
|
|
187
|
+
this.app.config.botCwd = cwdPath;
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
build(): App {
|
|
192
|
+
return this.app;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
async init(): Promise<this> {
|
|
197
|
+
if (!this.config.botToken) {
|
|
198
|
+
throw new Error("Bot token is not set");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.bot = new Telegraf<Telegraf2byteContext>(this.config.botToken);
|
|
202
|
+
|
|
203
|
+
if (this.config.telegrafLog) {
|
|
204
|
+
this.bot.use(Telegraf.log());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.debugLog("AppConfig", this.config);
|
|
208
|
+
|
|
209
|
+
await this.registerSections();
|
|
210
|
+
|
|
211
|
+
this.middlewares.forEach((middleware: CallableFunction) => {
|
|
212
|
+
middleware();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
this.registerCommands();
|
|
216
|
+
this.registerActionForCallbackQuery();
|
|
217
|
+
this.registerHears();
|
|
218
|
+
this.registerMessageHandlers();
|
|
219
|
+
await this.registerServices();
|
|
220
|
+
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async launch(): Promise<this> {
|
|
225
|
+
if (!this.bot) {
|
|
226
|
+
throw new Error("Bot is not initialized");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.bot.catch((err) => {
|
|
230
|
+
this.debugLog("Error in bot:", err);
|
|
231
|
+
if (this.config.debug && err instanceof Error) {
|
|
232
|
+
this.debugLog("Stack trace:", err.stack);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await this.bot.launch(this.config.telegrafConfigLaunch || {});
|
|
237
|
+
|
|
238
|
+
if (this.config.terminateSigInt) {
|
|
239
|
+
process.once("SIGINT", () => {
|
|
240
|
+
this.bot?.stop("SIGINT");
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.config.terminateSigTerm) {
|
|
245
|
+
process.once("SIGTERM", () => {
|
|
246
|
+
this.bot?.stop("SIGTERM");
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async mainMiddleware() {
|
|
254
|
+
this.bot.use(async (ctx: Telegraf2byteContext, next: () => Promise<void>) => {
|
|
255
|
+
const tgUsername = this.getTgUsername(ctx);
|
|
256
|
+
|
|
257
|
+
if (!tgUsername) {
|
|
258
|
+
return ctx.reply("Username is not set");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.config.userStorage) {
|
|
262
|
+
throw new Error("User storage is not set");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let startPayload: string | null = null;
|
|
266
|
+
let accessKey: string | null = null;
|
|
267
|
+
|
|
268
|
+
if (ctx?.message?.text?.startsWith("/start")) {
|
|
269
|
+
startPayload = ctx?.message?.text?.split(" ")[1] || null;
|
|
270
|
+
accessKey =
|
|
271
|
+
startPayload && startPayload.includes("key=")
|
|
272
|
+
? startPayload.split("key=")[1] || null
|
|
273
|
+
: null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check access by username and register user if not exists
|
|
277
|
+
if (!this.config.userStorage.exists(tgUsername)) {
|
|
278
|
+
const isAuthByUsername = !this.config.accessPublic && !accessKey;
|
|
279
|
+
|
|
280
|
+
// check access by username for private bots
|
|
281
|
+
if (isAuthByUsername) {
|
|
282
|
+
const requestUsername = this.getTgUsername(ctx);
|
|
283
|
+
this.debugLog("Private access mode. Checking username:", requestUsername);
|
|
284
|
+
const checkAccess =
|
|
285
|
+
this.config.envConfig.ACCESS_USERNAMES &&
|
|
286
|
+
this.config.envConfig.ACCESS_USERNAMES.split(",").map((name) => name.trim());
|
|
287
|
+
if (
|
|
288
|
+
checkAccess &&
|
|
289
|
+
checkAccess.every((name) => name.toLowerCase() !== requestUsername.toLowerCase())
|
|
290
|
+
) {
|
|
291
|
+
return ctx.reply("Access denied. Your username is not in the access list.");
|
|
292
|
+
}
|
|
293
|
+
this.debugLog("Username access granted.");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// check access keys for private bots
|
|
297
|
+
if (!isAuthByUsername && accessKey) {
|
|
298
|
+
this.debugLog("Private access mode. Checking access key in start payload.");
|
|
299
|
+
const accessKeys =
|
|
300
|
+
this.config.envConfig.BOT_ACCESS_KEYS &&
|
|
301
|
+
this.config.envConfig.BOT_ACCESS_KEYS.split(",").map((key) => key.trim());
|
|
302
|
+
if (
|
|
303
|
+
accessKeys &&
|
|
304
|
+
accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase())
|
|
305
|
+
) {
|
|
306
|
+
return ctx.reply("Access denied. Your access key is not valid.");
|
|
307
|
+
}
|
|
308
|
+
this.debugLog("Access key granted.");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!ctx.from) {
|
|
312
|
+
return ctx.reply("User information is not available");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const userRefIdFromStart = startPayload ? parseInt(startPayload) : 0;
|
|
316
|
+
|
|
317
|
+
await this.registerUser({
|
|
318
|
+
user_refid: userRefIdFromStart,
|
|
319
|
+
tg_id: ctx.from.id,
|
|
320
|
+
tg_username: tgUsername,
|
|
321
|
+
tg_first_name: ctx.from.first_name || tgUsername,
|
|
322
|
+
tg_last_name: ctx.from.last_name || "",
|
|
323
|
+
role: "user",
|
|
324
|
+
language: ctx.from.language_code || "en",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
ctx.user = this.config.userStorage.find(tgUsername);
|
|
329
|
+
ctx.userStorage = this.config.userStorage;
|
|
330
|
+
ctx.userSession = this.config.userStorage.findSession(ctx.user);
|
|
331
|
+
Object.assign(ctx, Telegraf2byteContextExtraMethods);
|
|
332
|
+
|
|
333
|
+
this.config.userStorage.upActive(tgUsername);
|
|
334
|
+
|
|
335
|
+
if (ctx.msgId) {
|
|
336
|
+
this.config.userStorage.storeMessageId(tgUsername, ctx.msgId, 10);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return next();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async registerActionForCallbackQuery() {
|
|
344
|
+
// Register actions
|
|
345
|
+
this.bot.action(/(.+)/, async (ctx: Telegraf2byteContext) => {
|
|
346
|
+
let actionPath = (ctx as any).match?.[1];
|
|
347
|
+
const actionParamsString = actionPath.match(/\[(.+)\]/);
|
|
348
|
+
let actionParams: URLSearchParams = new URLSearchParams();
|
|
349
|
+
|
|
350
|
+
if (actionParamsString && actionParamsString[1]) {
|
|
351
|
+
actionParams = new URLSearchParams(actionParamsString[1]);
|
|
352
|
+
|
|
353
|
+
actionPath = actionPath.split("[")[0];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.debugLog(
|
|
357
|
+
`Run action ${actionPath} with params ${actionParams.toString()} for user ${
|
|
358
|
+
ctx.user.username
|
|
359
|
+
}`
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
if (!actionPath) return;
|
|
363
|
+
|
|
364
|
+
// Assuming SectionData is a class to parse actionPath, but it's missing, so we will parse manually here
|
|
365
|
+
const actionPathParts = actionPath.split(".");
|
|
366
|
+
|
|
367
|
+
if (actionPathParts.length >= 2) {
|
|
368
|
+
const sectionId = actionPathParts[0];
|
|
369
|
+
|
|
370
|
+
let sectionClass = this.sectionClasses.get(sectionId);
|
|
371
|
+
|
|
372
|
+
if (!sectionClass) {
|
|
373
|
+
throw new Error(`Section class not found for sectionId ${sectionId}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const method = sectionClass.actionRoutes[actionPath];
|
|
377
|
+
|
|
378
|
+
if (!method) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Action ${actionPath} method ${method} not found in section ${sectionId}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const sectionRoute = new RunSectionRoute()
|
|
385
|
+
.section(sectionId)
|
|
386
|
+
.method(method)
|
|
387
|
+
.callbackParams(actionPath, actionParams.toString());
|
|
388
|
+
|
|
389
|
+
this.runSection(ctx, sectionRoute).catch((err) => {
|
|
390
|
+
this.debugLog("Error running section:", err);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
registerHears() {
|
|
397
|
+
// Register hears
|
|
398
|
+
Object.entries(this.config.hears).forEach(([key, sectionMethod]) => {
|
|
399
|
+
this.bot.hears(key, async (ctx: Telegraf2byteContext) => {
|
|
400
|
+
const [sectionId, method] = sectionMethod.split(".");
|
|
401
|
+
const sectionRoute = new RunSectionRoute().section(sectionId).method(method).hearsKey(key);
|
|
402
|
+
|
|
403
|
+
this.debugLog(`Hears matched: ${key}, running section ${sectionId}, method ${method}`);
|
|
404
|
+
|
|
405
|
+
this.runSection(ctx, sectionRoute).catch((err) => {
|
|
406
|
+
this.debugLog("Error running section:", err);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
registerMessageHandlers() {
|
|
413
|
+
// Register message handler for text messages
|
|
414
|
+
this.bot.on(message("text"), async (ctx: Telegraf2byteContext) => {
|
|
415
|
+
this.debugLog("[registerMessageHandlers]: received text", (ctx.update as any).message?.text);
|
|
416
|
+
const messageText = (ctx.update as any).message?.text;
|
|
417
|
+
if (!messageText) return;
|
|
418
|
+
|
|
419
|
+
await this.handleUserInput(ctx, messageText, "text");
|
|
420
|
+
|
|
421
|
+
// otherwise, if not handled by awaitingInput or awaitingInputPromise, we can check if user is in a section that has a message handler
|
|
422
|
+
if (
|
|
423
|
+
!ctx.userSession.awaitingInput &&
|
|
424
|
+
!ctx.userSession.awaitingInputPromise &&
|
|
425
|
+
!ctx.userSession.stateAfterValidatedUserResponse
|
|
426
|
+
) {
|
|
427
|
+
this.messageHandlers.forEach(async (handler: any) => {
|
|
428
|
+
const handlerIsClass =
|
|
429
|
+
typeof handler === "function" && /^\s*class\s+/.test(handler.toString());
|
|
430
|
+
const nameHandler = handlerIsClass
|
|
431
|
+
? handler.name
|
|
432
|
+
: handler.constructor?.name || "unknown";
|
|
433
|
+
|
|
434
|
+
this.debugLog("Handling message with handler class:", nameHandler);
|
|
435
|
+
|
|
436
|
+
if (handlerIsClass) {
|
|
437
|
+
await new handler(this).handle(ctx);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
} else {
|
|
441
|
+
this.debugLog("Message input already handled by awaitingInput or awaitingInputPromise. stateAfterValidatedUserResponse:", ctx.userSession.stateAfterValidatedUserResponse);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
delete ctx.userSession.stateAfterValidatedUserResponse; // Clear the state after handling the message
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Register message handler for documents/files
|
|
448
|
+
this.bot.on(message("document"), async (ctx: Telegraf2byteContext) => {
|
|
449
|
+
const document = (ctx.update as any).message?.document;
|
|
450
|
+
if (!document) return;
|
|
451
|
+
|
|
452
|
+
await this.handleUserInput(ctx, document, "file");
|
|
453
|
+
|
|
454
|
+
delete ctx.userSession.stateAfterValidatedUserResponse; // Clear the state after handling the message
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Register message handler for photos
|
|
458
|
+
this.bot.on(message("photo"), async (ctx: Telegraf2byteContext) => {
|
|
459
|
+
const photo = (ctx.update as any).message?.photo;
|
|
460
|
+
if (!photo || !photo.length) return;
|
|
461
|
+
|
|
462
|
+
// Get the largest photo (the last one in the array is usually the largest)
|
|
463
|
+
const largestPhoto = photo[photo.length - 1];
|
|
464
|
+
await this.handleUserInput(ctx, largestPhoto, "photo");
|
|
465
|
+
|
|
466
|
+
delete ctx.userSession.stateAfterValidatedUserResponse; // Clear the state after handling the message
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private async registerServices() {
|
|
471
|
+
this.apiServiceManager = ApiServiceManager.init(this);
|
|
472
|
+
|
|
473
|
+
const registerServices = async (pathDirectory: string) => {
|
|
474
|
+
try {
|
|
475
|
+
await this.apiServiceManager.loadServicesFromDirectory(pathDirectory);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
this.debugLog("Error loading services:", error);
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.debugLog(
|
|
482
|
+
"Registered API services:%s in dir: %s",
|
|
483
|
+
Array.from(this.apiServiceManager.getAll().keys()),
|
|
484
|
+
pathDirectory
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
for (const [name, service] of this.apiServiceManager.getAll()) {
|
|
488
|
+
await service.setup();
|
|
489
|
+
this.debugLog(`Service ${name} setup completed`);
|
|
490
|
+
await service.run();
|
|
491
|
+
this.debugLog(`Service ${name} run completed`);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Register services from bot directory
|
|
496
|
+
await registerServices(this.config.botCwd + "/workflow/services");
|
|
497
|
+
// Register services from framework directory
|
|
498
|
+
await registerServices(path.resolve(__dirname, "../workflow/services"));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private async unregisterServices() {
|
|
502
|
+
this.apiServiceManager = ApiServiceManager.init(this);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
this.apiServiceManager.unsetupAllServices();
|
|
506
|
+
} catch (error) {
|
|
507
|
+
this.debugLog("Error unsetting up services:", error);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private async handleUserInput(
|
|
513
|
+
ctx: Telegraf2byteContext,
|
|
514
|
+
inputValue: any,
|
|
515
|
+
inputType: "text" | "file" | "photo"
|
|
516
|
+
) {
|
|
517
|
+
// Handling awaitingInputPromise (for requestInputWithAwait)
|
|
518
|
+
if (ctx.userSession.awaitingInputPromise) {
|
|
519
|
+
this.debugLog("Handling input for awaitingInputPromise");
|
|
520
|
+
const awaitingPromise = ctx.userSession.awaitingInputPromise;
|
|
521
|
+
const {
|
|
522
|
+
key,
|
|
523
|
+
validator,
|
|
524
|
+
errorMessage,
|
|
525
|
+
allowCancel,
|
|
526
|
+
retryCount = 0,
|
|
527
|
+
resolve,
|
|
528
|
+
reject,
|
|
529
|
+
} = awaitingPromise;
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const isValid = await this.validateUserInput(
|
|
533
|
+
inputValue,
|
|
534
|
+
validator,
|
|
535
|
+
inputType,
|
|
536
|
+
awaitingPromise.fileValidation
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
this.debugLog(`Input validation result for key ${key}:`, isValid);
|
|
540
|
+
|
|
541
|
+
if (isValid) {
|
|
542
|
+
// save the input value in user session under the specified key
|
|
543
|
+
ctx.userSession[key] = inputValue;
|
|
544
|
+
// clear the awaiting promise state
|
|
545
|
+
delete ctx.userSession.awaitingInputPromise;
|
|
546
|
+
|
|
547
|
+
ctx.userSession.stateAfterValidatedUserResponse = true; // Set a flag to indicate that we are now in the state after receiving input
|
|
548
|
+
// Resolve the Promise
|
|
549
|
+
resolve(inputValue);
|
|
550
|
+
ctx.deleteLastMessage();
|
|
551
|
+
} else {
|
|
552
|
+
// Increase the retry count
|
|
553
|
+
awaitingPromise.retryCount = retryCount + 1;
|
|
554
|
+
|
|
555
|
+
// Send an error message
|
|
556
|
+
let errorMsg = errorMessage;
|
|
557
|
+
if (awaitingPromise.retryCount > 1) {
|
|
558
|
+
errorMsg += ` (попытка ${awaitingPromise.retryCount})`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (allowCancel) {
|
|
562
|
+
errorMsg += '\n\nИспользуйте кнопку "Отмена" для отмены ввода.';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
await ctx.reply(errorMsg, {
|
|
566
|
+
...Markup.inlineKeyboard([
|
|
567
|
+
[
|
|
568
|
+
Markup.button.callback(
|
|
569
|
+
"Отмена",
|
|
570
|
+
ctx?.userSession?.previousSection?.route?.getActionPath() ?? "home.index"
|
|
571
|
+
),
|
|
572
|
+
],
|
|
573
|
+
]),
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Do not clear the awaiting state - the user remains in input mode
|
|
577
|
+
// The state will be cleared only upon successful input or cancellation
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
await ctx.reply(`Validation error: ${error}`);
|
|
581
|
+
if (allowCancel) {
|
|
582
|
+
await ctx.reply('Use the "Cancel" button to cancel input.');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Handling awaitingInput (for requestInput with callback)
|
|
589
|
+
if (ctx.userSession.awaitingInput) {
|
|
590
|
+
const awaitingInput = ctx.userSession.awaitingInput;
|
|
591
|
+
const {
|
|
592
|
+
key,
|
|
593
|
+
validator,
|
|
594
|
+
errorMessage,
|
|
595
|
+
allowCancel,
|
|
596
|
+
retryCount = 0,
|
|
597
|
+
runSection,
|
|
598
|
+
} = awaitingInput;
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const isValid = await this.validateUserInput(
|
|
602
|
+
inputValue,
|
|
603
|
+
validator,
|
|
604
|
+
inputType,
|
|
605
|
+
awaitingInput.fileValidation
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
if (isValid) {
|
|
609
|
+
// save the input value in user session under the specified key
|
|
610
|
+
ctx.userSession[key] = inputValue;
|
|
611
|
+
// clear the awaiting promise state
|
|
612
|
+
delete ctx.userSession.awaitingInput;
|
|
613
|
+
|
|
614
|
+
ctx.userSession.stateAfterValidatedUserResponse = true; // Set a flag to indicate that we are now in the state after receiving input
|
|
615
|
+
|
|
616
|
+
// If runSection is specified, execute it
|
|
617
|
+
if (runSection) {
|
|
618
|
+
runSection.runAsCommand();
|
|
619
|
+
await this.runSection(ctx, runSection);
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
// Increase the retry count
|
|
623
|
+
awaitingInput.retryCount = retryCount + 1;
|
|
624
|
+
|
|
625
|
+
// Send an error message
|
|
626
|
+
let errorMsg = errorMessage;
|
|
627
|
+
if (awaitingInput.retryCount > 1) {
|
|
628
|
+
errorMsg += ` (attempt ${awaitingInput.retryCount})`;
|
|
629
|
+
}
|
|
630
|
+
if (allowCancel) {
|
|
631
|
+
errorMsg += '\n\nUse the "Cancel" button to cancel input.';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
await ctx.reply(errorMsg, {
|
|
635
|
+
...Markup.inlineKeyboard([
|
|
636
|
+
[
|
|
637
|
+
Markup.button.callback(
|
|
638
|
+
"Cancel",
|
|
639
|
+
ctx?.userSession?.previousSection?.route?.getActionPath() ?? "home.index"
|
|
640
|
+
),
|
|
641
|
+
],
|
|
642
|
+
]),
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Do not clear the awaiting state - the user remains in input mode
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
await ctx.reply(`Validation error: ${error}`);
|
|
649
|
+
if (allowCancel) {
|
|
650
|
+
await ctx.reply('Use the "Cancel" button to cancel input.');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private async validateUserInput(
|
|
658
|
+
value: any,
|
|
659
|
+
validator?: "number" | "phone" | "code" | "file" | ((value: any) => boolean | Promise<boolean>),
|
|
660
|
+
inputType?: "text" | "file" | "photo",
|
|
661
|
+
fileValidation?: { allowedTypes?: string[]; maxSize?: number; minSize?: number }
|
|
662
|
+
): Promise<boolean> {
|
|
663
|
+
if (!validator) return true;
|
|
664
|
+
|
|
665
|
+
if (typeof validator === "function") {
|
|
666
|
+
const result = validator(value);
|
|
667
|
+
return result instanceof Promise ? await result : result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
switch (validator) {
|
|
671
|
+
case "number":
|
|
672
|
+
if (inputType !== "text") return false;
|
|
673
|
+
return !isNaN(Number(value)) && value.trim() !== "";
|
|
674
|
+
|
|
675
|
+
case "phone":
|
|
676
|
+
if (inputType !== "text") return false;
|
|
677
|
+
// Remove all non-digit characters
|
|
678
|
+
const cleanNumber = value.replace(/\D/g, "");
|
|
679
|
+
// Check international phone number format:
|
|
680
|
+
// - Optional '+' at start
|
|
681
|
+
// - May start with country code (1-3 digits)
|
|
682
|
+
// - Followed by 6-12 digits
|
|
683
|
+
// This covers most international formats including:
|
|
684
|
+
// - Russian format (7xxxxxxxxxx)
|
|
685
|
+
// - US/Canada format (1xxxxxxxxxx)
|
|
686
|
+
// - European formats
|
|
687
|
+
// - Asian formats
|
|
688
|
+
const phoneRegex = /^(\+?\d{1,3})?[0-9]{6,12}$/;
|
|
689
|
+
return phoneRegex.test(cleanNumber);
|
|
690
|
+
|
|
691
|
+
case "code":
|
|
692
|
+
if (inputType !== "text") return false;
|
|
693
|
+
// Проверяем код подтверждения (обычно 5-6 цифр)
|
|
694
|
+
const codeRegex = /^[0-9]{5,6}$/;
|
|
695
|
+
return codeRegex.test(value);
|
|
696
|
+
|
|
697
|
+
case "file":
|
|
698
|
+
if (inputType !== "file" && inputType !== "photo") return false;
|
|
699
|
+
|
|
700
|
+
// Валидация файла
|
|
701
|
+
if (fileValidation) {
|
|
702
|
+
// Проверка типа файла
|
|
703
|
+
if (fileValidation.allowedTypes && fileValidation.allowedTypes.length > 0) {
|
|
704
|
+
const mimeType = value.mime_type || "";
|
|
705
|
+
if (!fileValidation.allowedTypes.includes(mimeType)) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`Неподдерживаемый тип файла. Разрешены: ${fileValidation.allowedTypes.join(", ")}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Проверка размера файла
|
|
713
|
+
const fileSize = value.file_size || 0;
|
|
714
|
+
if (fileValidation.maxSize && fileSize > fileValidation.maxSize) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`Файл слишком большой. Максимальный размер: ${Math.round(
|
|
717
|
+
fileValidation.maxSize / 1024 / 1024
|
|
718
|
+
)} МБ`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (fileValidation.minSize && fileSize < fileValidation.minSize) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`Файл слишком маленький. Минимальный размер: ${Math.round(
|
|
725
|
+
fileValidation.minSize / 1024
|
|
726
|
+
)} КБ`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return true;
|
|
732
|
+
|
|
733
|
+
default:
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
registerCommands() {
|
|
739
|
+
// Register command handlers
|
|
740
|
+
// Register commands according to sections, each section class has static command method
|
|
741
|
+
Array.from(this.sectionClasses.entries()).forEach(([sectionId, sectionClass]) => {
|
|
742
|
+
const command = (sectionClass as any).command;
|
|
743
|
+
this.debugLog(`Register command ${command} for section ${sectionId}`);
|
|
744
|
+
if (command) {
|
|
745
|
+
this.bot.command(command, async (ctx: Telegraf2byteContext) => {
|
|
746
|
+
const sectionRoute = new RunSectionRoute()
|
|
747
|
+
.section(sectionId)
|
|
748
|
+
.method("index")
|
|
749
|
+
.runAsCommand();
|
|
750
|
+
await this.runSection(ctx, sectionRoute);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async loadSection(sectionId: string, freshVersion: boolean = false): Promise<typeof Section> {
|
|
757
|
+
const sectionParams = Object.entries(this.config.sections).find(
|
|
758
|
+
([sectionId]) => sectionId === sectionId
|
|
759
|
+
)?.[1] as SectionEntityConfig;
|
|
760
|
+
|
|
761
|
+
if (!sectionParams) {
|
|
762
|
+
throw new Error(`Section ${sectionId} not found`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let pathSectionModule =
|
|
766
|
+
sectionParams.pathModule ??
|
|
767
|
+
path.join(process.cwd(), "./sections/" + nameToCapitalize(sectionId) + "Section");
|
|
768
|
+
|
|
769
|
+
this.debugLog("Path to section module: ", pathSectionModule);
|
|
770
|
+
|
|
771
|
+
// Check if file exists
|
|
772
|
+
try {
|
|
773
|
+
await access(pathSectionModule + ".ts");
|
|
774
|
+
} catch {
|
|
775
|
+
throw new Error(`Section ${sectionId} not found at path ${pathSectionModule}.ts`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// For bypassing cache in Bun, we need to clear the module cache
|
|
779
|
+
if (freshVersion && typeof Bun !== "undefined") {
|
|
780
|
+
// Clear Bun's module cache for this specific module
|
|
781
|
+
const modulePath = pathSectionModule + ".ts";
|
|
782
|
+
this.debugLog("Clearing cache for fresh version of section:", modulePath);
|
|
783
|
+
|
|
784
|
+
// In Bun, we can use dynamic import with a unique query to bypass cache
|
|
785
|
+
// But we need to resolve the absolute path first
|
|
786
|
+
const absolutePath = path.resolve(modulePath);
|
|
787
|
+
|
|
788
|
+
// Try to delete from require cache if it exists
|
|
789
|
+
if (require.cache && require.cache[absolutePath]) {
|
|
790
|
+
delete require.cache[absolutePath];
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const sectionClass = (await import(pathSectionModule)).default as typeof Section;
|
|
795
|
+
this.debugLog("Loaded section", sectionId);
|
|
796
|
+
|
|
797
|
+
return sectionClass;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async registerSections() {
|
|
801
|
+
// Register sections routes
|
|
802
|
+
for (const sectionId of Object.keys(this.config.sections)) {
|
|
803
|
+
this.debugLog("Registration section: " + sectionId);
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
this.sectionClasses.set(sectionId, await this.loadSection(sectionId));
|
|
807
|
+
} catch (err) {
|
|
808
|
+
this.debugLog("Error stack:", err instanceof Error ? err.stack : "No stack available");
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Failed to load section ${sectionId}: ${err instanceof Error ? err.message : err}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async runSection(
|
|
817
|
+
ctx: Telegraf2byteContext,
|
|
818
|
+
sectionRoute: RunSectionRoute,
|
|
819
|
+
params: Partial<{ cbBeforeRunMethod: (sectionInstance: Section) => Promise<void> }> = {}
|
|
820
|
+
): Promise<void> {
|
|
821
|
+
const sectionId = sectionRoute.getSection();
|
|
822
|
+
const method = sectionRoute.getMethod();
|
|
823
|
+
|
|
824
|
+
this.debugLog(`Run section ${sectionId} method ${method}`);
|
|
825
|
+
|
|
826
|
+
if (!sectionId || !method) {
|
|
827
|
+
throw new Error("Section or method is not set");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let sectionClass: typeof Section;
|
|
831
|
+
|
|
832
|
+
if (this.config.devHotReloadSections) {
|
|
833
|
+
sectionClass = await this.loadSection(sectionId, true);
|
|
834
|
+
} else {
|
|
835
|
+
if (!this.sectionClasses.has(sectionId)) {
|
|
836
|
+
throw new Error(`Section ${sectionId} not found`);
|
|
837
|
+
}
|
|
838
|
+
sectionClass = this.sectionClasses.get(sectionId) as typeof Section;
|
|
839
|
+
}
|
|
840
|
+
this.debugLog("Using section class:", sectionClass);
|
|
841
|
+
|
|
842
|
+
let sectionInstance: Section | undefined;
|
|
843
|
+
|
|
844
|
+
const createSectionInstance = (sectionClass: typeof Section) => {
|
|
845
|
+
return new sectionClass({
|
|
846
|
+
ctx,
|
|
847
|
+
bot: this.bot,
|
|
848
|
+
app: this,
|
|
849
|
+
route: sectionRoute,
|
|
850
|
+
} as SectionOptions);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const createRunnedSection = (instance: Section, route: RunSectionRoute): RunnedSection => {
|
|
854
|
+
return {
|
|
855
|
+
instance,
|
|
856
|
+
route,
|
|
857
|
+
};
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const findRunnedSection = () => {
|
|
861
|
+
const userRunnedSections = this.runnedSections.get(ctx.user);
|
|
862
|
+
if (userRunnedSections && Array.isArray(userRunnedSections)) {
|
|
863
|
+
return userRunnedSections.find((section) => section.route.getSection() === sectionId);
|
|
864
|
+
}
|
|
865
|
+
return undefined;
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
let isRestoredSection = false;
|
|
869
|
+
let runnedSection: RunnedSection | undefined = undefined;
|
|
870
|
+
let createdNewSectionInstance = false;
|
|
871
|
+
|
|
872
|
+
if (this.config.keepSectionInstances) {
|
|
873
|
+
runnedSection = findRunnedSection();
|
|
874
|
+
if (runnedSection) {
|
|
875
|
+
runnedSection.instance
|
|
876
|
+
.updateCtx(ctx)
|
|
877
|
+
.updateRoute(sectionRoute)
|
|
878
|
+
.setCallbackParams(sectionRoute.getCallbackParams());
|
|
879
|
+
|
|
880
|
+
runnedSection.route.runAsCallbackQuery(sectionRoute.runIsCallbackQuery);
|
|
881
|
+
|
|
882
|
+
isRestoredSection = true;
|
|
883
|
+
} else {
|
|
884
|
+
createdNewSectionInstance = true;
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
createdNewSectionInstance = true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (isRestoredSection) {
|
|
891
|
+
this.debugLog(
|
|
892
|
+
`Restored a runned section for user ${ctx.user.username}:`,
|
|
893
|
+
runnedSection?.instance.sectionId
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (createdNewSectionInstance) {
|
|
898
|
+
this.debugLog(`Creating new section instance for user ${ctx.user.username}`);
|
|
899
|
+
runnedSection = createRunnedSection(createSectionInstance(sectionClass), sectionRoute);
|
|
900
|
+
if (this.config.keepSectionInstances) {
|
|
901
|
+
if (!this.runnedSections.has(ctx.user)) {
|
|
902
|
+
this.runnedSections.set(ctx.user, []);
|
|
903
|
+
}
|
|
904
|
+
(this.runnedSections.get(ctx.user) as RunnedSection[]).push(runnedSection);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (runnedSection) {
|
|
909
|
+
sectionInstance = runnedSection.instance;
|
|
910
|
+
} else {
|
|
911
|
+
throw new Error(`Failed to create or retrieve runned section for ${sectionId}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!(sectionInstance as any)[method]) {
|
|
915
|
+
throw new Error(`Method ${method} not found in section ${sectionId}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (params.cbBeforeRunMethod) {
|
|
919
|
+
this.debugLog("Executing callback before running all method for section:", sectionId);
|
|
920
|
+
await params.cbBeforeRunMethod(sectionInstance);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (sectionRoute.hasTriggers()) {
|
|
924
|
+
this.debugLog("Section route has triggers, executing them before running method:", sectionId);
|
|
925
|
+
sectionRoute.getTriggers().forEach((trigger) => {
|
|
926
|
+
if (trigger.name === 'cbBeforeRunMethod') {
|
|
927
|
+
this.debugLog(`Executing cbBeforeRunMethod trigger for section ${sectionId}`);
|
|
928
|
+
this.debugLog("Trigger details:", trigger);
|
|
929
|
+
|
|
930
|
+
trigger.cb(sectionInstance);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Run section methods in the following order:
|
|
937
|
+
* 1. setup (if section is installed)
|
|
938
|
+
* 2. up
|
|
939
|
+
* 3. method (action)
|
|
940
|
+
* 4. down (if section is installed)
|
|
941
|
+
* 5. unsetup (if section is installed and previous section is different)
|
|
942
|
+
*/
|
|
943
|
+
|
|
944
|
+
const setupMethod = sectionInstance.setup;
|
|
945
|
+
const upMethod = sectionInstance.up;
|
|
946
|
+
const downMethod = sectionInstance.down;
|
|
947
|
+
const unsetupMethod = sectionInstance.unsetup;
|
|
948
|
+
|
|
949
|
+
// Run setup if section is installed
|
|
950
|
+
if (createdNewSectionInstance && setupMethod && typeof setupMethod === "function") {
|
|
951
|
+
this.debugLog(`[Setup] Section ${sectionId} install for user ${ctx.user.username}`);
|
|
952
|
+
await sectionInstance.setup();
|
|
953
|
+
this.debugLog(`[Setup finish] Section ${sectionId} installed for user ${ctx.user.username}`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Run up method
|
|
957
|
+
if (upMethod && typeof upMethod === "function") {
|
|
958
|
+
this.debugLog(`[Up] Section ${sectionId} up for user ${ctx.user.username}`);
|
|
959
|
+
await sectionInstance.up();
|
|
960
|
+
this.debugLog(`[Up finish] Section ${sectionId} up for user ${ctx.user.username}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
await (sectionInstance as any)[method]();
|
|
965
|
+
} catch (error) {
|
|
966
|
+
this.debugLog(`[Error] Section ${sectionId} error for user ${ctx.user.username}:`, error);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Run down method if section is installed
|
|
970
|
+
const previousSection = (ctx.userSession.previousSection = runnedSection as RunnedSection);
|
|
971
|
+
|
|
972
|
+
if (downMethod && typeof downMethod === "function") {
|
|
973
|
+
this.debugLog(`[Down] Section ${sectionId} down for user ${ctx.user.username}`);
|
|
974
|
+
await sectionInstance.down();
|
|
975
|
+
this.debugLog(`[Down finish] Section ${sectionId} down for user ${ctx.user.username}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Run unsetup method if section is installed and previous section is different
|
|
979
|
+
if (previousSection && previousSection.constructor.name !== sectionInstance.constructor.name) {
|
|
980
|
+
this.debugLog(
|
|
981
|
+
`Previous section ${previousSection.constructor.name} is different from current section ${sectionInstance.constructor.name}`
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
if (unsetupMethod && typeof unsetupMethod === "function") {
|
|
985
|
+
this.debugLog(
|
|
986
|
+
`[Unsetup] Section ${previousSection.instance.constructor.name} unsetup for user ${ctx.user.username}`
|
|
987
|
+
);
|
|
988
|
+
await previousSection.instance.unsetup();
|
|
989
|
+
this.debugLog(
|
|
990
|
+
`[Unsetup finish] Section ${previousSection.instance.constructor.name} unsetup for user ${ctx.user.username}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
getRunnedSection(user: UserModel): RunnedSection | Map<string, RunnedSection> {
|
|
997
|
+
const section = this.runnedSections.get(user);
|
|
998
|
+
|
|
999
|
+
if (!section) {
|
|
1000
|
+
throw new Error("Section not found");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return section;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async registerUser(data: UserRegistrationData): Promise<UserModel | null> {
|
|
1007
|
+
try {
|
|
1008
|
+
const user = await UserModel.register(data);
|
|
1009
|
+
|
|
1010
|
+
if (this.config.userStorage) {
|
|
1011
|
+
this.config.userStorage.add(data.tg_username, user);
|
|
1012
|
+
this.debugLog("User added to storage:", data.tg_username);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return user;
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.error("User registration error:", error);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Runs a task with bidirectional communication support
|
|
1024
|
+
* @param ctx Telegram context
|
|
1025
|
+
* @param task Function that performs the task with message handlers
|
|
1026
|
+
* @param options Configuration options for the task
|
|
1027
|
+
* @returns Task controller object with methods for communication and control
|
|
1028
|
+
*/
|
|
1029
|
+
runTask(
|
|
1030
|
+
ctx: Telegraf2byteContext,
|
|
1031
|
+
task: (controller: {
|
|
1032
|
+
signal: AbortSignal;
|
|
1033
|
+
sendMessage: (message: string) => Promise<void>;
|
|
1034
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
|
|
1035
|
+
}) => Promise<any>,
|
|
1036
|
+
options: {
|
|
1037
|
+
taskId?: string;
|
|
1038
|
+
notifyStart?: boolean;
|
|
1039
|
+
notifyComplete?: boolean;
|
|
1040
|
+
startMessage?: string;
|
|
1041
|
+
completeMessage?: string;
|
|
1042
|
+
errorMessage?: string;
|
|
1043
|
+
silent?: boolean;
|
|
1044
|
+
} = {}
|
|
1045
|
+
) {
|
|
1046
|
+
const {
|
|
1047
|
+
taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1048
|
+
notifyStart = true,
|
|
1049
|
+
notifyComplete = true,
|
|
1050
|
+
startMessage = "Задача запущена и будет выполняться в фоновом режиме.",
|
|
1051
|
+
completeMessage = "Задача успешно завершена!",
|
|
1052
|
+
errorMessage = "Произошла ошибка при выполнении задачи.",
|
|
1053
|
+
silent = false,
|
|
1054
|
+
} = options;
|
|
1055
|
+
|
|
1056
|
+
// Create abort controller for task cancellation
|
|
1057
|
+
const abortController = new AbortController();
|
|
1058
|
+
|
|
1059
|
+
// Message handling setup
|
|
1060
|
+
const messageHandlers: ((message: string, source: "task" | "external") => void)[] = [];
|
|
1061
|
+
const messageQueue: Array<{ message: string; source: "task" | "external" }> = [];
|
|
1062
|
+
|
|
1063
|
+
// Create task controller interface
|
|
1064
|
+
const taskController = {
|
|
1065
|
+
signal: abortController.signal,
|
|
1066
|
+
// Send message from task to handlers
|
|
1067
|
+
sendMessage: async (message: string) => {
|
|
1068
|
+
if (!silent) {
|
|
1069
|
+
await ctx.reply(`[Задача ${taskId}]: ${message}`).catch(console.error);
|
|
1070
|
+
}
|
|
1071
|
+
messageQueue.push({ message, source: "task" });
|
|
1072
|
+
messageHandlers.forEach((handler) => handler(message, "task"));
|
|
1073
|
+
},
|
|
1074
|
+
// Handle incoming messages to task
|
|
1075
|
+
onMessage: (handler: (message: string, source: "task" | "external") => void) => {
|
|
1076
|
+
messageHandlers.push(handler);
|
|
1077
|
+
// Process any queued messages
|
|
1078
|
+
messageQueue.forEach(({ message, source }) => handler(message, source));
|
|
1079
|
+
},
|
|
1080
|
+
// Receive message from external source
|
|
1081
|
+
receiveMessage: async (message: string) => {
|
|
1082
|
+
messageQueue.push({ message, source: "external" });
|
|
1083
|
+
messageHandlers.forEach((handler) => handler(message, "external"));
|
|
1084
|
+
if (!silent) {
|
|
1085
|
+
await ctx
|
|
1086
|
+
.reply(`[Внешнее сообщение для задачи ${taskId}]: ${message}`)
|
|
1087
|
+
.catch(console.error);
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// Send start notification if enabled
|
|
1093
|
+
if (notifyStart && !silent) {
|
|
1094
|
+
ctx.reply(startMessage).catch(console.error);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Create and run task promise
|
|
1098
|
+
const taskPromise = Promise.resolve().then(() => {
|
|
1099
|
+
return task(taskController);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// Save task information
|
|
1103
|
+
this.runningTasks.set(taskId, {
|
|
1104
|
+
task: taskPromise,
|
|
1105
|
+
cancel: () => abortController.abort(),
|
|
1106
|
+
status: "running",
|
|
1107
|
+
startTime: Date.now(),
|
|
1108
|
+
ctx,
|
|
1109
|
+
controller: taskController,
|
|
1110
|
+
messageQueue,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// Handle task completion and errors
|
|
1114
|
+
taskPromise
|
|
1115
|
+
.then((result) => {
|
|
1116
|
+
const taskInfo = this.runningTasks.get(taskId);
|
|
1117
|
+
if (taskInfo) {
|
|
1118
|
+
taskInfo.status = "completed";
|
|
1119
|
+
taskInfo.endTime = Date.now();
|
|
1120
|
+
if (notifyComplete && !silent) {
|
|
1121
|
+
ctx.reply(completeMessage).catch(console.error);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return result;
|
|
1125
|
+
})
|
|
1126
|
+
.catch((error) => {
|
|
1127
|
+
const taskInfo = this.runningTasks.get(taskId);
|
|
1128
|
+
if (taskInfo) {
|
|
1129
|
+
taskInfo.status = error.name === "AbortError" ? "cancelled" : "failed";
|
|
1130
|
+
taskInfo.endTime = Date.now();
|
|
1131
|
+
taskInfo.error = error;
|
|
1132
|
+
|
|
1133
|
+
if (error.name !== "AbortError" && !silent) {
|
|
1134
|
+
console.error("Task error:", error);
|
|
1135
|
+
ctx.reply(`${errorMessage}\nОшибка: ${error.message}`).catch(console.error);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
return taskId;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Get information about a running task
|
|
1145
|
+
* @param taskId The ID of the task to check
|
|
1146
|
+
*/
|
|
1147
|
+
getTaskInfo(taskId: string) {
|
|
1148
|
+
return this.runningTasks.get(taskId);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Cancel a running task
|
|
1153
|
+
* @param taskId The ID of the task to cancel
|
|
1154
|
+
* @returns true if the task was cancelled, false if it couldn't be cancelled
|
|
1155
|
+
*/
|
|
1156
|
+
cancelTask(taskId: string): boolean {
|
|
1157
|
+
const taskInfo = this.runningTasks.get(taskId);
|
|
1158
|
+
if (taskInfo && taskInfo.cancel && taskInfo.status === "running") {
|
|
1159
|
+
taskInfo.cancel();
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Send a message to a running task
|
|
1167
|
+
* @param taskId The ID of the task to send the message to
|
|
1168
|
+
* @param message The message to send
|
|
1169
|
+
* @returns true if the message was sent, false if the task wasn't found or isn't running
|
|
1170
|
+
*/
|
|
1171
|
+
async sendMessageToTask(taskId: string, message: string): Promise<boolean> {
|
|
1172
|
+
const taskInfo = this.runningTasks.get(taskId);
|
|
1173
|
+
if (taskInfo && taskInfo.controller && taskInfo.status === "running") {
|
|
1174
|
+
await taskInfo.controller.receiveMessage(message);
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Get all tasks for a specific user
|
|
1182
|
+
* @param userId Telegram user ID
|
|
1183
|
+
*/
|
|
1184
|
+
getUserTasks(
|
|
1185
|
+
userId: number
|
|
1186
|
+
): Array<{ taskId: string; status: string; startTime: number; endTime?: number }> {
|
|
1187
|
+
const tasks: Array<{ taskId: string; status: string; startTime: number; endTime?: number }> =
|
|
1188
|
+
[];
|
|
1189
|
+
|
|
1190
|
+
for (const [taskId, taskInfo] of this.runningTasks) {
|
|
1191
|
+
if (taskInfo.ctx.from?.id === userId) {
|
|
1192
|
+
tasks.push({
|
|
1193
|
+
taskId,
|
|
1194
|
+
status: taskInfo.status,
|
|
1195
|
+
startTime: taskInfo.startTime,
|
|
1196
|
+
endTime: taskInfo.endTime,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return tasks;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Clean up completed/failed/cancelled tasks older than the specified age
|
|
1206
|
+
* @param maxAge Maximum age in milliseconds (default: 1 hour)
|
|
1207
|
+
*/
|
|
1208
|
+
cleanupOldTasks(maxAge: number = 3600000): void {
|
|
1209
|
+
const now = Date.now();
|
|
1210
|
+
for (const [taskId, taskInfo] of this.runningTasks) {
|
|
1211
|
+
if (taskInfo.status !== "running" && taskInfo.endTime && now - taskInfo.endTime > maxAge) {
|
|
1212
|
+
this.runningTasks.delete(taskId);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private getTgUsername(ctx: Telegraf2byteContext): string {
|
|
1218
|
+
return ctx.from?.username || "";
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private getTgName(ctx: Telegraf2byteContext): string {
|
|
1222
|
+
return ctx.from?.first_name || "";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private getTgId(ctx: Telegraf2byteContext): number {
|
|
1226
|
+
return ctx.from?.id || 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
debugLog(...args: any[]): void {
|
|
1230
|
+
if (this.config.debug) {
|
|
1231
|
+
console.log(...args);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
get sections(): SectionList {
|
|
1236
|
+
return this.config.sections;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
get configApp(): AppConfig {
|
|
1240
|
+
return this.config;
|
|
1241
|
+
}
|
|
1242
|
+
}
|