@2byte/tgbot-framework 1.0.3 → 1.0.5

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