@2byte/tgbot-framework 1.0.2 → 1.0.4

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 (36) hide show
  1. package/bin/2byte-cli.ts +13 -0
  2. package/package.json +6 -1
  3. package/src/cli/GenerateCommand.ts +93 -9
  4. package/src/cli/TgAccountManager.ts +50 -0
  5. package/src/core/ApiService.ts +21 -0
  6. package/src/core/ApiServiceManager.ts +63 -0
  7. package/src/core/App.ts +133 -32
  8. package/src/illumination/InlineKeyboard.ts +2 -1
  9. package/src/illumination/Message2Byte.ts +2 -1
  10. package/src/illumination/Message2ByteLiveProgressive.ts +2 -2
  11. package/src/illumination/Section.ts +1 -1
  12. package/src/index.ts +9 -0
  13. package/src/libs/TelegramAccountControl.ts +409 -7
  14. package/src/libs/TgSender.ts +53 -0
  15. package/src/models/Model.ts +67 -0
  16. package/src/models/Proxy.ts +218 -0
  17. package/src/models/TgAccount.ts +362 -0
  18. package/src/models/index.ts +3 -0
  19. package/src/types.ts +6 -1
  20. package/src/user/UserModel.ts +9 -0
  21. package/src/workflow/services/MassSendApiService.ts +80 -0
  22. package/templates/bot/.env.example +6 -1
  23. package/templates/bot/bot.ts +6 -1
  24. package/templates/bot/database/migrations/007_proxy.sql +27 -0
  25. package/templates/bot/database/migrations/008_tg_accounts.sql +32 -0
  26. package/templates/bot/docs/CLI_SERVICES.md +536 -0
  27. package/templates/bot/docs/INPUT_SYSTEM.md +211 -0
  28. package/templates/bot/docs/SERVICE_EXAMPLES.md +384 -0
  29. package/templates/bot/docs/TASK_SYSTEM.md +156 -0
  30. package/templates/bot/models/Model.ts +7 -0
  31. package/templates/bot/models/index.ts +2 -0
  32. package/templates/bot/sectionList.ts +4 -2
  33. package/templates/bot/sections/ExampleInputSection.ts +85 -0
  34. package/templates/bot/sections/ExampleLiveTaskerSection.ts +60 -0
  35. package/templates/bot/sections/HomeSection.ts +10 -10
  36. package/templates/bot/workflow/services/ExampleService.ts +24 -0
package/bin/2byte-cli.ts CHANGED
@@ -63,6 +63,19 @@ generate
63
63
  }
64
64
  });
65
65
 
66
+ generate
67
+ .command('service <name>')
68
+ .description('Generate a new API service')
69
+ .action(async (name) => {
70
+ try {
71
+ const command = new GenerateCommand();
72
+ await command.generateService(name);
73
+ } catch (error) {
74
+ console.error(chalk.red('❌ Error:'), error.message);
75
+ process.exit(1);
76
+ }
77
+ });
78
+
66
79
  generate
67
80
  .command('migration <name>')
68
81
  .description('Generate a new migration')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2byte/tgbot-framework",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A TypeScript framework for creating Telegram bots with sections-based architecture (Bun optimized)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -29,7 +29,11 @@
29
29
  "author": "2byte",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
+ "chalk": "^5.6.2",
33
+ "commander": "^14.0.2",
32
34
  "dotenv": "^16.6.1",
35
+ "fs-extra": "^11.3.2",
36
+ "input": "^1.0.1",
33
37
  "inquirer": "^12.9.6",
34
38
  "mustache": "^4.2.0",
35
39
  "socks": "^2.8.6",
@@ -37,6 +41,7 @@
37
41
  "telegram": "^2.26.22"
38
42
  },
