@2byte/tgbot-framework 1.0.18 → 1.0.19

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.19",
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,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.19"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^20.19.8",