@2byte/tgbot-framework 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2byte/tgbot-framework",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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",
package/src/core/App.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import { Section } from "../illumination/Section";
9
9
  import { RunSectionRoute } from "../illumination/RunSectionRoute";
10
10
  import { UserModel } from "../user/UserModel";
11
+ import { AccessKey } from "../models/AccessKey";
11
12
  import { UserStore } from "../user/UserStore";
12
13
  import {
13
14
  AppConfig,
@@ -278,11 +279,21 @@ export class App {
278
279
  ? startPayload.split("key=")[1] || null
279
280
  : null;
280
281
  }
281
-
282
+ this.debugLog(
283
+ "exists storage",
284
+ this.config.userStorage.exists(tgUsername),
285
+ "remeber:",
286
+ this.rememberUser(tgUsername)
287
+ );
282
288
  // Check access by username and register user if not exists
283
289
  if (!this.config.userStorage.exists(tgUsername) && !this.rememberUser(tgUsername)) {
284
290
  const isAuthByUsername = !this.config.accessPublic && !accessKey;
285
-
291
+ this.debugLog(
292
+ "Access control check. isAuthByUsername:",
293
+ isAuthByUsername,
294
+ "accessKey in start payload:",
295
+ accessKey
296
+ );
286
297
  // check access by username for private bots
287
298
  if (isAuthByUsername) {
288
299
  const requestUsername = this.getTgUsername(ctx);
@@ -306,9 +317,14 @@ export class App {
306
317
  const accessKeys =
307
318
  this.config.envConfig.BOT_ACCESS_KEYS &&
308
319
  this.config.envConfig.BOT_ACCESS_KEYS.split(",").map((key) => key.trim());
320
+
321
+ AccessKey.setDatabase(globalThis.db);
322
+ const checkTempKey = AccessKey.checkKeyValid(accessKey);
323
+
309
324
  if (
310
325
  accessKeys &&
311
- accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase())
326
+ accessKeys.every((key) => key.toLowerCase() !== accessKey?.toLowerCase()) &&
327
+ !checkTempKey
312
328
  ) {
313
329
  return ctx.reply("Access denied. Your access key is not valid.");
314
330
  }
@@ -321,15 +337,18 @@ export class App {
321
337
 
322
338
  const userRefIdFromStart = startPayload ? parseInt(startPayload) : 0;
323
339
 
324
- await this.registerUser({
325
- user_refid: userRefIdFromStart,
326
- tg_id: ctx.from.id,
327
- tg_username: tgUsername,
328
- tg_first_name: ctx.from.first_name || tgUsername,
329
- tg_last_name: ctx.from.last_name || "",
330
- role: "user",
331
- language: ctx.from.language_code || "en",
332
- });
340
+ await this.registerUser(
341
+ {
342
+ user_refid: userRefIdFromStart,
343
+ tg_id: ctx.from.id,
344
+ tg_username: tgUsername,
345
+ tg_first_name: ctx.from.first_name || tgUsername,
346
+ tg_last_name: ctx.from.last_name || "",
347
+ role: "user",
348
+ language: ctx.from.language_code || "en",
349
+ },
350
+ accessKey || undefined
351
+ );
333
352
  }
334
353
 
335
354
  ctx.user = this.config.userStorage.find(tgUsername);
@@ -433,7 +452,9 @@ export class App {
433
452
  ) {
434
453
  this.messageHandlers.forEach(async (handler: any) => {
435
454
  if (ctx.caught) {
436
- this.debugLog("Message already caught by another handler, skipping remaining handlers.");
455
+ this.debugLog(
456
+ "Message already caught by another handler, skipping remaining handlers."
457
+ );
437
458
  return;
438
459
  }
439
460
 
@@ -447,7 +468,9 @@ export class App {
447
468
  },
448
469
  });
449
470
  if (ctx.caught) {
450
- this.debugLog("Message handler route caught the message, skipping remaining handlers.");
471
+ this.debugLog(
472
+ "Message handler route caught the message, skipping remaining handlers."
473
+ );
451
474
  return;
452
475
  }
453
476
  }
@@ -459,17 +482,25 @@ export class App {
459
482
  : handler.constructor?.name || "unknown";
460
483
 
461
484
  if (handlerIsClass && !ctx.caught) {
462
- this.debugLog(`Running message handler class ${nameHandler} for user ${ctx.user.username}`);
485
+ this.debugLog(
486
+ `Running message handler class ${nameHandler} for user ${ctx.user.username}`
487
+ );
463
488
  await new handler(this).handle(ctx);
464
489
  if (ctx.caught) {
465
- this.debugLog("Message handler class caught the message, skipping remaining handlers.");
490
+ this.debugLog(
491
+ "Message handler class caught the message, skipping remaining handlers."
492
+ );
466
493
  return;
467
494
  }
468
495
  } else if (!handlerIsClass && typeof handler === "function" && !ctx.caught) {
469
- this.debugLog(`Running message handler function ${nameHandler} for user ${ctx.user.username}`);
496
+ this.debugLog(
497
+ `Running message handler function ${nameHandler} for user ${ctx.user.username}`
498
+ );
470
499
  await handler(ctx);
471
500
  if (ctx.caught) {
472
- this.debugLog("Message handler function caught the message, skipping remaining handlers.");
501
+ this.debugLog(
502
+ "Message handler function caught the message, skipping remaining handlers."
503
+ );
473
504
  return;
474
505
  }
475
506
  }
@@ -876,7 +907,7 @@ export class App {
876
907
  }
877
908
  sectionClass = this.sectionClasses.get(sectionId) as typeof Section;
878
909
  }
879
- this.debugLog("Using section class:", sectionClass);
910
+ this.debugLog("Using section class:", sectionClass.constructor.name);
880
911
 
881
912
  let sectionInstance: Section | undefined;
882
913
 
@@ -1042,10 +1073,13 @@ export class App {
1042
1073
  return section;
1043
1074
  }
1044
1075
 
1045
- async registerUser(data: UserRegistrationData): Promise<UserModel | null> {
1076
+ async registerUser(data: UserRegistrationData, accessKey?: string): Promise<UserModel | null> {
1046
1077
  try {
1047
1078
  const user = await UserModel.register(data);
1048
1079
 
1080
+ if (accessKey) {
1081
+ AccessKey.markUsed(accessKey, user.id);
1082
+ }
1049
1083
  if (this.config.userStorage) {
1050
1084
  this.config.userStorage.add(data.tg_username, user);
1051
1085
  this.debugLog("User added to storage:", data.tg_username);
@@ -1063,22 +1097,22 @@ export class App {
1063
1097
  * @param tgUsername Telegram username of the user to remember. This method checks if the user exists in storage, and if not, tries to fetch it from the database and add to storage. This is useful for cases when user data might be updated in the database and we want to refresh the storage with the latest data.
1064
1098
  * @returns A boolean indicating whether the user was successfully remembered (true) or not (false).
1065
1099
  */
1066
- async rememberUser(tgUsername: string): Promise<boolean> {
1100
+ rememberUser(tgUsername: string): boolean {
1067
1101
  if (this.config.userStorage && !this.config.userStorage.exists(tgUsername)) {
1068
- this.debugLog("Warning: Username not found in storage:", tgUsername);
1069
- this.debugLog("Trying getting to database:", tgUsername);
1070
-
1102
+ this.debugLog("Warning: Remembering, Username not found in storage:", tgUsername);
1103
+ this.debugLog("Remembering, Trying getting to database:", tgUsername);
1104
+
1071
1105
  // Try to get user from database and add to storage
1072
1106
  UserModel.resolveDb();
1073
1107
  const userFromDb = UserModel.findByUsername(tgUsername);
1074
-
1108
+
1075
1109
  if (userFromDb) {
1076
1110
  this.config.userStorage.add(tgUsername, userFromDb);
1077
1111
  this.debugLog("Success: User found in database and added to storage:", tgUsername);
1078
1112
  this.debugLog('Success: Remembered user "' + tgUsername + '"');
1079
1113
  return true;
1080
1114
  } else {
1081
- this.debugLog("Warning: User not found in database:", tgUsername);
1115
+ this.debugLog("Warning: Remembering, User not found in database:", tgUsername);
1082
1116
  }
1083
1117
  }
1084
1118
  return false;
@@ -1328,7 +1362,7 @@ export class App {
1328
1362
  const source = `${colors.bright}${colors.fg.magenta}AppDebug${colors.reset}`;
1329
1363
 
1330
1364
  // Format args: highlight objects, errors, etc.
1331
- const formattedArgs = args.map(arg => {
1365
+ const formattedArgs = args.map((arg) => {
1332
1366
  if (arg instanceof Error) {
1333
1367
  return `${colors.fg.red}${arg.stack || arg.message}${colors.reset}`;
1334
1368
  }
@@ -1,10 +1,12 @@
1
- import * as path from 'path';
2
1
  import chalk from 'chalk';
3
2
  import { Artisan } from '../illumination/Artisan';
3
+ import { UserModel } from '../user/UserModel';
4
+ import type { Database } from 'bun:sqlite'
4
5
 
5
6
  export interface BotArtisanOptions {
6
7
  botName: string;
7
8
  sectionsPath?: string;
9
+ db?: Database;
8
10
  }
9
11
 
10
12
  export class BotArtisan {
@@ -13,7 +15,11 @@ export class BotArtisan {
13
15
 
14
16
  constructor(botPath: string, options: BotArtisanOptions) {
15
17
  this.options = options;
16
- this.artisan = new Artisan(botPath);
18
+ this.artisan = new Artisan(botPath, { db: options.db });
19
+
20
+ if (options.db) {
21
+ UserModel.setDatabase(options.db);
22
+ }
17
23
  }
18
24
 
19
25
  async run(): Promise<void> {
@@ -49,6 +55,40 @@ export class BotArtisan {
49
55
  await this.artisan.listSections();
50
56
  break;
51
57
 
58
+ case 'list:users': {
59
+ const users = UserModel.getAll();
60
+ if (!users.length) {
61
+ console.log(chalk.yellow('No users found.'));
62
+ break;
63
+ }
64
+ console.log(chalk.green('Users:'));
65
+ users.forEach(u => {
66
+ console.log(
67
+ `ID: ${u.id} | Username: ${u.tgUsername} | Name: ${u.tgName} | Role: ${u.role}`
68
+ );
69
+ });
70
+ break;
71
+ }
72
+
73
+ case 'set:admin': {
74
+ if (args.length < 1) {
75
+ console.error(chalk.red('❌ Error: User ID is required'));
76
+ console.log('Usage: artisan set:admin <userId>');
77
+ break;
78
+ }
79
+ const userId = Number(args[0]);
80
+
81
+ const user = UserModel.findById(userId);
82
+
83
+ if (!user) {
84
+ console.error(chalk.red(`❌ Error: User with ID ${userId} not found`));
85
+ break;
86
+ }
87
+ UserModel.update(userId, { role: 'admin' });
88
+ console.log(chalk.green(`User ${user.tgUsername} (ID: ${userId}) is now admin.`));
89
+ break;
90
+ }
91
+
52
92
  default:
53
93
  console.error(chalk.red(`❌ Unknown command: ${command}`));
54
94
  this.showHelp();
@@ -71,10 +111,15 @@ Available commands:
71
111
  add:method <section> <name> Add a new method to existing section
72
112
  list:sections List all sections
73
113
 
114
+ list:users List all users
115
+ set:admin <userId> Set user role to admin
116
+
74
117
  Examples:
75
118
  artisan make:section Auth Create new AuthSection
76
119
  artisan add:method Auth login Add login method to AuthSection
77
120
  artisan list:sections Show all available sections
121
+ artisan list:users Show all users
122
+ artisan set:admin 1 Set user with ID 1 as admin
78
123
  `);
79
124
  }
80
125
  }
@@ -1,11 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import type { Database } from 'bun:sqlite';
4
+
5
+ export type ArtisanOptions = {
6
+ db?: Database;
7
+ };
3
8
 
4
9
  export class Artisan {
5
10
  private basePath: string;
11
+ private options: ArtisanOptions = {};
6
12
 
7
- constructor(basePath: string) {
13
+ constructor(basePath: string, options: ArtisanOptions) {
8
14
  this.basePath = basePath;
15
+ this.options = options;
9
16
  }
10
17
 
11
18
  /**
@@ -33,57 +40,28 @@ export class Artisan {
33
40
  // Создаем файл секции
34
41
  fs.writeFileSync(sectionPath, template);
35
42
  console.log(`✅ Created section ${sectionName} at ${sectionPath}`);
43
+ console.log('To enable the section, add key it to the sections array in ' + process.cwd() + '/sectionList.ts');
36
44
  }
37
45
 
38
46
  /**
39
47
  * Форматирует имя секции (первая буква заглавная, остальные строчные)
40
48
  */
41
49
  private formatSectionName(name: string): string {
42
- return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
50
+ return name.charAt(0).toUpperCase() + name.slice(1);
43
51
  }
44
52
 
45
53
  /**
46
54
  * Возвращает шаблон для новой секции
47
55
  */
48
56
  private getSectionTemplate(name: string): string {
49
- return `import { Section } from "@2byte/tgbot-framework";
50
- import { SectionOptions } from "@2byte/tgbot-framework";
51
- import { InlineKeyboard } from "@2byte/tgbot-framework";
52
-
53
- export default class ${name}Section extends Section {
54
- static command = "${name.toLowerCase()}";
55
- static description = "${name} section";
56
- static actionRoutes = {
57
- "${name.toLowerCase()}.index": "index",
58
- };
59
-
60
- public sectionId = "${name.toLowerCase()}";
61
- private mainInlineKeyboard: InlineKeyboard;
62
-
63
- constructor(options: SectionOptions) {
64
- super(options);
65
-
66
- this.mainInlineKeyboard = this.makeInlineKeyboard([
67
- [this.makeInlineButton("🏠 На главную", "home.index")],
68
- ]);
69
- }
70
-
71
- public async up(): Promise<void> {}
72
- public async down(): Promise<void> {}
73
- public async setup(): Promise<void> {}
74
- public async unsetup(): Promise<void> {}
75
-
76
- async index() {
77
- const message = \`
78
- 👋 Welcome to ${name} Section
79
- \`;
80
-
81
- await this.message(message)
82
- .inlineKeyboard(this.mainInlineKeyboard)
83
- .send();
84
- }
85
- }
86
- `;
57
+ const filePath = path.join(__dirname, '../../templates', 'TemplateSection.ts');
58
+ let template = fs.readFileSync(filePath, 'utf-8');
59
+ const nameCamelCase = name.charAt(0).toLowerCase() + name.slice(1);
60
+
61
+ template = template.replace(/\$\{name\}/g, nameCamelCase);
62
+ template = template.replace(/\$\{commandName\}/g, name.toLowerCase());
63
+ template = template.replace(/TemplateSection/g, `${name}Section`);
64
+ return template;
87
65
  }
88
66
 
89
67
  /**
@@ -0,0 +1,103 @@
1
+ import { Model } from "./Model";
2
+ import crypto from "crypto";
3
+ import { UserModel } from "../user/UserModel";
4
+
5
+ export interface AccessKeyData {
6
+ id?: number;
7
+ user_id: number;
8
+ key?: string;
9
+ used?: number;
10
+ used_user_id?: number | null;
11
+ created_at?: string;
12
+ }
13
+
14
+ export class AccessKey extends Model {
15
+ static tableName = "access_keys";
16
+
17
+ /** Generate a secure random key (hex) */
18
+ static generateKey(lengthBytes = 24): string {
19
+ return crypto.randomBytes(lengthBytes).toString("hex");
20
+ }
21
+
22
+ /** Create a new access key and ensure uniqueness */
23
+ static create(data: AccessKeyData): string {
24
+ const { user_id } = data;
25
+
26
+ const key = this.generateKey();
27
+
28
+ try {
29
+ this.execute(
30
+ `INSERT INTO ${this.tableName} (user_id, key, used, used_user_id) VALUES (?, ?, 0, NULL)`,
31
+ [user_id, key]
32
+ );
33
+ return key;
34
+ } catch (e) {
35
+ // If UNIQUE constraint violation, retry generating a new key
36
+ // other errors will bubble up
37
+ const msg = (e && (e as any).message) || "";
38
+ if (!msg.includes("UNIQUE") && !msg.includes("unique")) throw e;
39
+ }
40
+
41
+ return key;
42
+ }
43
+
44
+ static async findByKey(key: string): Promise<AccessKeyData | null> {
45
+ return this.queryOne(`SELECT * FROM ${this.tableName} WHERE key = ?`, [key]);
46
+ }
47
+
48
+ static async findById(id: number): Promise<AccessKeyData | null> {
49
+ return this.queryOne(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
50
+ }
51
+
52
+ static markUsed(key: string, usedUserId?: number): Promise<void> {
53
+ this.execute(
54
+ `UPDATE ${this.tableName} SET used = 1, used_user_id = ?, created_at = created_at WHERE key = ?`,
55
+ [usedUserId || null, key]
56
+ );
57
+ }
58
+
59
+ static async revoke(key: string): Promise<void> {
60
+ await this.execute(`DELETE FROM ${this.tableName} WHERE key = ?`, [key]);
61
+ }
62
+
63
+ static async getAll(limit = 100): Promise<AccessKeyData[]> {
64
+ return this.query(`SELECT * FROM ${this.tableName} ORDER BY created_at DESC LIMIT ?`, [limit]);
65
+ }
66
+
67
+ /** Get all access keys for a given user */
68
+ static async findByUser(userId: number, limit = 100): Promise<AccessKeyData[]> {
69
+ return this.query(
70
+ `SELECT * FROM ${this.tableName} WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`,
71
+ [userId, limit]
72
+ );
73
+ }
74
+
75
+ /** Return the owner `UserModel` for a given access entry or null */
76
+ static async getUserByEntry(entry: AccessKeyData): Promise<any | null> {
77
+ const userData = await this.queryOne(`SELECT * FROM users WHERE id = ?`, [entry.user_id]);
78
+ if (!userData) return null;
79
+ try {
80
+ return UserModel.make(userData);
81
+ } catch (e) {
82
+ return userData;
83
+ }
84
+ }
85
+
86
+ /** Find owner by key string */
87
+ static async getUserByKey(key: string): Promise<any | null> {
88
+ const entry = await this.findByKey(key);
89
+ if (!entry) return null;
90
+ return this.getUserByEntry(entry as AccessKeyData);
91
+ }
92
+
93
+ static checkKeyValid(key: string): boolean {
94
+ const queryKey = this.queryOne(
95
+ `SELECT COUNT(*) as count FROM ${this.tableName} WHERE key = ? AND used = 0`,
96
+ [key]
97
+ );
98
+
99
+ return queryKey && queryKey.count > 0;
100
+ }
101
+ }
102
+
103
+ export default AccessKey;
@@ -1,3 +1,4 @@
1
1
  export * from './Model';
2
2
  export * from './TgAccount';
3
- export * from './Proxy';
3
+ export * from './Proxy';
4
+ export * from './AccessKey';
package/src/types.ts CHANGED
@@ -41,7 +41,7 @@ export interface RunnedSection {
41
41
  }
42
42
 
43
43
  export interface UserAttributes {
44
- id?: number;
44
+ id: number;
45
45
  user_refid?: number;
46
46
  tg_id: number;
47
47
  tg_username: string;
@@ -47,10 +47,18 @@ export class UserModel extends Model {
47
47
  return this.attributes;
48
48
  }
49
49
 
50
- get id(): number | undefined {
50
+ get id(): number {
51
51
  return this.attributes.id;
52
52
  }
53
53
 
54
+ get tgUsername(): string {
55
+ return this.attributes.tg_username;
56
+ }
57
+
58
+ get tgName(): string {
59
+ return this.attributes.tg_first_name + (this.attributes.tg_last_name ? " " + this.attributes.tg_last_name : "");
60
+ }
61
+
54
62
  get lastMessageIds(): number[] {
55
63
  return this.serviceAttributes.lastMessageIds;
56
64
  }
@@ -132,6 +140,20 @@ export class UserModel extends Model {
132
140
  throw new Error("Error not found user params:" + JSON.stringify(params));
133
141
  }
134
142
 
143
+ static findById(id: number): UserModel | undefined {
144
+ if (this.db) {
145
+ const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
146
+
147
+ if (userData) {
148
+ return UserModel.make(userData);
149
+ }
150
+ } else {
151
+ throw new Error("Database connection is not set.");
152
+ }
153
+
154
+ return undefined;
155
+ }
156
+
135
157
  static findByUsername(tgUsername: string): UserModel | undefined {
136
158
  if (this.db) {
137
159
  const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE tg_username = ?`, [tgUsername]);
@@ -153,6 +175,7 @@ export class UserModel extends Model {
153
175
 
154
176
  const now = new Date().toISOString();
155
177
  return UserModel.make({
178
+ id: 0,
156
179
  tg_id: 0,
157
180
  tg_username: tgUsername,
158
181
  tg_first_name: tgUsername,
@@ -192,6 +215,23 @@ export class UserModel extends Model {
192
215
  }
193
216
  }
194
217
 
218
+ static update(id: number, attributes: Partial<UserAttributes>): UserModel | undefined {
219
+ if (this.db) {
220
+ const user = this.findById(id);
221
+ if (user) {
222
+ Object.assign(user.attributes, attributes);
223
+ const keys = Object.keys(attributes);
224
+ const values = Object.values(attributes);
225
+ const setString = keys.map(key => `${key} = ?`).join(', ');
226
+ this.db.run(`UPDATE ${this.tableName} SET ${setString} WHERE id = ?`, [...values, id]);
227
+ return user;
228
+ }
229
+ } else {
230
+ throw new Error("Database connection is not set.");
231
+ }
232
+ return undefined;
233
+ }
234
+
195
235
  get username(): string {
196
236
  return this.attributes.tg_username;
197
237
  }
@@ -218,6 +258,10 @@ export class UserModel extends Model {
218
258
  return this.attributes.role;
219
259
  }
220
260
 
261
+ get roleIsAdmin(): boolean {
262
+ return this.attributes.role === 'admin';
263
+ }
264
+
221
265
  get language(): string {
222
266
  return this.attributes.language;
223
267
  }
@@ -31,11 +31,10 @@ export class MassSendApiService extends ApiService<MassSendApiParams> {
31
31
  this.app.debugLog("Received data for mass message:", receivedData);
32
32
 
33
33
  let userIds: number[] = [];
34
- let message: string = "Hello from MassSendApiService";
35
34
 
36
- if (receivedData && typeof receivedData == "object") {
35
+ if (receivedData && typeof receivedData == "object" && receivedData.message) {
37
36
  userIds = receivedData?.userIds || [];
38
- message = receivedData?.message || "Hello from MassSendApiService";
37
+ const message = receivedData?.message;
39
38
 
40
39
  this.sendMassMessage(userIds, message, receivedData.extra);
41
40
  }
@@ -0,0 +1,37 @@
1
+ import { Section } from "@2byte/tgbot-framework";
2
+ import { SectionOptions } from "@2byte/tgbot-framework";
3
+ import { InlineKeyboard } from "@2byte/tgbot-framework";
4
+
5
+ export default class TemplateSection extends Section {
6
+ static override command = "${commandName}";
7
+ static override description = "${name} section";
8
+ static override actionRoutes = {
9
+ "${name}.index": "index",
10
+ };
11
+
12
+ public override sectionId = "${name}";
13
+ private mainInlineKeyboard: InlineKeyboard;
14
+
15
+ constructor(options: SectionOptions) {
16
+ super(options);
17
+
18
+ this.mainInlineKeyboard = this.makeInlineKeyboard().addFootFixedButtons(
19
+ this.makeInlineButton("🏠 На главную", "home.index")
20
+ );
21
+ }
22
+
23
+ public override async up(): Promise<void> {}
24
+ public override async down(): Promise<void> {}
25
+ public override async setup(): Promise<void> {}
26
+ public override async unsetup(): Promise<void> {}
27
+
28
+ async index() {
29
+ const message = `
30
+ 👋 Welcome to ${this.ctx.user.attributes.tg_username} Section
31
+ `;
32
+
33
+ await this.message(message)
34
+ .inlineKeyboard(this.mainInlineKeyboard)
35
+ .send();
36
+ }
37
+ }
@@ -21,7 +21,7 @@
21
21
  "author": "{{author}}",
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "@2byte/tgbot-framework": "^1.0.15"
24
+ "@2byte/tgbot-framework": "^1.0.17"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.19.8",