39
43
  "devDependencies": {
44
+ "@types/bun": "^1.3.1",
40
45
  "@types/node": "^20.19.8",
41
46
  "bun-types": "^1.2.18",
42
47
  "typescript": "^5.8.3"
@@ -27,6 +27,31 @@ export class GenerateCommand {
27
27
  console.log(chalk.green(`✅ Created section ${sectionName} at ${sectionPath}`));
28
28
  }
29
29
 
30
+ async generateService(name: string): Promise<void> {
31
+ console.log(chalk.blue(`⚙️ Generating service: ${name}`));
32
+
33
+ const currentDir = process.cwd();
34
+ const servicesDir = path.join(currentDir, 'workflow', 'services');
35
+ const serviceName = this.formatServiceName(name);
36
+ const servicePath = path.join(servicesDir, `${serviceName}.ts`);
37
+
38
+ // Ensure services directory exists
39
+ await fs.ensureDir(servicesDir);
40
+
41
+ // Check if service already exists
42
+ if (await fs.pathExists(servicePath)) {
43
+ console.log(chalk.red(`❌ Service ${serviceName} already exists at ${servicePath}`));
44
+ return;
45
+ }
46
+
47
+ // Generate service content
48
+ const template = this.getServiceTemplate(serviceName);
49
+ await fs.writeFile(servicePath, template);
50
+
51
+ console.log(chalk.green(`✅ Created service ${serviceName} at ${servicePath}`));
52
+ console.log(chalk.yellow(`💡 Service will be automatically loaded from workflow/services directory`));
53
+ }
54
+
30
55
  async generateMigration(name: string): Promise<void> {
31
56
  console.log(chalk.blue(`🗃️ Generating migration: ${name}`));
32
57
 
@@ -54,30 +79,34 @@ export class GenerateCommand {
54
79
  }
55
80
 
56
81
  private formatSectionName(name: string): string {
57
- return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
82
+ return name.charAt(0).toUpperCase() + name.slice(1);
83
+ }
84
+
85
+ private formatServiceName(name: string): string {
86
+ // Convert to PascalCase and add "Service" suffix if not present
87
+ const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
88
+ return pascalName.endsWith('Service') ? pascalName : `${pascalName}Service`;
58
89
  }
59
90
 
60
91
  private getSectionTemplate(name: string): string {
61
- return `import { Section } from "2bytetgbot";
62
- import { SectionOptions } from "2bytetgbot";
63
- import { InlineKeyboard } from "2bytetgbot";
92
+ return `import { Section } from "@2byte/tgbot-framework";
93
+ import { SectionOptions } from "@2byte/tgbot-framework";
94
+ import { InlineKeyboard } from "@2byte/tgbot-framework";
64
95
 
65
96
  export default class ${name}Section extends Section {
66
97
  static command = "${name.toLowerCase()}";
67
98
  static description = "${name} section";
68
99
  static actionRoutes = {
69
- "${name.toLowerCase()}.index": "index",
100
+ "${name}.index": "index",
70
101
  };
71
102
 
72
- public sectionId = "${name.toLowerCase()}";
103
+ public sectionId = "${name}";
73
104
  private mainInlineKeyboard: InlineKeyboard;
74
105
 
75
106
  constructor(options: SectionOptions) {
76
107
  super(options);
77
108
 
78
- this.mainInlineKeyboard = this.makeInlineKeyboard([
79
- [this.makeInlineButton("🏠 На главную", "home.index")],
80
- ]);
109
+ this.mainInlineKeyboard = this.makeInlineKeyboard().addFootFixedButtons(this.btnHome);
81
110
  }
82
111
 
83
112
  public async up(): Promise<void> {}
@@ -98,6 +127,61 @@ export default class ${name}Section extends Section {
98
127
  `;
99
128
  }
100
129
 
130
+ private getServiceTemplate(name: string): string {
131
+ return `import { App } from "@2byte/tgbot-framework";
132
+ import { ApiService } from "@2byte/tgbot-framework";
133
+
134
+ export default class ${name} extends ApiService {
135
+
136
+ constructor(
137
+ protected app: App,
138
+ public name: string = "${name}"
139
+ ) {
140
+ super(app, name);
141
+ }
142
+
143
+ /**
144
+ * Setup method called when service is registered
145
+ * Use this for initialization tasks like setting up connections,
146
+ * loading configurations, etc.
147
+ */
148
+ public async setup(): Promise<void> {
149
+ // TODO: Add setup logic here
150
+ this.app.debugLog(\`[\${this.name}] Service setup completed\`);
151
+ return Promise.resolve();
152
+ }
153
+
154
+ /**
155
+ * Cleanup method called when service is being destroyed
156
+ * Use this for cleanup tasks like closing connections,
157
+ * releasing resources, etc.
158
+ */
159
+ public async unsetup(): Promise<void> {
160
+ // TODO: Add cleanup logic here
161
+ this.app.debugLog(\`[\${this.name}] Service cleanup completed\`);
162
+ return Promise.resolve();
163
+ }
164
+
165
+ /**
166
+ * Main run method for the service
167
+ * This is where your service's main logic should be implemented
168
+ */
169
+ public async run(): Promise<void> {
170
+ // TODO: Add your service logic here
171
+ this.app.debugLog(\`[\${this.name}] Service running\`);
172
+ return Promise.resolve();
173
+ }
174
+
175
+ /**
176
+ * Example method - you can add your own methods here
177
+ */
178
+ // public async exampleMethod(): Promise<void> {
179
+ // // Your custom logic
180
+ // }
181
+ }
182
+ `;
183
+ }
184
+
101
185
  private getMigrationTemplate(name: string): string {
102
186
  return `-- UP
103
187
  CREATE TABLE IF NOT EXISTS ${name} (
@@ -0,0 +1,50 @@
1
+ import {
2
+ TelegramManagerCredentialsDB,
3
+ TelegramAccountRemote,
4
+ } from "../libs/TelegramAccountControl";
5
+ import { Model } from "../models/Model";
6
+ import Input from "input";
7
+
8
+ export const manualAdderTgAccount = async () => {
9
+ const credentialsManager = new TelegramManagerCredentialsDB(Model.getConnection());
10
+
11
+ const tgAccountControl = TelegramAccountRemote.init({
12
+ appId: process.env.TG_APP_ID!,
13
+ appHash: process.env.TG_APP_HASH!,
14
+ credetialsManager: credentialsManager,
15
+ });
16
+
17
+ const phone = await Input.text("Введите номер телефона (с кодом страны, например, 79614416445):");
18
+
19
+ await credentialsManager.addCredential({
20
+ phone,
21
+ });
22
+
23
+ const credentials = await credentialsManager.getCredential(
24
+ phone
25
+ );
26
+
27
+ if (!credentials) {
28
+ console.log("Учётная запись с таким номером телефона не найдена.");
29
+ return;
30
+ }
31
+
32
+ await tgAccountControl.login(
33
+ credentials,
34
+ async () => {
35
+ console.log("Требуется код подтверждения");
36
+ return await Input.text("Введите код подтверждения:");
37
+ },
38
+ async () => {
39
+ console.log("Требуется пароль");
40
+ return await Input.password("Введите пароль:");
41
+ },
42
+ async (err: any) => {
43
+ console.log("Ошибка логина:", err);
44
+ tgAccountControl.disconnect();
45
+ throw new Error(err);
46
+ }
47
+ );
48
+
49
+ tgAccountControl.disconnect();
50
+ };
@@ -0,0 +1,21 @@
1
+ import { App } from "./App";
2
+
3
+ export abstract class ApiService {
4
+
5
+ constructor(
6
+ protected app: App,
7
+ public name: string
8
+ ) {}
9
+
10
+ async setup(): Promise<void> {
11
+ // Implement your API logic here
12
+ }
13
+
14
+ async unsetup(): Promise<void> {
15
+ // Implement your API logic here
16
+ }
17
+
18
+ async run(): Promise<void> {
19
+ // Implement your API logic here
20
+ }
21
+ }
@@ -0,0 +1,63 @@
1
+ import { App } from "./App";
2
+ import { ApiService } from "./ApiService";
3
+ import { readdirSync } from "fs";
4
+
5
+ export class ApiServiceManager {
6
+ private services: Map<string, ApiService> = new Map();
7
+
8
+ constructor(private app: App) {}
9
+
10
+ static init(app: App): ApiServiceManager {
11
+ return new ApiServiceManager(app);
12
+ }
13
+
14
+ async loadServicesFromDirectory(pathDirectory: string): Promise<void> {
15
+ for (const entry of readdirSync(pathDirectory, { withFileTypes: true })) {
16
+ if (entry.isFile() && entry.name.endsWith(".ts")) {
17
+ const serviceModule = await import(`${pathDirectory}/${entry.name}`);
18
+ const ServiceClass = serviceModule.default;
19
+ const serviceInstance = new ServiceClass(this.app);
20
+ this.registerService(entry.name.replace(".ts", ""), serviceInstance);
21
+ }
22
+ }
23
+ }
24
+
25
+ public registerService(name: string, service: ApiService): void {
26
+ this.services.set(name, service);
27
+ }
28
+
29
+ public getService(name: string): ApiService | undefined {
30
+ return this.services.get(name);
31
+ }
32
+
33
+ public async setupService(name: string): Promise<void> {
34
+ const service = this.getService(name);
35
+ if (service) {
36
+ await service.setup();
37
+ }
38
+ }
39
+
40
+ public async unsetupService(name: string): Promise<void> {
41
+ const service = this.getService(name);
42
+ if (service) {
43
+ await service.unsetup();
44
+ }
45
+ }
46
+
47
+ public async runService(name: string): Promise<void> {
48
+ const service = this.getService(name);
49
+ if (service) {
50
+ await service.run();
51
+ }
52
+ }
53
+
54
+ public getAll(): Map<string, ApiService> {
55
+ return this.services;
56
+ }
57
+
58
+ public async unsetupAllServices(): Promise<void> {
59
+ for (const [name, service] of this.services) {
60
+ await service.unsetup();
61
+ }
62
+ }
63
+ }
package/src/core/App.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { Telegraf, Markup } from "telegraf";
2
2
  import path from "node:path";
3
- import { Telegraf2byteContext, Telegraf2byteContextExtraMethods } from "../illumination/Telegraf2byteContext";
3
+ import { access } from "fs/promises";
4
+ import {
5
+ Telegraf2byteContext,
6
+ Telegraf2byteContextExtraMethods,
7
+ } from "../illumination/Telegraf2byteContext";
4
8
  import { Section } from "../illumination/Section";
5
9
  import { RunSectionRoute } from "../illumination/RunSectionRoute";
6
10
  import { UserModel } from "../user/UserModel";
@@ -15,6 +19,7 @@ import {
15
19
  UserRegistrationData,
16
20
  } from "../types";
17
21
  import { nameToCapitalize } from "./utils";
22
+ import { ApiServiceManager } from "./ApiServiceManager";
18
23
 
19
24
  export class App {
20
25
  private config: AppConfig = {
@@ -36,13 +41,15 @@ export class App {
36
41
  terminateSigInt: true,
37
42
  terminateSigTerm: true,
38
43
  keepSectionInstances: false,
44
+ botCwd: process.cwd(),
39
45
  };
40
46
 
41
- private bot!: Telegraf<Telegraf2byteContext>;
47
+ public bot!: Telegraf<Telegraf2byteContext>;
42
48
  private sectionClasses: Map<string, typeof Section> = new Map();
43
49
  private runnedSections: WeakMap<UserModel, RunnedSection | Map<string, RunnedSection>> =
44
50
  new WeakMap();
45
51
  private middlewares: CallableFunction[] = [];
52
+ private apiServiceManager!: ApiServiceManager;
46
53
 
47
54
  // Система управления фоновыми задачами
48
55
  private runningTasks: Map<
@@ -58,10 +65,10 @@ export class App {
58
65
  controller?: {
59
66
  signal: AbortSignal;
60
67
  sendMessage: (message: string) => Promise<void>;
61
- onMessage: (handler: (message: string, source: 'task' | 'external') => void) => void;
68
+ onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
62
69
  receiveMessage: (message: string) => Promise<void>;
63
70
  };
64
- messageQueue?: Array<{ message: string; source: 'task' | 'external' }>;
71
+ messageQueue?: Array<{ message: string; source: "task" | "external" }>;
65
72
  }
66
73
  > = new Map();
67
74
 
@@ -169,6 +176,11 @@ export class App {
169
176
  return this;
170
177
  }
171
178
 
179
+ botCwd(cwdPath: string): this {
180
+ this.app.config.botCwd = cwdPath;
181
+ return this;
182
+ }
183
+
172
184
  build(): App {
173
185
  return this.app;
174
186
  }
@@ -197,6 +209,7 @@ export class App {
197
209
  this.registerHears();
198
210
  this.registerCommands();
199
211
  this.registerMessageHandlers();
212
+ await this.registerServices();
200
213
 
201
214
  return this;
202
215
  }
@@ -241,22 +254,52 @@ export class App {
241
254
  if (!this.config.userStorage) {
242
255
  throw new Error("User storage is not set");
243
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
+ }
244
265
 
266
+ // Check access by username and register user if not exists
245
267
  if (!this.config.userStorage.exists(tgUsername)) {
246
- if (!this.config.accessPublic) {
268
+ const isAuthByUsername = !this.config.accessPublic && !accessKey;
269
+
270
+ // check access by username for private bots
271
+ if (isAuthByUsername) {
247
272
  const requestUsername = this.getTgUsername(ctx);
248
273
  this.debugLog("Private access mode. Checking username:", requestUsername);
249
- const checkAccess = this.config.envConfig.ACCESS_USERNAMES && this.config.envConfig.ACCESS_USERNAMES.split(",").map(name => name.trim());
250
- if (checkAccess && checkAccess.every(name => name !== 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
+ ) {
251
281
  return ctx.reply("Access denied. Your username is not in the access list.");
252
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.");
253
296
  }
254
297
 
255
298
  if (!ctx.from) {
256
299
  return ctx.reply("User information is not available");
257
300
  }
258
301
 
259
- const userRefIdFromStart = ctx.startPayload ? parseInt(ctx.startPayload) : 0;
302
+ const userRefIdFromStart = startPayload ? parseInt(startPayload) : 0;
260
303
 
261
304
  await this.registerUser({
262
305
  user_refid: userRefIdFromStart,
@@ -264,9 +307,9 @@ export class App {
264
307
  tg_username: tgUsername,
265
308
  tg_first_name: ctx.from.first_name || tgUsername,
266
309
  tg_last_name: ctx.from.last_name || "",
267
- role: 'user',
310
+ role: "user",
268
311
  language: ctx.from.language_code || "en",
269
- })
312
+ });
270
313
  }
271
314
 
272
315
  ctx.user = this.config.userStorage.find(tgUsername);
@@ -312,7 +355,7 @@ export class App {
312
355
  const sectionId = actionPathParts[0];
313
356
 
314
357
  let sectionClass = this.sectionClasses.get(sectionId);
315
-
358
+
316
359
  if (!sectionClass) {
317
360
  throw new Error(`Section class not found for sectionId ${sectionId}`);
318
361
  }
@@ -320,7 +363,9 @@ export class App {
320
363
  const method = sectionClass.actionRoutes[actionPath];
321
364
 
322
365
  if (!method) {
323
- throw new Error(`Method ${actionPath} not found in section ${sectionId}`);
366
+ throw new Error(
367
+ `Action ${actionPath} method ${method} not found in section ${sectionId}`
368
+ );
324
369
  }
325
370
 
326
371
  const sectionRoute = new RunSectionRoute()
@@ -339,10 +384,9 @@ export class App {
339
384
  // Register hears
340
385
  Object.entries(this.config.hears).forEach(([key, sectionMethod]) => {
341
386
  this.bot.hears(key, async (ctx: Telegraf2byteContext) => {
342
-
343
387
  const [sectionId, method] = sectionMethod.split(".");
344
388
  const sectionRoute = new RunSectionRoute().section(sectionId).method(method).hearsKey(key);
345
-
389
+
346
390
  this.debugLog(`Hears matched: ${key}, running section ${sectionId}, method ${method}`);
347
391
 
348
392
  this.runSection(ctx, sectionRoute).catch((err) => {
@@ -381,6 +425,48 @@ export class App {
381
425
  });
382
426
  }
383
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
+
384
470
  private async handleUserInput(
385
471
  ctx: Telegraf2byteContext,
386
472
  inputValue: any,
@@ -417,6 +503,7 @@ export class App {
417
503
  delete ctx.userSession.awaitingInputPromise;
418
504
  // Разрешаем Promise
419
505
  resolve(inputValue);
506
+ ctx.deleteLastMessage();
420
507
  } else {
421
508
  // Увеличиваем счетчик попыток
422
509
  awaitingPromise.retryCount = retryCount + 1;
@@ -628,13 +715,20 @@ export class App {
628
715
  let pathSectionModule =
629
716
  sectionParams.pathModule ??
630
717
  path.join(process.cwd(), "./sections/" + nameToCapitalize(sectionId) + "Section");
631
-
632
- this.debugLog('Path to section module: ', pathSectionModule);
633
-
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
+
634
728
  if (freshVersion) {
635
729
  pathSectionModule += "?update=" + Date.now();
636
730
  }
637
-
731
+
638
732
  const sectionClass = (await import(pathSectionModule)).default;
639
733
 
640
734
  this.debugLog("Loaded section", sectionId);
@@ -650,8 +744,10 @@ export class App {
650
744
  try {
651
745
  this.sectionClasses.set(sectionId, await this.loadSection(sectionId));
652
746
  } catch (err) {
653
- this.debugLog('Error stack:', err instanceof Error ? err.stack : 'No stack available');
654
- throw new Error(`Failed to load section ${sectionId}: ${err instanceof Error ? err.message : 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
+ );
655
751
  }
656
752
  }
657
753
  }
@@ -752,7 +848,9 @@ export class App {
752
848
  if (sectionInstalled) {
753
849
  this.debugLog(`[Setup] Section ${sectionId} install for user ${ctx.user.username}`);
754
850
  await sectionInstance.setup();
755
- this.debugLog(`[Setup finish] Section ${sectionId} installed for user ${ctx.user.username}`);
851
+ this.debugLog(
852
+ `[Setup finish] Section ${sectionId} installed for user ${ctx.user.username}`
853
+ );
756
854
  }
757
855
  }
758
856
 
@@ -812,6 +910,7 @@ export class App {
812
910
 
813
911
  if (this.config.userStorage) {
814
912
  this.config.userStorage.add(data.tg_username, user);
913
+ this.debugLog('User added to storage:', data.tg_username);
815
914
  }
816
915
 
817
916
  return user;
@@ -833,7 +932,7 @@ export class App {
833
932
  task: (controller: {
834
933
  signal: AbortSignal;
835
934
  sendMessage: (message: string) => Promise<void>;
836
- onMessage: (handler: (message: string, source: 'task' | 'external') => void) => void;
935
+ onMessage: (handler: (message: string, source: "task" | "external") => void) => void;
837
936
  }) => Promise<any>,
838
937
  options: {
839
938
  taskId?: string;
@@ -859,8 +958,8 @@ export class App {
859
958
  const abortController = new AbortController();
860
959
 
861
960
  // Message handling setup
862
- const messageHandlers: ((message: string, source: 'task' | 'external') => void)[] = [];
863
- const messageQueue: Array<{ message: string; source: 'task' | 'external' }> = [];
961
+ const messageHandlers: ((message: string, source: "task" | "external") => void)[] = [];
962
+ const messageQueue: Array<{ message: string; source: "task" | "external" }> = [];
864
963
 
865
964
  // Create task controller interface
866
965
  const taskController = {
@@ -870,23 +969,25 @@ export class App {
870
969
  if (!silent) {
871
970
  await ctx.reply(`[Задача ${taskId}]: ${message}`).catch(console.error);
872
971
  }
873
- messageQueue.push({ message, source: 'task' });
874
- messageHandlers.forEach(handler => handler(message, 'task'));
972
+ messageQueue.push({ message, source: "task" });
973
+ messageHandlers.forEach((handler) => handler(message, "task"));
875
974
  },
876
975
  // Handle incoming messages to task
877
- onMessage: (handler: (message: string, source: 'task' | 'external') => void) => {
976
+ onMessage: (handler: (message: string, source: "task" | "external") => void) => {
878
977
  messageHandlers.push(handler);
879
978
  // Process any queued messages
880
979
  messageQueue.forEach(({ message, source }) => handler(message, source));
881
980
  },
882
981
  // Receive message from external source
883
982
  receiveMessage: async (message: string) => {
884
- messageQueue.push({ message, source: 'external' });
885
- messageHandlers.forEach(handler => handler(message, 'external'));
983
+ messageQueue.push({ message, source: "external" });
984
+ messageHandlers.forEach((handler) => handler(message, "external"));
886
985
  if (!silent) {
887
- await ctx.reply(`[Внешнее сообщение для задачи ${taskId}]: ${message}`).catch(console.error);
986
+ await ctx
987
+ .reply(`[Внешнее сообщение для задачи ${taskId}]: ${message}`)
988
+ .catch(console.error);
888
989
  }
889
- }
990
+ },
890
991
  };
891
992
 
892
993
  // Send start notification if enabled
@@ -907,7 +1008,7 @@ export class App {
907
1008
  startTime: Date.now(),
908
1009
  ctx,
909
1010
  controller: taskController,
910
- messageQueue
1011
+ messageQueue,
911
1012
  });
912
1013
 
913
1014
  // Handle task completion and errors
@@ -22,7 +22,7 @@ export class InlineKeyboard {
22
22
  return this;
23
23
  }
24
24
 
25
- append(row: any[] | any[][]): InlineKeyboard {
25
+ append(row: any[] | any[][] | any): InlineKeyboard {
26
26
  if (!Array.isArray(row)) {
27
27
  this.keyboard.push([row]);
28
28
  } else if (Array.isArray(row[0])) {
@@ -51,6 +51,7 @@ export class InlineKeyboard {
51
51
  keyboard.push(...this.footFixedButtons);
52
52
  }
53
53
 
54
+ this.keyboard = [];
54
55
  return keyboard;
55
56
  }
56
57