@2byte/tgbot-framework 1.0.18 → 1.0.20

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.18",
3
+ "version": "1.0.20",
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",
package/src/core/App.ts CHANGED
@@ -259,10 +259,12 @@ export class App {
259
259
 
260
260
  async mainMiddleware() {
261
261
  this.bot.use(async (ctx: Telegraf2byteContext, next: () => Promise<void>) => {
262
- const tgUsername = this.getTgUsername(ctx);
262
+ const genTgUsername = () => `unknown_${ctx.from?.id || "user"}`;
263
+ const tgId = this.getTgId(ctx);
264
+ const tgUsername = this.getTgUsername(ctx) || genTgUsername();
263
265
 
264
- if (!tgUsername) {
265
- return ctx.reply("Username is not set");
266
+ if (!tgId) {
267
+ return ctx.reply("Unable to identify user");
266
268
  }
267
269
 
268
270
  if (!this.config.userStorage) {
@@ -272,6 +274,7 @@ export class App {
272
274
  let startPayload: string | null = null;
273
275
  let accessKey: string | null = null;
274
276
 
277
+ // If the message starts with /start, we can check for access key in the payload for private bots. The payload can be in format /start key=ACCESS_KEY or /start refid=USER_REF_ID or /start key=ACCESS_KEY refid=USER_REF_ID
275
278
  if (ctx?.message?.text?.startsWith("/start")) {
276
279
  startPayload = ctx?.message?.text?.split(" ")[1] || null;
277
280
  accessKey =
@@ -279,9 +282,13 @@ export class App {
279
282
  ? startPayload.split("key=")[1] || null
280
283
  : null;
281
284
  }
285
+ // Also check for access key in message text for cases when user sends the access key separately after /start command
286
+ if (!accessKey && ctx?.message?.text && ctx?.message?.text?.length == 48) {
287
+ accessKey = ctx.message.text.trim();
288
+ }
282
289
 
283
- // Check access by username and register user if not exists
284
- if (!this.config.userStorage.exists(tgUsername) && !this.rememberUser(tgUsername)) {
290
+ // Check access by id and register user if not exists
291
+ if (!this.config.userStorage.exists(tgId) && !this.rememberUserById(tgId)) {
285
292
  const isAuthByUsername = !this.config.accessPublic && !accessKey;
286
293
  this.debugLog(
287
294
  "Access control check. isAuthByUsername:",
@@ -293,12 +300,12 @@ export class App {
293
300
  if (isAuthByUsername) {
294
301
  const requestUsername = this.getTgUsername(ctx);
295
302
  this.debugLog("Private access mode. Checking username:", requestUsername);
296
- const checkAccess =
303
+ const accessUsernameAllowed =
297
304
  this.config.envConfig.ACCESS_USERNAMES &&
298
305
  this.config.envConfig.ACCESS_USERNAMES.split(",").map((name) => name.trim());
299
306
  if (
300
- checkAccess &&
301
- checkAccess.every((name) => name.toLowerCase() !== requestUsername.toLowerCase())
307
+ accessUsernameAllowed &&
308
+ accessUsernameAllowed.every((name) => name.toLowerCase() !== requestUsername.toLowerCase())
302
309
  ) {
303
310
  this.debugLog("Username access denied:", requestUsername);
304
311
  return ctx.reply("Access denied. Your username is not in the access list.");
@@ -337,7 +344,7 @@ export class App {
337
344
  user_refid: userRefIdFromStart,
338
345
  tg_id: ctx.from.id,
339
346
  tg_username: tgUsername,
340
- tg_first_name: ctx.from.first_name || tgUsername,
347
+ tg_first_name: ctx.from.first_name || "",
341
348
  tg_last_name: ctx.from.last_name || "",
342
349
  role: "user",
343
350
  language: ctx.from.language_code || "en",
@@ -346,15 +353,15 @@ export class App {
346
353
  );
347
354
  }
348
355
 
349
- ctx.user = this.config.userStorage.find(tgUsername);
356
+ ctx.user = this.config.userStorage.find(tgId);
350
357
  ctx.userStorage = this.config.userStorage;
351
358
  ctx.userSession = this.config.userStorage.findSession(ctx.user);
352
359
  Object.assign(ctx, Telegraf2byteContextExtraMethods);
353
360
 
354
- this.config.userStorage.upActive(tgUsername);
361
+ this.config.userStorage.upActive(tgId);
355
362
 
356
363
  if (ctx.msgId) {
357
- this.config.userStorage.storeMessageId(tgUsername, ctx.msgId, 10);
364
+ this.config.userStorage.storeMessageId(tgId, ctx.msgId, 10);
358
365
  }
359
366
 
360
367
  return next();
@@ -1080,8 +1087,8 @@ export class App {
1080
1087
  AccessKey.markUsed(accessKey, user.id);
1081
1088
  }
1082
1089
  if (this.config.userStorage) {
1083
- this.config.userStorage.add(data.tg_username, user);
1084
- this.debugLog("User added to storage:", data.tg_username);
1090
+ this.config.userStorage.add(data.tg_id, user);
1091
+ this.debugLog("User added to storage by tgId:", data.tg_id);
1085
1092
  }
1086
1093
 
1087
1094
  return user;
@@ -1092,26 +1099,25 @@ export class App {
1092
1099
  }
1093
1100
 
1094
1101
  /**
1095
- * 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.
1096
- * @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.
1102
+ * Remembers a user in storage by their Telegram ID. If the user does not exist in storage, it attempts to fetch the user from the database and add them to storage.
1103
+ * @param tgId Telegram user ID of the user to remember.
1097
1104
  * @returns A boolean indicating whether the user was successfully remembered (true) or not (false).
1098
1105
  */
1099
- rememberUser(tgUsername: string): boolean {
1100
- if (this.config.userStorage && !this.config.userStorage.exists(tgUsername)) {
1101
- this.debugLog("Warning: Remembering, Username not found in storage:", tgUsername);
1102
- this.debugLog("Remembering, Trying getting to database:", tgUsername);
1106
+ rememberUserById(tgId: number): boolean {
1107
+ if (this.config.userStorage && !this.config.userStorage.exists(tgId)) {
1108
+ this.debugLog("Warning: Remembering, tgId not found in storage:", tgId);
1109
+ this.debugLog("Remembering, Trying getting from database:", tgId);
1103
1110
 
1104
1111
  // Try to get user from database and add to storage
1105
1112
  UserModel.resolveDb();
1106
- const userFromDb = UserModel.findByUsername(tgUsername);
1113
+ const userFromDb = UserModel.findByTgId(tgId);
1107
1114
 
1108
1115
  if (userFromDb) {
1109
- this.config.userStorage.add(tgUsername, userFromDb);
1110
- this.debugLog("Success: User found in database and added to storage:", tgUsername);
1111
- this.debugLog('Success: Remembered user "' + tgUsername + '"');
1116
+ this.config.userStorage.add(tgId, userFromDb);
1117
+ this.debugLog("Success: User found in database and added to storage by tgId:", tgId);
1112
1118
  return true;
1113
1119
  } else {
1114
- this.debugLog("Warning: Remembering, User not found in database:", tgUsername);
1120
+ this.debugLog("Warning: Remembering, User not found in database for tgId:", tgId);
1115
1121
  }
1116
1122
  }
1117
1123
  return false;
@@ -131,7 +131,7 @@ export class UserModel extends Model {
131
131
  });
132
132
  }
133
133
 
134
- const user = this.findByUsername(params.tg_username);
134
+ const user = this.findByTgId(params.tg_id);
135
135
 
136
136
  if (user) {
137
137
  return user;
@@ -168,6 +168,20 @@ export class UserModel extends Model {
168
168
  return undefined;
169
169
  }
170
170
 
171
+ static findByTgId(tgId: number): UserModel | undefined {
172
+ if (this.db) {
173
+ const userData = this.queryOne(`SELECT * FROM ${this.tableName} WHERE tg_id = ?`, [tgId]);
174
+
175
+ if (userData) {
176
+ return UserModel.make(userData);
177
+ }
178
+ } else {
179
+ throw new Error("Database connection is not set.");
180
+ }
181
+
182
+ return undefined;
183
+ }
184
+
171
185
  static async findOnServer(tgUsername: string): Promise<UserModel> {
172
186
  // Здесь должен быть запрос к API, но мы оставим это для будущей реализации
173
187
  // const user = await api.fetch("user/get/" + tgUsername);
@@ -189,7 +203,7 @@ export class UserModel extends Model {
189
203
  async refresh(): Promise<boolean> {
190
204
  if (this.userSession) {
191
205
  return this.userSession.add(
192
- this.attributes.tg_username,
206
+ this.attributes.tg_id,
193
207
  await UserModel.findOnServer(this.attributes.tg_username)
194
208
  );
195
209
  }
@@ -27,11 +27,11 @@ export class UserStore {
27
27
  }
28
28
 
29
29
  async checkOrRegister(params: { tgUsername: string, tgId: number, tgName: string }): Promise<any> {
30
- if (!this.usersMap.has(params.tgUsername) && this.api) {
30
+ if (!this.usersMap.has(String(params.tgId)) && this.api) {
31
31
  let userData = null;
32
32
 
33
33
  try {
34
- let resApi = await this.api.fetch('user/get/' + params.tgUsername);
34
+ let resApi = await this.api.fetch('user/get/' + params.tgId);
35
35
  userData = resApi.data;
36
36
  } catch (err) {
37
37
  let resApi = await this.api.fetch('user/register', 'post', {
@@ -44,7 +44,7 @@ export class UserStore {
44
44
  }
45
45
 
46
46
  if (userData !== null) {
47
- this.add(params.tgUsername, userData);
47
+ this.add(params.tgId, userData);
48
48
  }
49
49
 
50
50
  return userData;
@@ -52,48 +52,49 @@ export class UserStore {
52
52
  return null;
53
53
  }
54
54
 
55
- find(tgUsername: string): UserModel {
56
- return this.usersMap.get(tgUsername) as UserModel;
55
+ find(tgId: number): UserModel {
56
+ return this.usersMap.get(String(tgId)) as UserModel;
57
57
  }
58
58
 
59
59
  findSession(userModel: UserModel): UserSession {
60
60
  return this.usersSession.get(userModel) || {};
61
61
  }
62
62
 
63
- exists(tgUsername: string): boolean {
64
- return this.usersMap.has(tgUsername);
63
+ exists(tgId: number): boolean {
64
+ return this.usersMap.has(String(tgId));
65
65
  }
66
66
 
67
- add(tgUsername: string, data: UserModel | any): boolean {
67
+ add(tgId: number, data: UserModel | any): boolean {
68
+ const key = String(tgId);
68
69
  if (data instanceof UserModel) {
69
- this.usersMap.set(tgUsername, data);
70
+ this.usersMap.set(key, data);
70
71
  } else {
71
- this.usersMap.set(tgUsername, new UserModel(data));
72
+ this.usersMap.set(key, new UserModel(data));
72
73
  }
73
-
74
- this.usersSession.set(this.usersMap.get(tgUsername) as UserModel, {});
74
+
75
+ this.usersSession.set(this.usersMap.get(key) as UserModel, {});
75
76
 
76
77
  return true;
77
78
  }
78
79
 
79
- upActive(tgUsername: string): this {
80
- const user = this.usersMap.get(tgUsername);
80
+ upActive(tgId: number): this {
81
+ const user = this.usersMap.get(String(tgId));
81
82
  if (user) {
82
83
  user.upActive();
83
84
  }
84
85
  return this;
85
86
  }
86
87
 
87
- storeMessageId(tgUsername: string, messageId: number, limit: number): this {
88
- const user = this.usersMap.get(tgUsername);
88
+ storeMessageId(tgId: number, messageId: number, limit: number): this {
89
+ const user = this.usersMap.get(String(tgId));
89
90
  if (user) {
90
91
  user.storeMessageId(messageId, limit);
91
92
  }
92
93
  return this;
93
94
  }
94
95
 
95
- getLastMessageId(tgUsername: string): number | undefined {
96
- const user = this.usersMap.get(tgUsername);
96
+ getLastMessageId(tgId: number): number | undefined {
97
+ const user = this.usersMap.get(String(tgId));
97
98
  if (user) {
98
99
  const ids = user.lastMessageIds;
99
100
  return ids.length ? ids[ids.length - 1] : undefined;
@@ -103,7 +104,7 @@ export class UserStore {
103
104
 
104
105
  autocleanup(minutes: number = 1): void {
105
106
  const getNotActiveUsers = (): [string, UserModel][] => {
106
- return Array.from(this.usersMap).filter(([tgUsername, data]) => {
107
+ return Array.from(this.usersMap).filter(([_key, data]) => {
107
108
  return (
108
109
  data.serviceAttributes.lastActive.getTime() <= Date.now() - minutes * 60 * 1000
109
110
  );
@@ -111,17 +112,18 @@ export class UserStore {
111
112
  };
112
113
 
113
114
  setInterval(() => {
114
- getNotActiveUsers().forEach(([tgUsername]) => {
115
- this.usersMap.delete(tgUsername);
115
+ getNotActiveUsers().forEach(([key]) => {
116
+ this.usersMap.delete(key);
116
117
  });
117
118
  }, 60 * 1000);
118
119
  }
119
120
 
120
121
  deleteByUsername(tgUsername: string): boolean {
121
- const user = this.usersMap.get(tgUsername);
122
- if (user) {
122
+ const entry = Array.from(this.usersMap).find(([_, user]) => user.username === tgUsername);
123
+ if (entry) {
124
+ const [key, user] = entry;
123
125
  this.usersSession.delete(user);
124
- return this.usersMap.delete(tgUsername);
126
+ return this.usersMap.delete(key);
125
127
  }
126
128
  return false;
127
129
  }
@@ -129,9 +131,9 @@ export class UserStore {
129
131
  deleteById(userId: number): boolean {
130
132
  const userEntry = Array.from(this.usersMap).find(([_, user]) => user.id === userId);
131
133
  if (userEntry) {
132
- const [tgUsername, user] = userEntry;
134
+ const [key, user] = userEntry;
133
135
  this.usersSession.delete(user);
134
- return this.usersMap.delete(tgUsername);
136
+ return this.usersMap.delete(key);
135
137
  }
136
138
  return false;
137
139
  }
@@ -1,11 +1,87 @@
1
- import { ExtraReplyMessage } from "telegraf/typings/telegram-types";
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { basename } from "path";
2
3
  import { ApiService } from "../../core/ApiService";
3
4
  import { UserModel } from "../../user/UserModel";
4
5
 
5
6
  export type MassSendApiParams = {
6
- port?: number;
7
+ port?: number;
7
8
  };
8
9
 
10
+ /** Supported mass-send message types */
11
+ export type MassSendType = "text" | "photo" | "video" | "document" | "audio" | "animation" | "voice";
12
+
13
+ /** Base fields common to all mass-send requests */
14
+ export interface MassSendBasePayload {
15
+ /** Telegram user IDs to send to. Omit or pass empty array to send to ALL users. */
16
+ userIds?: number[];
17
+ /** Comma-separated list of tg_usernames to send to (resolves alongside userIds). */
18
+ usernames?: string[];
19
+ /** Parse mode for text / caption: "HTML" | "Markdown" | "MarkdownV2" */
20
+ parseMode?: "HTML" | "Markdown" | "MarkdownV2";
21
+ /** Milliseconds delay between each message to avoid flood limits. Default: 50ms */
22
+ delay?: number;
23
+ }
24
+
25
+ export interface MassSendTextPayload extends MassSendBasePayload {
26
+ type: "text";
27
+ message: string;
28
+ }
29
+
30
+ export interface MassSendPhotoPayload extends MassSendBasePayload {
31
+ type: "photo";
32
+ /** file_id, URL or local path accessible from the bot */
33
+ media: string;
34
+ caption?: string;
35
+ }
36
+
37
+ export interface MassSendVideoPayload extends MassSendBasePayload {
38
+ type: "video";
39
+ media: string;
40
+ caption?: string;
41
+ /** Send video without sound (as animation) */
42
+ supportsStreaming?: boolean;
43
+ }
44
+
45
+ export interface MassSendDocumentPayload extends MassSendBasePayload {
46
+ type: "document";
47
+ media: string;
48
+ caption?: string;
49
+ }
50
+
51
+ export interface MassSendAudioPayload extends MassSendBasePayload {
52
+ type: "audio";
53
+ media: string;
54
+ caption?: string;
55
+ /** Duration in seconds */
56
+ duration?: number;
57
+ performer?: string;
58
+ title?: string;
59
+ }
60
+
61
+ export interface MassSendAnimationPayload extends MassSendBasePayload {
62
+ type: "animation";
63
+ media: string;
64
+ caption?: string;
65
+ }
66
+
67
+ export interface MassSendVoicePayload extends MassSendBasePayload {
68
+ type: "voice";
69
+ media: string;
70
+ caption?: string;
71
+ duration?: number;
72
+ }
73
+
74
+ export type MassSendPayload =
75
+ | MassSendTextPayload
76
+ | MassSendPhotoPayload
77
+ | MassSendVideoPayload
78
+ | MassSendDocumentPayload
79
+ | MassSendAudioPayload
80
+ | MassSendAnimationPayload
81
+ | MassSendVoicePayload;
82
+
83
+ // ---------------------------------------------------------------------------
84
+
9
85
  export class MassSendApiService extends ApiService<MassSendApiParams> {
10
86
  public override name = "MassSendApiService";
11
87
  private bunServerInstance: any;
@@ -15,79 +91,164 @@ export class MassSendApiService extends ApiService<MassSendApiParams> {
15
91
  }
16
92
 
17
93
  public async unsetup(): Promise<void> {
94
+ if (this.bunServerInstance) {
95
+ this.bunServerInstance.stop();
96
+ }
18
97
  return Promise.resolve();
19
98
  }
20
99
 
21
100
  public async run(): Promise<void> {
101
+ const port =
102
+ this.params.port || this.app.configApp.envConfig.BOT_APP_API_PORT || 3033;
103
+
22
104
  this.bunServerInstance = Bun.serve({
23
- port: this.params.port || this.app.configApp.envConfig.BOT_APP_API_PORT || 3033,
105
+ port,
24
106
  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
-
35
- if (receivedData && typeof receivedData == "object" && receivedData.message) {
36
- userIds = receivedData?.userIds || [];
37
- const message = receivedData?.message;
38
-
39
- this.sendMassMessage(userIds, message, receivedData.extra);
107
+ "/send": async (req) => {
108
+ if (req.method !== "POST") {
109
+ return Response.json({ status: 405, error: "Method Not Allowed" }, { status: 405 });
110
+ }
111
+ const payload = (await req.json()) as MassSendPayload;
112
+ this.app.debugLog("MassSendApiService received payload:", payload);
113
+
114
+ if (!payload || !payload.type) {
115
+ return Response.json(
116
+ { status: 400, error: 'Missing required field "type"' },
117
+ { status: 400 }
118
+ );
40
119
  }
41
120
 
42
- return Response.json({ status: 200, body: "Mass message sending initiated." });
121
+ // fire-and-forget
122
+ this.dispatch(payload);
123
+
124
+ return Response.json({ status: 200, message: "Mass send initiated." });
43
125
  },
126
+
127
+ // health-check
128
+ "/": async () => Response.json({ status: 200, service: "MassSendApiService" }),
44
129
  },
45
130
  });
46
131
 
47
132
  this.app.debugLog(
48
133
  `MassSendApiService Bun server running at http://localhost:${this.bunServerInstance.port}/`
49
134
  );
50
-
51
135
  return Promise.resolve();
52
136
  }
53
137
 
54
- private async sendMassMessage(
55
- userIds: number[] = [],
56
- message: string,
57
- extra?: ExtraReplyMessage
58
- ): Promise<void> {
59
- if (userIds.length === 0) {
60
- if (!db) {
61
- throw new Error("Database connection is not established.");
138
+ // ---------------------------------------------------------------------------
139
+ // Internal dispatcher
140
+ // ---------------------------------------------------------------------------
141
+
142
+ private async dispatch(payload: MassSendPayload): Promise<void> {
143
+ const chatIds = await this.resolveChatIds(payload);
144
+ const delay = payload.delay ?? 50;
145
+
146
+ for (const chatId of chatIds) {
147
+ try {
148
+ await this.sendOne(chatId, payload);
149
+ } catch (error) {
150
+ this.app.debugLog(`Failed to send to chatId ${chatId}:`, error);
62
151
  }
152
+ if (delay > 0) await this.sleep(delay);
153
+ }
154
+ }
63
155
 
64
- UserModel.setDatabase(db);
156
+ /**
157
+ * If `media` is a local file path that exists on disk, returns
158
+ * `{ source: Buffer, filename }` so Telegraf uploads it.
159
+ * Otherwise returns the string as-is (file_id or HTTPS URL).
160
+ */
161
+ private resolveMedia(media: string): string | { source: Buffer; filename: string } {
162
+ const isPath = media.startsWith("/") || media.startsWith("./") || media.startsWith("../");
163
+ if (isPath && existsSync(media)) {
164
+ return { source: readFileSync(media), filename: basename(media) };
165
+ }
166
+ return media;
167
+ }
65
168
 
66
- this.app.debugLog("Fetching all users for mass message...");
67
- const users = UserModel.getAll();
169
+ private async sendOne(chatId: number, payload: MassSendPayload): Promise<void> {
170
+ const tg = this.app.bot.telegram;
171
+ const parseMode = payload.parseMode;
172
+
173
+ switch (payload.type) {
174
+ case "text":
175
+ await tg.sendMessage(chatId, payload.message, { parse_mode: parseMode });
176
+ break;
177
+
178
+ case "photo":
179
+ await tg.sendPhoto(chatId, this.resolveMedia(payload.media) as any, {
180
+ caption: payload.caption,
181
+ parse_mode: parseMode,
182
+ });
183
+ break;
184
+
185
+ case "video":
186
+ await tg.sendVideo(chatId, this.resolveMedia(payload.media) as any, {
187
+ caption: payload.caption,
188
+ parse_mode: parseMode,
189
+ supports_streaming: (payload as MassSendVideoPayload).supportsStreaming,
190
+ });
191
+ break;
192
+
193
+ case "document":
194
+ await tg.sendDocument(chatId, this.resolveMedia(payload.media) as any, {
195
+ caption: payload.caption,
196
+ parse_mode: parseMode,
197
+ });
198
+ break;
199
+
200
+ case "audio":
201
+ await tg.sendAudio(chatId, this.resolveMedia(payload.media) as any, {
202
+ caption: payload.caption,
203
+ parse_mode: parseMode,
204
+ duration: (payload as MassSendAudioPayload).duration,
205
+ performer: (payload as MassSendAudioPayload).performer,
206
+ title: (payload as MassSendAudioPayload).title,
207
+ });
208
+ break;
209
+
210
+ case "animation":
211
+ await tg.sendAnimation(chatId, this.resolveMedia(payload.media) as any, {
212
+ caption: payload.caption,
213
+ parse_mode: parseMode,
214
+ });
215
+ break;
216
+
217
+ case "voice":
218
+ await tg.sendVoice(chatId, this.resolveMedia(payload.media) as any, {
219
+ caption: payload.caption,
220
+ parse_mode: parseMode,
221
+ duration: (payload as MassSendVoicePayload).duration,
222
+ });
223
+ break;
224
+
225
+ default:
226
+ this.app.debugLog(`Unknown message type: ${(payload as any).type}`);
227
+ }
228
+ }
68
229
 
69
- this.app.debugLog("Fetched users for mass message:", users);
230
+ // ---------------------------------------------------------------------------
231
+ // Helpers
232
+ // ---------------------------------------------------------------------------
70
233
 
71
- if (users && users.length > 0) {
72
- for (const user of users) {
73
- this.app.debugLog(`Sending message to user ID: ${user.tgId} username: ${user.username}`);
234
+ private async resolveChatIds(payload: MassSendBasePayload): Promise<number[]> {
235
+ const explicitIds: number[] = payload.userIds ?? [];
74
236
 
75
- try {
76
- const extraOptions = extra || {};
77
- await this.app.bot.telegram.sendMessage(user.tgId, message, extraOptions);
78
- this.app.debugLog(`Message sent to user ID: ${user.tgId} username: ${user.username}`);
79
- } catch (error) {
80
- this.app.debugLog(
81
- `Sending message ${message} to user ID: ${user.tgId} username: ${user.username} failed`,
82
- error
83
- );
84
- this.app.debugLog(
85
- `Failed to send message to user ID: ${user.tgId} username: ${user.username}`,
86
- error
87
- );
88
- }
89
- }
90
- }
237
+ if (explicitIds.length > 0) {
238
+ return explicitIds;
239
+ }
240
+
241
+ // Fetch from DB
242
+ if (!db) {
243
+ throw new Error("Database connection is not established.");
91
244
  }
245
+ UserModel.setDatabase(db);
246
+ this.app.debugLog("Fetching all users for mass send...");
247
+ const users = UserModel.getAll();
248
+ return users.map((u) => u.tgId);
249
+ }
250
+
251
+ private sleep(ms: number): Promise<void> {
252
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
253
  }
93
254
  }
@@ -21,7 +21,7 @@
21
21
  "author": "{{author}}",
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "@2byte/tgbot-framework": "^1.0.18"
24
+ "@2byte/tgbot-framework": "^1.0.20"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.19.8",