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