@2byte/tgbot-framework 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2byte/tgbot-framework",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "A TypeScript framework for creating Telegram bots with sections-based architecture (Bun optimized)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,10 +1,13 @@
1
1
  import { App } from "./App";
2
2
 
3
- export abstract class ApiService {
3
+ export class ApiService<T = any> {
4
+
5
+ public name: string = "ApiService";
6
+
7
+ protected app!: App;
4
8
 
5
9
  constructor(
6
- protected app: App,
7
- public name: string
10
+ public params: T = {} as T
8
11
  ) {}
9
12
 
10
13
  async setup(): Promise<void> {
@@ -18,4 +21,9 @@ export abstract class ApiService {
18
21
  async run(): Promise<void> {
19
22
  // Implement your API logic here
20
23
  }
24
+
25
+ public setApp(app: App): this {
26
+ this.app = app;
27
+ return this;
28
+ }
21
29
  }
@@ -51,13 +51,29 @@ export class ApiServiceManager {
51
51
  }
52
52
  }
53
53
 
54
+ public async runAllServices(): Promise<void> {
55
+ for (const [name, service] of this.services) {
56
+ this.app.debugLog(`Running API service: ${name}`);
57
+ await service.run();
58
+ this.app.debugLog(`API service run completed: ${name}`);
59
+ }
60
+ }
61
+
54
62
  public getAll(): Map<string, ApiService> {
55
63
  return this.services;
56
64
  }
57
65
 
66
+ public async setupAllServices(): Promise<void> {
67
+ for (const [name, service] of this.services) {
68
+ this.app.debugLog(`Setting up API service: ${name}`);
69
+ await this.setupService(name);
70
+ this.app.debugLog(`API service setup completed: ${name}`);
71
+ }
72
+ }
73
+
58
74
  public async unsetupAllServices(): Promise<void> {
59
75
  for (const [name, service] of this.services) {
60
- await service.unsetup();
76
+ await this.unsetupService(name);
61
77
  }
62
78
  }
63
79
  }
package/src/core/App.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  import { nameToCapitalize } from "./utils";
22
22
  import { ApiServiceManager } from "./ApiServiceManager";
23
23
  import { message } from "telegraf/filters";
24
+ import { ApiService } from "./ApiService";
24
25
 
25
26
  export class App {
26
27
  private config: AppConfig = {
@@ -43,6 +44,7 @@ export class App {
43
44
  terminateSigTerm: true,
44
45
  keepSectionInstances: false,
45
46
  botCwd: process.cwd(),
47
+ services: [],
46
48
  };
47
49
 
48
50
  public bot!: Telegraf<Telegraf2byteContext>;
@@ -188,6 +190,11 @@ export class App {
188
190
  return this;
189
191
  }
190
192
 
193
+ services(services: ApiService[]): this {
194
+ this.app.config.services = services;
195
+ return this;
196
+ }
197
+
191
198
  build(): App {
192
199
  return this.app;
193
200
  }
@@ -274,7 +281,7 @@ export class App {
274
281
  }
275
282
 
276
283
  // Check access by username and register user if not exists
277
- if (!this.config.userStorage.exists(tgUsername)) {
284
+ if (!this.config.userStorage.exists(tgUsername) && !this.rememberUser(tgUsername)) {
278
285
  const isAuthByUsername = !this.config.accessPublic && !accessKey;
279
286
 
280
287
  // check access by username for private bots
@@ -288,6 +295,7 @@ export class App {
288
295
  checkAccess &&
289
296
  checkAccess.every((name) => name.toLowerCase() !== requestUsername.toLowerCase())
290
297
  ) {
298
+ this.debugLog("Username access denied:", requestUsername);
291
299
  return ctx.reply("Access denied. Your username is not in the access list.");
292
300
  }
293
301
  this.debugLog("Username access granted.");
@@ -438,7 +446,10 @@ export class App {
438
446
  }
439
447
  });
440
448
  } else {
441
- this.debugLog("Message input already handled by awaitingInput or awaitingInputPromise. stateAfterValidatedUserResponse:", ctx.userSession.stateAfterValidatedUserResponse);
449
+ this.debugLog(
450
+ "Message input already handled by awaitingInput or awaitingInputPromise. stateAfterValidatedUserResponse:",
451
+ ctx.userSession.stateAfterValidatedUserResponse
452
+ );
442
453
  }
443
454
 
444
455
  delete ctx.userSession.stateAfterValidatedUserResponse; // Clear the state after handling the message
@@ -462,7 +473,7 @@ export class App {
462
473
  // Get the largest photo (the last one in the array is usually the largest)
463
474
  const largestPhoto = photo[photo.length - 1];
464
475
  await this.handleUserInput(ctx, largestPhoto, "photo");
465
-
476
+
466
477
  delete ctx.userSession.stateAfterValidatedUserResponse; // Clear the state after handling the message
467
478
  });
468
479
  }
