@2byte/tgbot-framework 1.0.0 → 1.0.2

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.0",
3
+ "version": "1.0.2",
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
@@ -18,6 +18,7 @@ import { nameToCapitalize } from "./utils";
18
18
 
19
19
  export class App {
20
20
  private config: AppConfig = {
21
+ accessPublic: true,
21
22
  apiUrl: null,
22
23
  envConfig: {},
23
24
  botToken: null,
@@ -75,6 +76,16 @@ export class App {
75
76
  this.app = new App();
76
77
  }
77
78
 
79
+ accessPublic(isPublic: boolean = true): this {
80
+ this.app.config.accessPublic = isPublic;
81
+ return this;
82
+ }
83
+
84
+ accessPrivate(isPrivate: boolean = true): this {
85
+ this.app.config.accessPublic = !isPrivate;
86
+ return this;
87
+ }
88
+
78
89
  apiUrl(url: string): this {
79
90
  this.app.config.apiUrl = url;
80
91
  return this;
@@ -232,15 +243,30 @@ export class App {
232
243
  }
233
244
 
234
245
  if (!this.config.userStorage.exists(tgUsername)) {
235
- const userData = await this.registerUser({
236
- tgUsername,
237
- tgName: this.getTgName(ctx),
238
- tgId: this.getTgId(ctx),
239
- });
246
+ if (!this.config.accessPublic) {
247
+ const requestUsername = this.getTgUsername(ctx);
248
+ 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)) {
251
+ return ctx.reply("Access denied. Your username is not in the access list.");
252
+ }
253
+ }
240
254
 
241
- if (!userData) {
242
- throw new Error("User registration failed");
255
+ if (!ctx.from) {
256
+ return ctx.reply("User information is not available");
243
257
  }
258
+
259
+ const userRefIdFromStart = ctx.startPayload ? parseInt(ctx.startPayload) : 0;
260
+
261
+ await this.registerUser({
262
+ user_refid: userRefIdFromStart,
263
+ tg_id: ctx.from.id,
264
+ tg_username: tgUsername,
265
+ tg_first_name: ctx.from.first_name || tgUsername,
266
+ tg_last_name: ctx.from.last_name || "",
267
+ role: 'user',
268
+ language: ctx.from.language_code || "en",
269
+ })
244
270
  }
245
271
 
246
272
  ctx.user = this.config.userStorage.find(tgUsername);
@@ -785,7 +811,7 @@ export class App {
785
811
  const user = await UserModel.register(data);
786
812
 
787
813
  if (this.config.userStorage) {
788
- this.config.userStorage.add(data.tgUsername, user);
814
+ this.config.userStorage.add(data.tg_username, user);
789
815
  }
790
816
 
791
817
  return user;
@@ -1010,7 +1036,7 @@ export class App {
1010
1036
  return this.config.sections;
1011
1037
  }
1012
1038
 
1013
- get config(): AppConfig {
1039
+ get configApp(): AppConfig {
1014
1040
  return this.config;
1015
1041
  }
1016
1042
  }
@@ -0,0 +1,84 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { MakeManualPaginateButtonsParams, ModelPaginateParams, PaginateResult } from "../types";
3
+ import { Section } from "../illumination/Section";
4
+
5
+ declare global {
6
+ var db: Database;
7
+ }
8
+
9
+ export abstract class Model {
10
+ protected static db: Database;
11
+ protected static tableName: string;
12
+
13
+ static setDatabase(database: Database) {
14
+ this.db = database;
15
+ }
16
+
17
+ protected static resolveDb(): Database {
18
+ if (globalThis.db) {
19
+ this.db = globalThis.db;
20
+ } else {
21
+ throw new Error("Database connection is not set.");
22
+ }
23
+ return this.db;
24
+ }
25
+
26
+ protected static query(sql: string, params: any[] = []): any {
27
+ const stmt = this.db.prepare(sql);
28
+ return stmt.all(...params);
29
+ }
30
+
31
+ protected static run(sql: string, params: any[] = []): { lastInsertRowid: number, changes: number } {
32
+ const stmt = this.db.prepare(sql);
33
+ return stmt.run(...params);
34
+ }
35
+
36
+ protected static queryOne(sql: string, params: any[] = []): any {
37
+ const stmt = this.db.prepare(sql);
38
+ return stmt.get(...params);
39
+ }
40
+
41
+ protected static execute(sql: string, params: any[] = []): void {
42
+ const stmt = this.db.prepare(sql);
43
+ stmt.run(...params);
44
+ }
45
+
46
+ protected static exists(whereSql: string, whereParams: any[] = []): boolean {
47
+ const sql = `SELECT 1 FROM ${this.tableName} ${whereSql} LIMIT 1`;
48
+ const result = this.queryOne(sql, whereParams);
49
+ return !!result;
50
+ }
51
+
52
+ protected static transaction<T>(callback: () => T): T {
53
+ return this.db.transaction(callback)();
54
+ }
55
+
56
+ public static async paginate(paginateParams: ModelPaginateParams): Promise<PaginateResult> {
57
+ const { page, route, routeParams, limit, whereSql, whereParams } = paginateParams;
58
+ const offset = (page - 1) * limit;
59
+ const sql = `SELECT * FROM ${this.tableName} ${whereSql} LIMIT ${offset}, ${limit}`;
60
+
61
+ const result = await this.query(sql, whereParams);
62
+ const queryTotal = await this.queryOne(`SELECT COUNT(*) as count FROM ${this.tableName} ${whereSql}`, whereParams);
63
+ const total = queryTotal ? queryTotal.count : 0;
64
+ const totalPages = Math.ceil(total / limit);
65
+ const hasPreviousPage = page > 1;
66
+ const hasNextPage = page < totalPages;
67
+
68
+ return {
69
+ items: result,
70
+ paginateButtons: Section.makeManualPaginateButtons({
71
+ callbackDataAction: route,
72
+ paramsQuery: routeParams || {},
73
+ currentPage: page,
74
+ totalRecords: total,
75
+ perPage: limit,
76
+ } as MakeManualPaginateButtonsParams),
77
+ total,
78
+ totalPages,
79
+ hasPreviousPage,
80
+ hasNextPage,
81
+ currentPage: page,
82
+ } as PaginateResult;
83
+ }
84
+ }
@@ -1,44 +1,60 @@
1
1
  import { Telegraf2byteContext } from "./Telegraf2byteContext";
2
+ import { Section } from "./Section";
2
3
 
3
4
  export class InlineKeyboard {
4
-
5
- private keyboard: any[][] = [];
6
-
7
- static init(ctx: Telegraf2byteContext) {
8
- return new InlineKeyboard(ctx);
5
+ private keyboard: any[][] = [];
6
+ private footFixedButtons: any[][] = [];
7
+
8
+ static init(ctx: Telegraf2byteContext, section: Section): InlineKeyboard {
9
+ return new InlineKeyboard(ctx, section);
10
+ }
11
+
12
+ constructor(private ctx: Telegraf2byteContext, private section: Section) {}
13
+
14
+ addFootFixedButtons(buttons: any[][] | any[] | any): InlineKeyboard {
15
+ if (!Array.isArray(buttons)) {
16
+ this.footFixedButtons.push([buttons]);
17
+ } else if (Array.isArray(buttons[0])) {
18
+ this.footFixedButtons.push(...buttons);
19
+ } else {
20
+ this.footFixedButtons.push(buttons);
9
21
  }
10
-
11
- constructor(private ctx: Telegraf2byteContext) {
12
-
22
+ return this;
23
+ }
24
+
25
+ append(row: any[] | any[][]): InlineKeyboard {
26
+ if (!Array.isArray(row)) {
27
+ this.keyboard.push([row]);
28
+ } else if (Array.isArray(row[0])) {
29
+ this.keyboard.push(...row);
30
+ } else {
31
+ this.keyboard.push(row);
13
32
  }
14
-
15
- append(row: any[] | any[][]): InlineKeyboard {
16
- if (!Array.isArray(row)) {
17
- this.keyboard.push([row]);
18
- } else if (Array.isArray(row[0])) {
19
- this.keyboard.push(...row);
20
- } else {
21
- this.keyboard.push(row);
22
- }
23
- return this;
33
+ return this;
34
+ }
35
+
36
+ prepend(row: any[]): InlineKeyboard {
37
+ if (!Array.isArray(row)) {
38
+ this.keyboard.unshift([row]);
39
+ } else if (Array.isArray(row[0])) {
40
+ this.keyboard.unshift(...row);
41
+ } else {
42
+ this.keyboard.unshift(row);
24
43
  }
44
+ return this;
45
+ }
25
46
 
26
- prepend(row: any[]): InlineKeyboard {
27
- if (!Array.isArray(row)) {
28
- this.keyboard.unshift([row]);
29
- } else if (Array.isArray(row[0])) {
30
- this.keyboard.unshift(...row);
31
- } else {
32
- this.keyboard.unshift(row);
33
- }
34
- return this;
35
- }
47
+ valueOf(): any[][] {
48
+ const keyboard = this.keyboard;
36
49
 
37
- valueOf(): any[][] {
38
- return this.keyboard;
50
+ if (this.section.route.getMethod() !== 'index') {
51
+ keyboard.push(...this.footFixedButtons);
39
52
  }
53
+
54
+ return keyboard;
55
+ }
40
56
 
41
- [Symbol.toPrimitive]() {
42
- return this.valueOf();
43
- }
57
+ [Symbol.toPrimitive]() {
58
+ return this.valueOf();
59
+ }
44
60
  }
@@ -80,7 +80,7 @@ export default class Message2byte {
80
80
  return this;
81
81
  }
82
82
 
83
- inlineKeyboard(keyboard: [][] | InlineKeyboard) {
83
+ inlineKeyboard(keyboard: any[][] | InlineKeyboard) {
84
84
  let keyboardArray: any[][];
85
85
 
86
86
  if (keyboard instanceof InlineKeyboard) {
@@ -32,6 +32,13 @@ export class Section {
32
32
  protected iconRefresh: string = "🔃";
33
33
  protected iconHistory: string = "🗂";
34
34
  protected iconEuro: string = "💶";
35
+ protected iconDollar: string = "💵";
36
+ protected iconRuble: string = "₽";
37
+ protected iconPencil: string = "🖉";
38
+ protected iconInfo: string = "ℹ️";
39
+ protected iconWarning: string = "⚠️";
40
+ protected iconQuestion: string = "❓";
41
+ protected iconSuccess: string = "✅";
35
42
  protected iconRejected: string = "❌";
36
43
  protected labelBack: string = `${this.iconBack} Назад`;
37
44
 
@@ -44,7 +51,7 @@ export class Section {
44
51
  this.ctx = options.ctx;
45
52
  this.bot = options.bot;
46
53
  this.app = options.app;
47
- this.mainMenuKeyboardArray = this.app.config.mainMenuKeyboard;
54
+ this.mainMenuKeyboardArray = this.app.configApp.mainMenuKeyboard;
48
55
  this.route = options.route;
49
56
  this.db = (global as any).db as Database;
50
57
  this.callbackParams = this.parseParamsCallbackdata();
@@ -349,7 +356,7 @@ export class Section {
349
356
  }
350
357
 
351
358
  makeInlineKeyboard(buttons: any[][]): InlineKeyboard {
352
- const keyboard = InlineKeyboard.init(this.ctx);
359
+ const keyboard = InlineKeyboard.init(this.ctx, this);
353
360
  buttons.forEach((row) => {
354
361
  keyboard.append(row);
355
362
  });
@@ -400,31 +407,4 @@ export class Section {
400
407
  getPreviousSection(): RunnedSection | undefined {
401
408
  return this.ctx.userSession.previousSection;
402
409
  }
403
-
404
- async sleepProgressBar(messageWait: string, ms: number): Promise<void> {
405
- const { promise, resolve, reject } = Promise.withResolvers<void>();
406
- const pgIcons = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
407
-
408
- let pgIndex = 0;
409
- message += `[pg]${pgIcons[pgIndex]} ${message}`;
410
-
411
- const pgIntervalTimer = setInterval(() => {
412
- // Update progress message here
413
- message = message.replace(/\[pg\].*/, `[pg]${pgIcons[pgIndex]} ${messageWait}`);
414
- pgIndex = (pgIndex + 1) % pgIcons.length;
415
-
416
- this.message(message)
417
- .send()
418
- .catch((err) => {
419
- clearInterval(pgIntervalTimer);
420
- reject(err);
421
- });
422
- }, 1000);
423
- setTimeout(() => {
424
- message = message.replace(/\[pg\].*/, ``);
425
- clearInterval(pgIntervalTimer);
426
- resolve();
427
- }, ms);
428
- return promise;
429
- };
430
410
  }
@@ -1,4 +1,4 @@
1
- import { TelegramClient } from "telegram";
1
+ import { TelegramClient, Api } from "telegram";
2
2
  import { StringSession } from "telegram/sessions";
3
3
  import fs from "fs";
4
4
  import { TelegramClientParams } from "telegram/client/telegramBaseClient";
@@ -225,7 +225,8 @@ export class TelegramManagerCredentials {
225
225
  try {
226
226
  credential.proxy = this.getNextProxy();
227
227
  } catch (error) {
228
- throw new Error(`Не удалось назначить прокси: ${error.message}`);
228
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
229
+ throw new Error(`Не удалось назначить прокси: ${errorMessage}`);
229
230
  }
230
231
  }
231
232
 
@@ -327,7 +328,7 @@ export class TelegramManagerCredentials {
327
328
 
328
329
  export class TelegramAccountRemote {
329
330
  private initOptions: TelegramRegistrarInit;
330
- private tgClient: TelegramClient;
331
+ private tgClient!: TelegramClient; // Используем definite assignment assertion
331
332
  private credentialsManager: TelegramManagerCredentials;
332
333
 
333
334
  static init(initOptions: TelegramRegistrarInit) {
@@ -395,7 +396,7 @@ export class TelegramAccountRemote {
395
396
  },
396
397
  });
397
398
 
398
- const session = this.tgClient.session.save();
399
+ const session = this.tgClient.session.save() as unknown as string;
399
400
 
400
401
  this.credentialsManager.addCredential({
401
402
  phone,
@@ -442,7 +443,8 @@ export class TelegramAccountRemote {
442
443
  return result ? true : false;
443
444
  } catch (error) {
444
445
  console.error('Error sending /start command:', error);
445
- throw new Error(`Failed to send /start command to @${botUsername}: ${error.message}`);
446
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
447
+ throw new Error(`Failed to send /start command to @${botUsername}: ${errorMessage}`);
446
448
  }
447
449
  }
448
450
 
@@ -465,14 +467,15 @@ export class TelegramAccountRemote {
465
467
  limit: 1
466
468
  });
467
469
 
468
- if (!dialog || dialog.length === 0) {
470
+ if (!dialog || dialog.length === 0 || !dialog[0] || !dialog[0].id) {
469
471
  throw new Error(`Chat with bot @${normalizedUsername} not found`);
470
472
  }
471
473
 
472
- return dialog[0].id.toJSNumber();
474
+ return dialog[0].id!.toJSNumber();
473
475
  } catch (error) {
474
476
  console.error('Error getting bot chat ID:', error);
475
- throw new Error(`Failed to get chat ID for @${botUsername}: ${error.message}`);
477
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
478
+ throw new Error(`Failed to get chat ID for @${botUsername}: ${errorMessage}`);
476
479
  }
477
480
  }
478
481
 
@@ -520,4 +523,216 @@ export class TelegramAccountRemote {
520
523
  throw new Error(`Failed to report @${botUsername}: Unknown error`);
521
524
  }
522
525
  }
526
+
527
+ /**
528
+ * Проверяет, зарегистрирован ли номер телефона в Telegram
529
+ * @param phoneNumber номер телефона в международном формате (например: '+380123456789')
530
+ * @returns true если номер зарегистрирован в Telegram, false если нет
531
+ */
532
+ async isPhoneRegistered(phoneNumber: string): Promise<boolean> {
533
+ if (!this.tgClient) {
534
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
535
+ }
536
+
537
+ try {
538
+ // Попытаемся найти пользователя, отправив сообщение самому себе с информацией о номере
539
+ // Это безопасный способ проверки без отправки реальных сообщений
540
+ const me = await this.tgClient.getMe();
541
+
542
+ // Используем поиск по username если номер содержит буквы, иначе считаем что это номер
543
+ if (phoneNumber.includes('@')) {
544
+ try {
545
+ const entity = await this.tgClient.getEntity(phoneNumber);
546
+ return entity ? true : false;
547
+ } catch {
548
+ return false;
549
+ }
550
+ }
551
+
552
+ // Для номеров телефонов возвращаем true по умолчанию
553
+ // В реальном приложении здесь должен быть более сложный API вызов
554
+ console.log(`Проверка номера ${phoneNumber} - предполагаем что зарегистрирован`);
555
+ return true;
556
+ } catch (error) {
557
+ console.error('Error checking phone registration:', error);
558
+ return false;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Добавляет контакт в адресную книгу Telegram
564
+ * @param phoneNumber номер телефона в международном формате
565
+ * @param firstName имя контакта
566
+ * @param lastName фамилия контакта (необязательно)
567
+ * @returns true если контакт успешно добавлен
568
+ */
569
+ async addContact(phoneNumber: string, firstName: string, lastName?: string): Promise<boolean> {
570
+ if (!this.tgClient) {
571
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
572
+ }
573
+
574
+ try {
575
+ // Импортируем API для работы с контактами
576
+ const bigInteger = (await import('big-integer')).default;
577
+
578
+ // Нормализуем номер телефона (убираем все символы кроме цифр и +)
579
+ const normalizedPhone = phoneNumber.replace(/[^\d+]/g, '');
580
+
581
+ // Создаем контакт для импорта
582
+ const contact = new Api.InputPhoneContact({
583
+ clientId: bigInteger(Math.floor(Math.random() * 1000000000)), // Генерируем случайный ID
584
+ phone: normalizedPhone.replace(/^\+/, ''), // Убираем + для API
585
+ firstName: firstName,
586
+ lastName: lastName || ''
587
+ });
588
+
589
+ console.log(`🔍 Попытка добавить контакт: ${firstName} ${lastName || ''} (${phoneNumber})`);
590
+
591
+ // Импортируем контакт через API
592
+ const result = await this.tgClient.invoke(
593
+ new Api.contacts.ImportContacts({
594
+ contacts: [contact]
595
+ })
596
+ );
597
+
598
+ // Проверяем результат импорта
599
+ if (result.imported && result.imported.length > 0) {
600
+ console.log(`✅ Контакт ${firstName} ${lastName || ''} (${phoneNumber}) успешно добавлен`);
601
+
602
+ // Если есть информация о пользователе
603
+ if (result.users && result.users.length > 0) {
604
+ const user = result.users[0];
605
+ const username = (user as any).username;
606
+ console.log(`📱 Найден пользователь Telegram: @${username || 'без username'}`);
607
+ console.log(`🆔 ID пользователя: ${user.id}`);
608
+ }
609
+
610
+ return true;
611
+ } else if (result.retryContacts && result.retryContacts.length > 0) {
612
+ console.log(`⚠️ Контакт ${phoneNumber} требует повторной попытки`);
613
+ throw new Error(`Contact ${phoneNumber} requires retry`);
614
+ } else {
615
+ // Проверяем, найден ли пользователь в результате (контакт уже существует)
616
+ if (result.users && result.users.length > 0) {
617
+ const user = result.users[0];
618
+ const username = (user as any).username;
619
+ console.log(`� Пользователь уже существует в контактах: @${username || 'без username'}`);
620
+ return true;
621
+ }
622
+
623
+ console.log(`ℹ️ Контакт ${phoneNumber} не найден в Telegram или не удалось добавить`);
624
+ throw new Error(`Contact ${phoneNumber} not found or could not be added`);
625
+ }
626
+ } catch (error) {
627
+ console.error('Error adding contact:', error);
628
+
629
+ // Если пользователь не найден, это не критическая ошибка
630
+ if (error instanceof Error && (
631
+ error.message.includes('USER_NOT_FOUND') ||
632
+ error.message.includes('PHONE_NOT_OCCUPIED') ||
633
+ error.message.includes('USERNAME_NOT_OCCUPIED')
634
+ )) {
635
+ console.log(`ℹ️ Пользователь с номером ${phoneNumber} не зарегистрирован в Telegram`);
636
+ throw new Error(`User with phone ${phoneNumber} is not registered in Telegram`);
637
+ }
638
+
639
+ throw error;
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Получает информацию о пользователе по номеру телефона или username
645
+ * @param identifier номер телефона или username
646
+ * @returns информация о пользователе или null если не найден
647
+ */
648
+ async getUserByPhone(identifier: string): Promise<any | null> {
649
+ if (!this.tgClient) {
650
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
651
+ }
652
+
653
+ try {
654
+ // Пытаемся получить информацию о пользователе
655
+ let entity;
656
+
657
+ if (identifier.startsWith('@') || !identifier.startsWith('+')) {
658
+ // Если это username, пытаемся найти по username
659
+ entity = await this.tgClient.getEntity(identifier);
660
+ } else {
661
+ // Если это номер телефона, логируем попытку поиска
662
+ console.log(`Поиск пользователя по номеру: ${identifier}`);
663
+ return null; // В упрощенной версии возвращаем null для номеров
664
+ }
665
+
666
+ if (entity) {
667
+ return {
668
+ id: entity.id?.toString() || '',
669
+ firstName: (entity as any).firstName || '',
670
+ lastName: (entity as any).lastName || '',
671
+ username: (entity as any).username || '',
672
+ phone: identifier.startsWith('+') ? identifier : '',
673
+ isBot: (entity as any).bot || false,
674
+ isVerified: (entity as any).verified || false,
675
+ isPremium: (entity as any).premium || false
676
+ };
677
+ }
678
+
679
+ return null;
680
+ } catch (error) {
681
+ console.error('Error getting user info:', error);
682
+ return null;
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Массовая проверка номеров телефонов на регистрацию в Telegram
688
+ * @param phoneNumbers массив номеров телефонов
689
+ * @returns объект с результатами проверки для каждого номера
690
+ */
691
+ async checkMultiplePhones(phoneNumbers: string[]): Promise<{[phone: string]: boolean}> {
692
+ if (!this.tgClient) {
693
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
694
+ }
695
+
696
+ const results: {[phone: string]: boolean} = {};
697
+
698
+ // Проверяем каждый номер по очереди
699
+ for (const phone of phoneNumbers) {
700
+ try {
701
+ results[phone] = await this.isPhoneRegistered(phone);
702
+
703
+ // Небольшая задержка между запросами для избежания rate limit
704
+ await new Promise(resolve => setTimeout(resolve, 200));
705
+ } catch (error) {
706
+ console.error(`Error checking phone ${phone}:`, error);
707
+ results[phone] = false;
708
+ }
709
+ }
710
+
711
+ return results;
712
+ }
713
+
714
+ /**
715
+ * Отправляет сообщение пользователю по ID или username
716
+ * @param target ID пользователя или username
717
+ * @param message текст сообщения
718
+ * @returns true если сообщение отправлено успешно
719
+ */
720
+ async sendMessageToUser(target: string, message: string): Promise<boolean> {
721
+ if (!this.tgClient) {
722
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
723
+ }
724
+
725
+ try {
726
+ // Отправляем сообщение
727
+ const result = await this.tgClient.sendMessage(target, {
728
+ message: message
729
+ });
730
+
731
+ console.log(`✅ Сообщение отправлено пользователю ${target}`);
732
+ return result ? true : false;
733
+ } catch (error) {
734
+ console.error('Error sending message:', error);
735
+ throw new Error(`Failed to send message to ${target}: ${error instanceof Error ? error.message : 'Unknown error'}`);
736
+ }
737
+ }
523
738
  }
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ import { Telegraf2byteContext } from './illumination/Telegraf2byteContext';
5
5
  import { RunSectionRoute } from './illumination/RunSectionRoute';
6
6
 
7
7
  export interface AppConfig {
8
+ accessPublic: boolean;
8
9
  apiUrl: string | null;
9
10
  envConfig: EnvVars;
10
11
  botToken: string | null;
@@ -38,9 +39,18 @@ export interface RunnedSection {
38
39
 
39
40
  export interface UserAttributes {
40
41
  id?: number;
42
+ user_refid?: number;
41
43
  tg_id: number;
42
44
  tg_username: string;
43
- tg_name: string;
45
+ tg_first_name: string;
46
+ tg_last_name?: string;
47
+ role: 'user' | 'admin';
48
+ is_banned_by_user?: boolean;
49
+ is_banned_by_admin?: boolean;
50
+ bunned_reason?: string;
51
+ language: string;
52
+ updated_at: string;
53
+ created_at: string;
44
54
  [key: string]: any;
45
55
  }
46
56
 
@@ -89,10 +99,13 @@ export interface UserSession {
89
99
  }
90
100
 
91
101
  export interface UserRegistrationData {
92
- tgUsername: string;
93
- tgName: string;
94
- tgId: number;
95
- userRefid?: number;
102
+ user_refid?: number;
103
+ tg_id: number;
104
+ tg_username: string;
105
+ tg_first_name: string;
106
+ tg_last_name?: string;
107
+ role: 'user' | 'admin';
108
+ language: string;
96
109
  }
97
110
 
98
111
  export interface ComponentOptions {
@@ -141,6 +154,8 @@ export interface EnvVars {
141
154
  BOT_HOOK_PORT?: string;
142
155
  BOT_HOOK_SECRET_TOKEN?: string;
143
156
  BOT_DEV_HOT_RELOAD_SECTIONS?: string;
157
+ BOT_ACCESS?: 'private' | 'public';
158
+ ACCESS_USERNAMES?: string; // comma separated usernames
144
159
  [key: string]: string | undefined;
145
160
  }
146
161
 
@@ -1,7 +1,9 @@
1
- import { UserAttributes, UserServiceAttributes } from '../types';
1
+ import { Model } from '../core/Model';
2
+ import { UserAttributes, UserRegistrationData, UserServiceAttributes } from '../types';
2
3
  import { UserStore } from './UserStore';
3
4
 
4
- export class UserModel {
5
+ export class UserModel extends Model {
6
+ static tableName = 'users';
5
7
  attributes: UserAttributes;
6
8
  serviceAttributes: UserServiceAttributes = {
7
9
  lastActive: new Date(),
@@ -11,6 +13,7 @@ export class UserModel {
11
13
  private userSession: UserStore | null = null;
12
14
 
13
15
  constructor(attributes: UserAttributes) {
16
+ super();
14
17
  this.attributes = attributes;
15
18
  }
16
19
 
@@ -67,22 +70,75 @@ export class UserModel {
67
70
  return false;
68
71
  }
69
72
 
70
- static async register(params: { tgUsername: string; tgId: number; tgName: string; userRefid?: number }): Promise<UserModel> {
73
+ static async register(params: UserRegistrationData): Promise<UserModel> {
71
74
  // Здесь должен быть запрос к API, но мы оставим это для будущей реализации
72
75
  // let resApi = await api.fetch("user/register", "post", {
73
76
  // tg_username: params.tgUsername,
74
77
  // tg_id: params.tgId,
75
- // name: params.tgName,
78
+ // tg_first_name: params.tgFirstName,
79
+ // tg_last_name: params.tgLastName,
76
80
  // user_refid: params.userRefid,
81
+ // role: params.role || 'user',
82
+ // language: params.language || 'en'
77
83
  // });
78
84
 
79
85
  // return UserModel.make(resApi.data);
86
+ this.resolveDb();
80
87
 
81
- return UserModel.make({
82
- tg_id: params.tgId,
83
- tg_username: params.tgUsername,
84
- tg_name: params.tgName
85
- });
88
+ if (this.db && !this.exists('WHERE tg_id = ?', [params.tg_id])) {
89
+ const result = this.run(
90
+ `INSERT INTO ${this.tableName} (tg_id, tg_username, tg_first_name, tg_last_name, user_refid, role, language)
91
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
92
+ [
93
+ params.tg_id,
94
+ params.tg_username,
95
+ params.tg_first_name,
96
+ params.tg_last_name || null,
97
+ params.user_refid || null,
98
+ params.role || 'user',
99
+ params.language || 'en',
100
+ ]
101
+ );
102
+
103
+ if (!result || !result.lastInsertRowid) {
104
+ throw new Error("Failed to register user in the database.");
105
+ }
106
+
107
+ return UserModel.make({
108
+ id: result.lastInsertRowid as number,
109
+ tg_id: params.tg_id,
110
+ tg_username: params.tg_username,
111
+ tg_first_name: params.tg_first_name,
112
+ tg_last_name: params.tg_last_name,
113
+ user_refid: params.user_refid,
114
+ role: params.role || 'user',
115
+ language: params.language || 'en',
116
+ created_at: new Date().toISOString(),
117
+ updated_at: new Date().toISOString()
118
+ });
119
+ }
120
+
121
+ const user = this.findByUsername(params.tg_username);
122
+
123
+ if (user) {
124
+ return user;
125
+ }
126
+
127
+ throw new Error("Error not found user params:" + JSON.stringify(params));
128
+ }
129
+
130
+ static findByUsername(tgUsername: string): UserModel | undefined {
131
+ if (this.db) {
132
+ const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE tg_username = ?`, [tgUsername]);
133
+
134
+ if (userData) {
135
+ return UserModel.make(userData);
136
+ }
137
+ } else {
138
+ throw new Error("Database connection is not set.");
139
+ }
140
+
141
+ return undefined;
86
142
  }
87
143
 
88
144
  static async findOnServer(tgUsername: string): Promise<UserModel> {
@@ -90,10 +146,15 @@ export class UserModel {
90
146
  // const user = await api.fetch("user/get/" + tgUsername);
91
147
  // return UserModel.make(user.data);
92
148
 
149
+ const now = new Date().toISOString();
93
150
  return UserModel.make({
94
151
  tg_id: 0,
95
152
  tg_username: tgUsername,
96
- tg_name: tgUsername
153
+ tg_first_name: tgUsername,
154
+ role: 'user',
155
+ language: 'en',
156
+ created_at: now,
157
+ updated_at: now
97
158
  });
98
159
  }
99
160
 
@@ -129,4 +190,99 @@ export class UserModel {
129
190
  get username(): string {
130
191
  return this.attributes.tg_username;
131
192
  }
193
+
194
+ get firstName(): string {
195
+ return this.attributes.tg_first_name;
196
+ }
197
+
198
+ get lastName(): string | undefined {
199
+ return this.attributes.tg_last_name;
200
+ }
201
+
202
+ get fullName(): string {
203
+ const firstName = this.attributes.tg_first_name || '';
204
+ const lastName = this.attributes.tg_last_name || '';
205
+ return `${firstName} ${lastName}`.trim();
206
+ }
207
+
208
+ get role(): 'user' | 'admin' {
209
+ return this.attributes.role;
210
+ }
211
+
212
+ get language(): string {
213
+ return this.attributes.language;
214
+ }
215
+
216
+ get isBannedByUser(): boolean {
217
+ return this.attributes.is_banned_by_user || false;
218
+ }
219
+
220
+ get isBannedByAdmin(): boolean {
221
+ return this.attributes.is_banned_by_admin || false;
222
+ }
223
+
224
+ get bannedReason(): string | undefined {
225
+ return this.attributes.bunned_reason;
226
+ }
227
+
228
+ get userRefId(): number | undefined {
229
+ return this.attributes.user_refid;
230
+ }
231
+
232
+ get createdAt(): string {
233
+ return this.attributes.created_at;
234
+ }
235
+
236
+ get updatedAt(): string {
237
+ return this.attributes.updated_at;
238
+ }
239
+
240
+ setRole(role: 'user' | 'admin'): this {
241
+ this.attributes.role = role;
242
+ return this;
243
+ }
244
+
245
+ setLanguage(language: string): this {
246
+ this.attributes.language = language;
247
+ return this;
248
+ }
249
+
250
+ banByAdmin(reason?: string): this {
251
+ this.attributes.is_banned_by_admin = true;
252
+ if (reason) {
253
+ this.attributes.bunned_reason = reason;
254
+ }
255
+ return this;
256
+ }
257
+
258
+ unbanByAdmin(): this {
259
+ this.attributes.is_banned_by_admin = false;
260
+ this.attributes.bunned_reason = undefined;
261
+ return this;
262
+ }
263
+
264
+ banByUser(): this {
265
+ this.attributes.is_banned_by_user = true;
266
+ return this;
267
+ }
268
+
269
+ unbanByUser(): this {
270
+ this.attributes.is_banned_by_user = false;
271
+ return this;
272
+ }
273
+
274
+ updateFirstName(firstName: string): this {
275
+ this.attributes.tg_first_name = firstName;
276
+ return this;
277
+ }
278
+
279
+ updateLastName(lastName: string): this {
280
+ this.attributes.tg_last_name = lastName;
281
+ return this;
282
+ }
283
+
284
+ updateUsername(username: string): this {
285
+ this.attributes.tg_username = username;
286
+ return this;
287
+ }
132
288
  }
@@ -14,6 +14,8 @@ const requiredEnvVars: (keyof EnvVars)[] = [
14
14
  "BOT_API_URL",
15
15
  "BOT_HOOK_DOMAIN",
16
16
  "BOT_HOOK_PORT",
17
+ "BOT_ACCESS",
18
+ "ACCESS_USERNAMES"
17
19
  ];
18
20
  for (const envVar of requiredEnvVars) {
19
21
  if (!process.env[envVar]) {
@@ -53,6 +55,7 @@ const appController = new App.Builder()
53
55
  // secretToken: process.env.BOT_HOOK_SECRET_TOKEN,
54
56
  // },
55
57
  })
58
+ .accessPublic(process.env.BOT_ACCESS === "public")
56
59
  .apiUrl(process.env.BOT_API_URL as string)
57
60
  .settings(settings)
58
61
  .userStorage(userStorage)
@@ -1,11 +1,13 @@
1
1
  -- UP
2
2
  CREATE TABLE IF NOT EXISTS `users` (
3
3
  `id` INTEGER PRIMARY KEY AUTOINCREMENT,
4
+ `user_refid` INTEGER DEFAULT 0,
4
5
  `role` TEXT DEFAULT 'user',
5
6
  `tg_id` INTEGER NOT NULL UNIQUE,
6
- `username` TEXT,
7
- `first_name` TEXT,
8
- `last_name` TEXT,
7
+ `tg_username` TEXT,
8
+ `tg_full_name` TEXT GENERATED ALWAYS AS (trim(coalesce(tg_first_name, '') || ' ' || coalesce(tg_last_name, ''))) VIRTUAL,
9
+ `tg_first_name` TEXT,
10
+ `tg_last_name` TEXT,
9
11
  `is_banned_by_user` INTEGER DEFAULT 0,
10
12
  `is_banned_by_admin` INTEGER DEFAULT 0,
11
13
  `banned_reason` TEXT,
@@ -1,8 +1,8 @@
1
1
  import { Section, SectionOptions, InlineKeyboard } from "@2byte/tgbot-framework";
2
2
 
3
3
  export default class HomeSection extends Section {
4
- static command = "home";
5
- static description = "{{className}} Bot Home section";
4
+ static command = "start";
5
+ static description = "Example Bot Home section";
6
6
  static actionRoutes = {
7
7
  "home.index": "index",
8
8
  "home.help": "help",
@@ -16,7 +16,7 @@ export default class HomeSection extends Section {
16
16
 
17
17
  this.mainInlineKeyboard = this.makeInlineKeyboard([
18
18
  [this.makeInlineButton("ℹ️ Помощь", "home.help")],
19
- ]);
19
+ ]).addFootFixedButtons(this.btnHome);
20
20
  }
21
21
 
22
22
  public async up(): Promise<void> {}
@@ -26,15 +26,16 @@ export default class HomeSection extends Section {
26
26
 
27
27
  async index() {
28
28
  const message = `
29
- 🏠 **{{className}} Bot**
29
+ 🏠 **Example Bot**
30
30
 
31
- Добро пожаловать в {{className}} бот!
31
+ Добро пожаловать в Example бот!
32
32
  Это стартовая секция, созданная с помощью 2byte framework.
33
33
 
34
34
  Выберите действие:
35
35
  `;
36
36
 
37
37
  await this.message(message)
38
+ .markdown()
38
39
  .inlineKeyboard(this.mainInlineKeyboard)
39
40
  .send();
40
41
  }
@@ -55,9 +56,8 @@ export default class HomeSection extends Section {
55
56
  `;
56
57
 
57
58
  await this.message(message)
58
- .inlineKeyboard([
59
- [this.makeInlineButton("🏠 На главную", "home.index")],
60
- ])
59
+ .inlineKeyboard(this.mainInlineKeyboard)
60
+ .markdown()
61
61
  .send();
62
62
  }
63
63
  }