@2byte/tgbot-framework 1.0.0 → 1.0.1
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 +80 -0
- package/src/illumination/Message2Byte.ts +1 -1
- 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 +3 -1
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,80 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { MakeManualPaginateButtonsParams, ModelPaginateParams, PaginateResult } from "../types";
|
|
3
|
+
import { Section } from "../illumination/Section";
|
|
4
|
+
|
|
5
|
+
export abstract class Model {
|
|
6
|
+
protected static db: Database;
|
|
7
|
+
protected static tableName: string;
|
|
8
|
+
|
|
9
|
+
static setDatabase(database: Database) {
|
|
10
|
+
this.db = database;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected static resolveDb(): Database {
|
|
14
|
+
if (globalThis.db) {
|
|
15
|
+
this.db = globalThis.db;
|
|
16
|
+
} else {
|
|
17
|
+
throw new Error("Database connection is not set.");
|
|
18
|
+
}
|
|
19
|
+
return this.db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected static query(sql: string, params: any[] = []): any {
|
|
23
|
+
const stmt = this.db.prepare(sql);
|
|
24
|
+
return stmt.all(...params);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected static run(sql: string, params: any[] = []): { lastInsertRowid: number, changes: number } {
|
|
28
|
+
const stmt = this.db.prepare(sql);
|
|
29
|
+
return stmt.run(...params);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected static queryOne(sql: string, params: any[] = []): any {
|
|
33
|
+
const stmt = this.db.prepare(sql);
|
|
34
|
+
return stmt.get(...params);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected static execute(sql: string, params: any[] = []): void {
|
|
38
|
+
const stmt = this.db.prepare(sql);
|
|
39
|
+
stmt.run(...params);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected static exists(whereSql: string, whereParams: any[] = []): boolean {
|
|
43
|
+
const sql = `SELECT 1 FROM ${this.tableName} ${whereSql} LIMIT 1`;
|
|
44
|
+
const result = this.queryOne(sql, whereParams);
|
|
45
|
+
return !!result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected static transaction<T>(callback: () => T): T {
|
|
49
|
+
return this.db.transaction(callback)();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static async paginate(paginateParams: ModelPaginateParams): Promise<PaginateResult> {
|
|
53
|
+
const { page, route, routeParams, limit, whereSql, whereParams } = paginateParams;
|
|
54
|
+
const offset = (page - 1) * limit;
|
|
55
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereSql} LIMIT ${offset}, ${limit}`;
|
|
56
|
+
|
|
57
|
+
const result = await this.query(sql, whereParams);
|
|
58
|
+
const queryTotal = await this.queryOne(`SELECT COUNT(*) as count FROM ${this.tableName} ${whereSql}`, whereParams);
|
|
59
|
+
const total = queryTotal ? queryTotal.count : 0;
|
|
60
|
+
const totalPages = Math.ceil(total / limit);
|
|
61
|
+
const hasPreviousPage = page > 1;
|
|
62
|
+
const hasNextPage = page < totalPages;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
items: result,
|
|
66
|
+
paginateButtons: Section.makeManualPaginateButtons({
|
|
67
|
+
callbackDataAction: route,
|
|
68
|
+
paramsQuery: routeParams || {},
|
|
69
|
+
currentPage: page,
|
|
70
|
+
totalRecords: total,
|
|
71
|
+
perPage: limit,
|
|
72
|
+
} as MakeManualPaginateButtonsParams),
|
|
73
|
+
total,
|
|
74
|
+
totalPages,
|
|
75
|
+
hasPreviousPage,
|
|
76
|
+
hasNextPage,
|
|
77
|
+
currentPage: page,
|
|
78
|
+
} as PaginateResult;
|
|
79
|
+
}
|
|
80
|
+
}
|
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,7 +1,7 @@
|
|
|
1
1
|
import { Section, SectionOptions, InlineKeyboard } from "@2byte/tgbot-framework";
|
|
2
2
|
|
|
3
3
|
export default class HomeSection extends Section {
|
|
4
|
-
static command = "
|
|
4
|
+
static command = "start";
|
|
5
5
|
static description = "{{className}} Bot Home section";
|
|
6
6
|
static actionRoutes = {
|
|
7
7
|
"home.index": "index",
|
|
@@ -35,6 +35,7 @@ export default class HomeSection extends Section {
|
|
|
35
35
|
`;
|
|
36
36
|
|
|
37
37
|
await this.message(message)
|
|
38
|
+
.markdown()
|
|
38
39
|
.inlineKeyboard(this.mainInlineKeyboard)
|
|
39
40
|
.send();
|
|
40
41
|
}
|
|
@@ -55,6 +56,7 @@ export default class HomeSection extends Section {
|
|
|
55
56
|
`;
|
|
56
57
|
|
|
57
58
|
await this.message(message)
|
|
59
|
+
.markdown()
|
|
58
60
|
.inlineKeyboard([
|
|
59
61
|
[this.makeInlineButton("🏠 На главную", "home.index")],
|
|
60
62
|
])
|