@2byte/tgbot-framework 1.0.16 → 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 +8 -1
- package/src/models/AccessKey.ts +103 -0
- package/src/models/index.ts +2 -1
- package/src/user/UserModel.ts +31 -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
|
/**
|
|
@@ -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/user/UserModel.ts
CHANGED
|
@@ -140,6 +140,20 @@ export class UserModel extends Model {
|
|
|
140
140
|
throw new Error("Error not found user params:" + JSON.stringify(params));
|
|
141
141
|
}
|
|
142
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
|
+
|
|
143
157
|
static findByUsername(tgUsername: string): UserModel | undefined {
|
|
144
158
|
if (this.db) {
|
|
145
159
|
const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE tg_username = ?`, [tgUsername]);
|
|
@@ -201,6 +215,23 @@ export class UserModel extends Model {
|
|
|
201
215
|
}
|
|
202
216
|
}
|
|
203
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
|
+
|
|
204
235
|
get username(): string {
|
|
205
236
|
return this.attributes.tg_username;
|
|
206
237
|
}
|