@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,11 +1,87 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
105
|
+
port,
|
|
24
106
|
routes: {
|
|
25
|
-
"/": async (req) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
70
233
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|