@@ -470,39 +481,38 @@ export class App {
470
481
  private async registerServices() {
471
482
  this.apiServiceManager = ApiServiceManager.init(this);
472
483
 
473
- const registerServices = async (pathDirectory: string) => {
474
- try {
475
- await this.apiServiceManager.loadServicesFromDirectory(pathDirectory);
476
- } catch (error) {
477
- this.debugLog("Error loading services:", error);
478
- throw error;
479
- }
484
+ // Register services from config
485
+ this.debugLog(
486
+ "Registering services from config:",
487
+ this.config.services.map((service) => service.constructor.name)
488
+ );
480
489
 
481
- this.debugLog(
482
- "Registered API services:%s in dir: %s",
483
- Array.from(this.apiServiceManager.getAll().keys()),
484
- pathDirectory
485
- );
490
+ this.config.services.forEach((service) => {
491
+ this.debugLog(`Registering service: ${service.constructor.name}`);
492
+ this.apiServiceManager.registerService(service.name, service.setApp(this));
493
+ this.debugLog(`Service ${service.constructor.name} registered`);
494
+ });
486
495
 
487
- for (const [name, service] of this.apiServiceManager.getAll()) {
488
- await service.setup();
489
- this.debugLog(`Service ${name} setup completed`);
490
- await service.run();
491
- this.debugLog(`Service ${name} run completed`);
492
- }
493
- };
496
+ try {
497
+ await this.apiServiceManager.setupAllServices();
498
+ } catch (error) {
499
+ this.debugLog("Error setting up services:", error);
500
+ throw error;
501
+ }
494
502
 
495
- // Register services from bot directory
496
- await registerServices(this.config.botCwd + "/workflow/services");
497
- // Register services from framework directory
498
- await registerServices(path.resolve(__dirname, "../workflow/services"));
503
+ try {
504
+ await this.apiServiceManager.runAllServices();
505
+ } catch (error) {
506
+ this.debugLog("Error running services:", error);
507
+ throw error;
508
+ }
499
509
  }
500
510
 
501
511
  private async unregisterServices() {
502
512
  this.apiServiceManager = ApiServiceManager.init(this);
503
513
 
504
514
  try {
505
- this.apiServiceManager.unsetupAllServices();
515
+ await this.apiServiceManager.unsetupAllServices();
506
516
  } catch (error) {
507
517
  this.debugLog("Error unsetting up services:", error);
508
518
  throw error;
@@ -923,7 +933,7 @@ export class App {
923
933
  if (sectionRoute.hasTriggers()) {
924
934
  this.debugLog("Section route has triggers, executing them before running method:", sectionId);
925
935
  sectionRoute.getTriggers().forEach((trigger) => {
926
- if (trigger.name === 'cbBeforeRunMethod') {
936
+ if (trigger.name === "cbBeforeRunMethod") {
927
937
  this.debugLog(`Executing cbBeforeRunMethod trigger for section ${sectionId}`);
928
938
  this.debugLog("Trigger details:", trigger);
929
939
 
@@ -1019,6 +1029,32 @@ export class App {
1019
1029
  }
1020
1030
  }
1021
1031
 
1032
+ /**
1033
+ * Remembers a user in storage by their Telegram username. If the user does not exist in storage, it attempts to fetch the user from the database and add them to storage. This is useful for ensuring that the storage has the latest user data from the database, especially in cases where user information might have been updated.
1034
+ * @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.
1035
+ * @returns A boolean indicating whether the user was successfully remembered (true) or not (false).
1036
+ */
1037
+ async rememberUser(tgUsername: string): Promise<boolean> {
1038
+ if (this.config.userStorage && !this.config.userStorage.exists(tgUsername)) {
1039
+ this.debugLog("Warning: Username not found in storage:", tgUsername);
1040
+ this.debugLog("Trying getting to database:", tgUsername);
1041
+
1042
+ // Try to get user from database and add to storage
1043
+ UserModel.resolveDb();
1044
+ const userFromDb = UserModel.findByUsername(tgUsername);
1045
+
1046
+ if (userFromDb) {
1047
+ this.config.userStorage.add(tgUsername, userFromDb);
1048
+ this.debugLog("Success: User found in database and added to storage:", tgUsername);
1049
+ this.debugLog('Success: Remembered user "' + tgUsername + '"');
1050
+ return true;
1051
+ } else {
1052
+ this.debugLog("Warning: User not found in database:", tgUsername);
1053
+ }
1054
+ }
1055
+ return false;
1056
+ }
1057
+
1022
1058
  /**
1023
1059
  * Runs a task with bidirectional communication support
1024
1060
  * @param ctx Telegram context
@@ -1227,9 +1263,71 @@ export class App {
1227
1263
  }
1228
1264
 
1229
1265
  debugLog(...args: any[]): void {
1230
- if (this.config.debug) {
1231
- console.log(...args);
1232
- }
1266
+ if (!this.config.debug) return;
1267
+
1268
+ // Color palette
1269
+ const colors = {
1270
+ reset: "\x1b[0m",
1271
+ bright: "\x1b[1m",
1272
+ dim: "\x1b[2m",
1273
+ underscore: "\x1b[4m",
1274
+ fg: {
1275
+ red: "\x1b[31m",
1276
+ green: "\x1b[32m",
1277
+ yellow: "\x1b[33m",
1278
+ blue: "\x1b[34m",
1279
+ magenta: "\x1b[35m",
1280
+ cyan: "\x1b[36m",
1281
+ white: "\x1b[37m",
1282
+ },
1283
+ bg: {
1284
+ red: "\x1b[41m",
1285
+ green: "\x1b[42m",
1286
+ yellow: "\x1b[43m",
1287
+ blue: "\x1b[44m",
1288
+ magenta: "\x1b[45m",
1289
+ cyan: "\x1b[46m",
1290
+ white: "\x1b[47m",
1291
+ },
1292
+ };
1293
+
1294
+ // Timestamp
1295
+ const now = new Date();
1296
+ const timestamp = `${colors.dim}${colors.fg.cyan}[${now.toLocaleTimeString()}]${colors.reset}`;
1297
+
1298
+ // Source (App debug)
1299
+ const source = `${colors.bright}${colors.fg.magenta}AppDebug${colors.reset}`;
1300
+
1301
+ // Format args: highlight objects, errors, etc.
1302
+ const formattedArgs = args.map(arg => {
1303
+ if (arg instanceof Error) {
1304
+ return `${colors.fg.red}${arg.stack || arg.message}${colors.reset}`;
1305
+ }
1306
+ if (typeof arg === "object" && arg !== null) {
1307
+ try {
1308
+ return `${colors.fg.yellow}${JSON.stringify(arg, null, 2)}${colors.reset}`;
1309
+ } catch {
1310
+ return `${colors.fg.yellow}[Object]${colors.reset}`;
1311
+ }
1312
+ }
1313
+ if (typeof arg === "string") {
1314
+ // Highlight keywords
1315
+ if (/error|fail|exception/i.test(arg)) {
1316
+ return `${colors.fg.red}${arg}${colors.reset}`;
1317
+ }
1318
+ if (/success|done|complete/i.test(arg)) {
1319
+ return `${colors.fg.green}${arg}${colors.reset}`;
1320
+ }
1321
+ if (/warn|warning/i.test(arg)) {
1322
+ return `${colors.fg.yellow}${arg}${colors.reset}`;
1323
+ }
1324
+ return `${colors.fg.white}${arg}${colors.reset}`;
1325
+ }
1326
+ return String(arg);
1327
+ });
1328
+
1329
+ // Compose and print
1330
+ console.log(`${timestamp} ${source}:`, ...formattedArgs);
1233
1331
  }
1234
1332
 
1235
1333
  get sections(): SectionList {
package/src/core/Model.ts CHANGED
@@ -14,7 +14,7 @@ export abstract class Model {
14
14
  this.db = database;
15
15
  }
16
16
 
17
- protected static resolveDb(): Database {
17
+ public static resolveDb(): Database {
18
18
  if (globalThis.db) {
19
19
  this.db = globalThis.db;
20
20
  } else {
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export { App } from './core/App';
3
3
  export { ApiService } from './core/ApiService';
4
4
  export { ApiServiceManager } from './core/ApiServiceManager';
5
+ export { MassSendApiService } from './workflow/services/MassSendApiService';
5
6
  export { BotArtisan } from './core/BotArtisan';
6
7
  export { BotMigration } from './core/BotMigration';
7
8
  export { BotSeeder } from './core/BotSeeder';
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ import { Section } from './illumination/Section';
3
3
  import { UserStore } from './user/UserStore';
4
4
  import { Telegraf2byteContext } from './illumination/Telegraf2byteContext';
5
5
  import { RunSectionRoute } from './illumination/RunSectionRoute';
6
+ import { ApiService } from './core/ApiService';
6
7
 
7
8
  export interface AppConfig {
8
9
  accessPublic: boolean;
@@ -24,6 +25,7 @@ export interface AppConfig {
24
25
  terminateSigTerm: boolean;
25
26
  keepSectionInstances: boolean;
26
27
  botCwd: string;
28
+ services: ApiService[];
27
29
  }
28
30
 
29
31
  export interface SectionOptions {
@@ -1,83 +1,94 @@
1
- import { ExtraReplyMessage } from 'telegraf/typings/telegram-types';
2
- import { ApiService} from '../../core/ApiService';
3
- import { App } from '../../core/App';
4
- import { UserModel } from '../../user/UserModel';
5
-
6
- export default class MassSendApiService extends ApiService {
7
-
8
- private bunServerInstance: any;
9
-
10
- constructor(
11
- protected app: App,
12
- public name: string = "MassSendApiService"
13
- ) {
14
- super(app, name);
15
- }
16
-
17
- public async setup(): Promise<void> {
18
- return Promise.resolve();
19
- }
20
-
21
- public async unsetup(): Promise<void> {
22
- return Promise.resolve();
23
- }
24
-
25
- public async run(): Promise<void> {
26
- this.bunServerInstance = Bun.serve({
27
- port: this.app.configApp.envConfig.BOT_APP_API_PORT || 3033,
28
- routes: {
29
- '/': async (req) => {
30
- const receivedData = (await req.json()) as { userIds?: number[]; message?: string, extra?: ExtraReplyMessage };
31
- this.app.debugLog("Received data for mass message:", receivedData);
32
-
33
- let userIds: number[] = [];
34
- let message: string = "Hello from MassSendApiService";
35
-
36
- if (receivedData && typeof receivedData == 'object') {
37
- userIds = receivedData?.userIds || [];
38
- message = receivedData?.message || "Hello from MassSendApiService";
39
- }
40
-
41
- this.sendMassMessage(userIds, message, receivedData.extra);
42
-
43
- return Response.json({ status: 200, body: 'Mass message sending initiated.' });
44
- }
45
- }
46
- });
47
-
48
- this.app.debugLog(`MassSendApiService Bun server running at http://localhost:${this.bunServerInstance.port}/`);
49
-
50
- return Promise.resolve();
51
- }
52
-
53
- private async sendMassMessage(userIds: number[] = [], message: string, extra?: ExtraReplyMessage): Promise<void> {
54
- if (userIds.length === 0) {
55
-
56
- if (!db) {
57
- throw new Error("Database connection is not established.");
58
- }
59
-
60
- UserModel.setDatabase(db);
61
-
62
- this.app.debugLog("Fetching all users for mass message...");
63
- const users = UserModel.getAll();
64
-
65
- this.app.debugLog("Fetched users for mass message:", users);
66
-
67
- if (users && users.length > 0) {
68
- for (const user of users) {
69
- this.app.debugLog(`Sending message to user ID: ${user.tgId} username: ${user.username}`);
70
-
71
- try {
72
- const extraOptions = extra || {};
73
- await this.app.bot.telegram.sendMessage(user.tgId, message, extraOptions);
74
- this.app.debugLog(`Message sent to user ID: ${user.tgId} username: ${user.username}`);
75
- } catch (error) {
76
- this.app.debugLog(`Sending message ${message} to user ID: ${user.tgId} username: ${user.username} failed`, error);
77
- this.app.debugLog(`Failed to send message to user ID: ${user.tgId} username: ${user.username}`, error);
78
- }
79
- }
80
- }
1
+ import { ExtraReplyMessage } from "telegraf/typings/telegram-types";
2
+ import { ApiService } from "../../core/ApiService";
3
+ import { UserModel } from "../../user/UserModel";
4
+
5
+ export type MassSendApiParams = {
6
+ port?: number;
7
+ };
8
+
9
+ export class MassSendApiService extends ApiService<MassSendApiParams> {
10
+ public override name = "MassSendApiService";
11
+ private bunServerInstance: any;
12
+
13
+ public async setup(): Promise<void> {
14
+ return Promise.resolve();
15
+ }
16
+
17
+ public async unsetup(): Promise<void> {
18
+ return Promise.resolve();
19
+ }
20
+
21
+ public async run(): Promise<void> {
22
+ this.bunServerInstance = Bun.serve({
23
+ port: this.params.port || this.app.configApp.envConfig.BOT_APP_API_PORT || 3033,
24
+ routes: {
25
+ "/": async (req) => {
26
+ const receivedData = (await req.json()) as {
27
+ userIds?: number[];
28
+ message?: string;
29
+ extra?: ExtraReplyMessage;
30
+ };
31
+ this.app.debugLog("Received data for mass message:", receivedData);
32
+
33
+ let userIds: number[] = [];
34
+ let message: string = "Hello from MassSendApiService";
35
+
36
+ if (receivedData && typeof receivedData == "object") {
37
+ userIds = receivedData?.userIds || [];
38
+ message = receivedData?.message || "Hello from MassSendApiService";
39
+ }
40
+
41
+ this.sendMassMessage(userIds, message, receivedData.extra);
42
+
43
+ return Response.json({ status: 200, body: "Mass message sending initiated." });
44
+ },
45
+ },
46
+ });
47
+
48
+ this.app.debugLog(
49
+ `MassSendApiService Bun server running at http://localhost:${this.bunServerInstance.port}/`
50
+ );
51
+
52
+ return Promise.resolve();
53
+ }
54
+
55
+ private async sendMassMessage(
56
+ userIds: number[] = [],
57
+ message: string,
58
+ extra?: ExtraReplyMessage
59
+ ): Promise<void> {
60
+ if (userIds.length === 0) {
61
+ if (!db) {
62
+ throw new Error("Database connection is not established.");
63
+ }
64
+
65
+ UserModel.setDatabase(db);
66
+
67
+ this.app.debugLog("Fetching all users for mass message...");
68
+ const users = UserModel.getAll();
69
+
70
+ this.app.debugLog("Fetched users for mass message:", users);
71
+
72
+ if (users && users.length > 0) {
73
+ for (const user of users) {
74
+ this.app.debugLog(`Sending message to user ID: ${user.tgId} username: ${user.username}`);
75
+
76
+ try {
77
+ const extraOptions = extra || {};
78
+ await this.app.bot.telegram.sendMessage(user.tgId, message, extraOptions);
79
+ this.app.debugLog(`Message sent to user ID: ${user.tgId} username: ${user.username}`);
80
+ } catch (error) {
81
+ this.app.debugLog(
82
+ `Sending message ${message} to user ID: ${user.tgId} username: ${user.username} failed`,
83
+ error
84
+ );
85
+ this.app.debugLog(
86
+ `Failed to send message to user ID: ${user.tgId} username: ${user.username}`,
87
+ error
88
+ );
89
+ }
81
90
  }
91
+ }
82
92
  }
93
+ }
83
94
  }
@@ -21,7 +21,7 @@
21
21
  "author": "{{author}}",
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "@2byte/tgbot-framework": "^1.0.13"
24
+ "@2byte/tgbot-framework": "^1.0.14"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.19.8",