@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 +1 -1
- package/src/core/App.ts +35 -9
- package/src/core/Model.ts +84 -0
- package/src/illumination/InlineKeyboard.ts +49 -33
- package/src/illumination/Message2Byte.ts +1 -1
- package/src/illumination/Section.ts +9 -29
- package/src/libs/TelegramAccountControl.ts +223 -8
- package/src/types.ts +20 -5
- package/src/user/UserModel.ts +166 -10
- package/templates/bot/bot.ts +3 -0
- package/templates/bot/database/migrations/001_create_users.sql +5 -3
- package/templates/bot/sections/HomeSection.ts +8 -8
package/package.json
CHANGED
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 (!
|
|
242
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
38
|
-
|
|
50
|
+
if (this.section.route.getMethod() !== 'index') {
|
|
51
|
+
keyboard.push(...this.footFixedButtons);
|
|
39
52
|
}
|
|
53
|
+
|
|
54
|
+
return keyboard;
|
|
55
|
+
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
474
|
+
return dialog[0].id!.toJSNumber();
|
|
473
475
|
} catch (error) {
|
|
474
476
|
console.error('Error getting bot chat ID:', error);
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
package/src/user/UserModel.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
}
|
package/templates/bot/bot.ts
CHANGED
|
@@ -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
|
-
`
|
|
7
|
-
`
|
|
8
|
-
`
|
|
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 = "
|
|
5
|
-
static description = "
|
|
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
|
-
🏠 **
|
|
29
|
+
🏠 **Example Bot**
|
|
30
30
|
|
|
31
|
-
Добро пожаловать в
|
|
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
|
-
|
|
60
|
-
])
|
|
59
|
+
.inlineKeyboard(this.mainInlineKeyboard)
|
|
60
|
+
.markdown()
|
|
61
61
|
.send();
|
|
62
62
|
}
|
|
63
63
|
}
|