@2byte/tgbot-framework 1.0.17 → 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 +1 -1
- package/src/core/App.ts +6 -7
- package/src/illumination/InlineKeyboard.ts +10 -0
- package/src/illumination/Message2Byte.ts +1 -1
- package/src/illumination/RunSectionRoute.ts +4 -0
- package/src/models/AccessKey.ts +24 -13
- package/src/models/Model.ts +3 -3
- package/src/user/UserModel.ts +9 -0
- package/src/user/UserStore.ts +19 -0
- package/src/workflow/services/MassSendApiService.ts +212 -51
- package/templates/bot/database/migrations/010_access_keys.sql +12 -0
- package/templates/bot/package.json +1 -1
package/package.json
CHANGED
package/src/core/App.ts
CHANGED
|
@@ -279,12 +279,7 @@ export class App {
|
|
|
279
279
|
? startPayload.split("key=")[1] || null
|
|
280
280
|
: null;
|
|
281
281
|
}
|
|
282
|
-
|
|
283
|
-
"exists storage",
|
|
284
|
-
this.config.userStorage.exists(tgUsername),
|
|
285
|
-
"remeber:",
|
|
286
|
-
this.rememberUser(tgUsername)
|
|
287
|
-
);
|
|
282
|
+
|
|
288
283
|
// Check access by username and register user if not exists
|
|
289
284
|
if (!this.config.userStorage.exists(tgUsername) && !this.rememberUser(tgUsername)) {
|
|
290
285
|
const isAuthByUsername = !this.config.accessPublic && !accessKey;
|
|
@@ -907,7 +902,7 @@ export class App {
|
|
|
907
902
|
}
|
|
908
903
|
sectionClass = this.sectionClasses.get(sectionId) as typeof Section;
|
|
909
904
|
}
|
|
910
|
-
this.debugLog("Using section class:", sectionClass.
|
|
905
|
+
this.debugLog("Using section class:", sectionClass.name);
|
|
911
906
|
|
|
912
907
|
let sectionInstance: Section | undefined;
|
|
913
908
|
|
|
@@ -921,6 +916,10 @@ export class App {
|
|
|
921
916
|
};
|
|
922
917
|
|
|
923
918
|
const createRunnedSection = (instance: Section, route: RunSectionRoute): RunnedSection => {
|
|
919
|
+
if (route.hasCallbackParams()) {
|
|
920
|
+
instance.setCallbackParams(route.getCallbackParams());
|
|
921
|
+
this.debugLog("Set callback params for section instance:", route.getCallbackParams());
|
|
922
|
+
}
|
|
924
923
|
return {
|
|
925
924
|
instance,
|
|
926
925
|
route,
|
|
@@ -38,6 +38,16 @@ export class InlineKeyboard {
|
|
|
38
38
|
return this;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
appendArray(rows: any[][]): InlineKeyboard {
|
|
42
|
+
rows.forEach(row => this.append(row));
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
prependArray(rows: any[][]): InlineKeyboard {
|
|
47
|
+
rows.forEach(row => this.prepend(row));
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
prepend(...row: any[]): InlineKeyboard {
|
|
42
52
|
if (!Array.isArray(row)) {
|
|
43
53
|
this.keyboard.unshift([row]);
|
|
@@ -202,7 +202,7 @@ export default class Message2byte {
|
|
|
202
202
|
if (message) {
|
|
203
203
|
if ("media_group_id" in message || "caption" in message) {
|
|
204
204
|
const editMessageCaption = this.editMessageCaption(this.messageValue, this.messageExtra);
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
if (editMessageCaption && "message_id" in editMessageCaption) {
|
|
207
207
|
this.messageId = editMessageCaption.message_id as number;
|
|
208
208
|
}
|
|
@@ -74,6 +74,10 @@ export class RunSectionRoute {
|
|
|
74
74
|
getTriggers(): RunSectionRouteTrigger[] {
|
|
75
75
|
return this.runParams.triggers;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
hasCallbackParams(): boolean {
|
|
79
|
+
return this.runParams.callbackParams && Array.from(this.runParams.callbackParams).length > 0;
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
getMethod(): string | null {
|
|
79
83
|
return this.runParams.method;
|
package/src/models/AccessKey.ts
CHANGED
|
@@ -3,9 +3,9 @@ import crypto from "crypto";
|
|
|
3
3
|
import { UserModel } from "../user/UserModel";
|
|
4
4
|
|
|
5
5
|
export interface AccessKeyData {
|
|
6
|
-
id
|
|
6
|
+
id: number;
|
|
7
7
|
user_id: number;
|
|
8
|
-
key
|
|
8
|
+
key: string;
|
|
9
9
|
used?: number;
|
|
10
10
|
used_user_id?: number | null;
|
|
11
11
|
created_at?: string;
|
|
@@ -41,31 +41,31 @@ export class AccessKey extends Model {
|
|
|
41
41
|
return key;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
static
|
|
44
|
+
static findByKey(key: string): AccessKeyData | null {
|
|
45
45
|
return this.queryOne(`SELECT * FROM ${this.tableName} WHERE key = ?`, [key]);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
static
|
|
48
|
+
static findById(id: number): AccessKeyData | null {
|
|
49
49
|
return this.queryOne(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
static markUsed(key: string, usedUserId?: number):
|
|
52
|
+
static markUsed(key: string, usedUserId?: number): void {
|
|
53
53
|
this.execute(
|
|
54
54
|
`UPDATE ${this.tableName} SET used = 1, used_user_id = ?, created_at = created_at WHERE key = ?`,
|
|
55
55
|
[usedUserId || null, key]
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
static
|
|
60
|
-
|
|
59
|
+
static revoke(key: string): void {
|
|
60
|
+
this.execute(`DELETE FROM ${this.tableName} WHERE key = ?`, [key]);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
static
|
|
63
|
+
static getAll(limit = 100): AccessKeyData[] {
|
|
64
64
|
return this.query(`SELECT * FROM ${this.tableName} ORDER BY created_at DESC LIMIT ?`, [limit]);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** Get all access keys for a given user */
|
|
68
|
-
static
|
|
68
|
+
static findByUser(userId: number, limit = 100): AccessKeyData[] {
|
|
69
69
|
return this.query(
|
|
70
70
|
`SELECT * FROM ${this.tableName} WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`,
|
|
71
71
|
[userId, limit]
|
|
@@ -73,8 +73,19 @@ export class AccessKey extends Model {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Return the owner `UserModel` for a given access entry or null */
|
|
76
|
-
static
|
|
77
|
-
const userData =
|
|
76
|
+
static getUserByEntry(entry: AccessKeyData): any | null {
|
|
77
|
+
const userData = 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
|
+
static getUsedUserByEntry(entry: AccessKeyData): any | null {
|
|
87
|
+
if (!entry.used_user_id) return null;
|
|
88
|
+
const userData = this.queryOne(`SELECT * FROM users WHERE id = ?`, [entry.used_user_id]);
|
|
78
89
|
if (!userData) return null;
|
|
79
90
|
try {
|
|
80
91
|
return UserModel.make(userData);
|
|
@@ -84,8 +95,8 @@ export class AccessKey extends Model {
|
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
/** Find owner by key string */
|
|
87
|
-
static
|
|
88
|
-
const entry =
|
|
98
|
+
static getUserByKey(key: string): any | null {
|
|
99
|
+
const entry = this.findByKey(key);
|
|
89
100
|
if (!entry) return null;
|
|
90
101
|
return this.getUserByEntry(entry as AccessKeyData);
|
|
91
102
|
}
|
package/src/models/Model.ts
CHANGED
|
@@ -29,13 +29,13 @@ export abstract class Model {
|
|
|
29
29
|
return this.db.transaction(callback)();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
public static
|
|
32
|
+
public static paginate(paginateParams: ModelPaginateParams): PaginateResult {
|
|
33
33
|
const { page, route, routeParams, limit, whereSql, whereParams } = paginateParams;
|
|
34
34
|
const offset = (page - 1) * limit;
|
|
35
35
|
const sql = `SELECT * FROM ${this.tableName} ${whereSql} LIMIT ${offset}, ${limit}`;
|
|
36
36
|
|
|
37
|
-
const result =
|
|
38
|
-
const queryTotal =
|
|
37
|
+
const result = this.query(sql, whereParams);
|
|
38
|
+
const queryTotal = this.queryOne(`SELECT COUNT(*) as count FROM ${this.tableName} ${whereSql}`, whereParams);
|
|
39
39
|
const total = queryTotal ? queryTotal.count : 0;
|
|
40
40
|
const totalPages = Math.ceil(total / limit);
|
|
41
41
|
const hasPreviousPage = page > 1;
|
package/src/user/UserModel.ts
CHANGED
|
@@ -232,6 +232,15 @@ export class UserModel extends Model {
|
|
|
232
232
|
return undefined;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
static delete(id: number): boolean {
|
|
236
|
+
if (this.db) {
|
|
237
|
+
const result = this.db.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
238
|
+
return result.changes > 0;
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error("Database connection is not set.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
get username(): string {
|
|
236
245
|
return this.attributes.tg_username;
|
|
237
246
|
}
|
package/src/user/UserStore.ts
CHANGED
|
@@ -116,4 +116,23 @@ export class UserStore {
|
|
|
116
116
|
});
|
|
117
117
|
}, 60 * 1000);
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
deleteByUsername(tgUsername: string): boolean {
|
|
121
|
+
const user = this.usersMap.get(tgUsername);
|
|
122
|
+
if (user) {
|
|
123
|
+
this.usersSession.delete(user);
|
|
124
|
+
return this.usersMap.delete(tgUsername);
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
deleteById(userId: number): boolean {
|
|
130
|
+
const userEntry = Array.from(this.usersMap).find(([_, user]) => user.id === userId);
|
|
131
|
+
if (userEntry) {
|
|
132
|
+
const [tgUsername, user] = userEntry;
|
|
133
|
+
this.usersSession.delete(user);
|
|
134
|
+
return this.usersMap.delete(tgUsername);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
119
138
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- UP
|
|
2
|
+
CREATE TABLE IF NOT EXISTS access_keys (
|
|
3
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
user_id INTEGER NOT NULL,
|
|
5
|
+
key TEXT NOT NULL UNIQUE,
|
|
6
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
used_user_id INTEGER DEFAULT NULL,
|
|
8
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- DOWN
|
|
12
|
+
DROP TABLE IF EXISTS access_keys;
|