@2byte/tgbot-framework 1.0.1 → 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.1",
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/Model.ts CHANGED
@@ -2,6 +2,10 @@ import type { Database } from "bun:sqlite";
2
2
  import { MakeManualPaginateButtonsParams, ModelPaginateParams, PaginateResult } from "../types";
3
3
  import { Section } from "../illumination/Section";
4
4
 
5
+ declare global {
6
+ var db: Database;
7
+ }
8
+
5
9
  export abstract class Model {
6
10
  protected static db: Database;
7
11
  protected static tableName: string;
@@ -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
  }
@@ -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
  }
@@ -2,7 +2,7 @@ import { Section, SectionOptions, InlineKeyboard } from "@2byte/tgbot-framework"
2
2
 
3
3
  export default class HomeSection extends Section {
4
4
  static command = "start";
5
- static description = "{{className}} Bot Home section";
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,9 +26,9 @@ 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
  Выберите действие:
@@ -56,10 +56,8 @@ export default class HomeSection extends Section {
56
56
  `;
57
57
 
58
58
  await this.message(message)
59
+ .inlineKeyboard(this.mainInlineKeyboard)
59
60
  .markdown()
60
- .inlineKeyboard([
61
- [this.makeInlineButton("🏠 На главную", "home.index")],
62
- ])
63
61
  .send();
64
62
  }
65
63
  }