@llblab/pi-telegram 0.2.10 → 0.3.0
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/README.md +30 -19
- package/docs/architecture.md +51 -28
- package/index.ts +388 -1881
- package/lib/api.ts +396 -60
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +648 -14
- package/lib/config.ts +157 -0
- package/lib/media.ts +147 -41
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +240 -14
- package/lib/preview.ts +420 -25
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +560 -366
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +127 -23
- package/lib/updates.ts +340 -109
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -34
- package/lib/model-switch.ts +0 -62
- package/lib/types.ts +0 -137
- package/tests/api.test.ts +0 -331
- package/tests/attachments.test.ts +0 -132
- package/tests/commands.test.ts +0 -85
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -166
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -202
- package/tests/preview.test.ts +0 -480
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -526
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -247
- package/tests/updates.test.ts +0 -416
package/lib/config.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bridge config and pairing helpers
|
|
3
|
+
* Owns persisted bot/session pairing state, local config storage, authorization policy, and first-user pairing side effects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const AGENT_DIR = join(homedir(), ".pi", "agent");
|
|
11
|
+
const CONFIG_PATH = join(AGENT_DIR, "telegram.json");
|
|
12
|
+
|
|
13
|
+
export interface TelegramConfig {
|
|
14
|
+
botToken?: string;
|
|
15
|
+
botUsername?: string;
|
|
16
|
+
botId?: number;
|
|
17
|
+
allowedUserId?: number;
|
|
18
|
+
lastUpdateId?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TelegramConfigStore {
|
|
22
|
+
get: () => TelegramConfig;
|
|
23
|
+
set: (config: TelegramConfig) => void;
|
|
24
|
+
update: (mutate: (config: TelegramConfig) => void) => void;
|
|
25
|
+
getBotToken: () => string | undefined;
|
|
26
|
+
hasBotToken: () => boolean;
|
|
27
|
+
getAllowedUserId: () => number | undefined;
|
|
28
|
+
setAllowedUserId: (userId: number) => void;
|
|
29
|
+
load: () => Promise<void>;
|
|
30
|
+
persist: (config?: TelegramConfig) => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TelegramConfigStoreOptions {
|
|
34
|
+
initialConfig?: TelegramConfig;
|
|
35
|
+
agentDir?: string;
|
|
36
|
+
configPath?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readTelegramConfig(
|
|
40
|
+
configPath: string,
|
|
41
|
+
): Promise<TelegramConfig> {
|
|
42
|
+
try {
|
|
43
|
+
const content = await readFile(configPath, "utf8");
|
|
44
|
+
return JSON.parse(content) as TelegramConfig;
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function writeTelegramConfig(
|
|
51
|
+
agentDir: string,
|
|
52
|
+
configPath: string,
|
|
53
|
+
config: TelegramConfig,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
await mkdir(agentDir, { recursive: true });
|
|
56
|
+
await writeFile(configPath, JSON.stringify(config, null, "\t") + "\n", {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
mode: 0o600,
|
|
59
|
+
});
|
|
60
|
+
await chmod(configPath, 0o600);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createTelegramConfigStore(
|
|
64
|
+
options: TelegramConfigStoreOptions = {},
|
|
65
|
+
): TelegramConfigStore {
|
|
66
|
+
let config: TelegramConfig = options.initialConfig ?? {};
|
|
67
|
+
const agentDir = options.agentDir ?? AGENT_DIR;
|
|
68
|
+
const configPath = options.configPath ?? CONFIG_PATH;
|
|
69
|
+
return {
|
|
70
|
+
get: () => config,
|
|
71
|
+
set: (nextConfig) => {
|
|
72
|
+
config = nextConfig;
|
|
73
|
+
},
|
|
74
|
+
update: (mutate) => {
|
|
75
|
+
mutate(config);
|
|
76
|
+
},
|
|
77
|
+
getBotToken: () => config.botToken,
|
|
78
|
+
hasBotToken: () => !!config.botToken,
|
|
79
|
+
getAllowedUserId: () => config.allowedUserId,
|
|
80
|
+
setAllowedUserId: (userId) => {
|
|
81
|
+
config.allowedUserId = userId;
|
|
82
|
+
},
|
|
83
|
+
load: async () => {
|
|
84
|
+
config = await readTelegramConfig(configPath);
|
|
85
|
+
},
|
|
86
|
+
persist: async (nextConfig = config) => {
|
|
87
|
+
await writeTelegramConfig(agentDir, configPath, nextConfig);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type TelegramAuthorizationState =
|
|
93
|
+
| { kind: "pair"; userId: number }
|
|
94
|
+
| { kind: "allow" }
|
|
95
|
+
| { kind: "deny" };
|
|
96
|
+
|
|
97
|
+
export interface TelegramUserPairingDeps<TContext> {
|
|
98
|
+
allowedUserId?: number;
|
|
99
|
+
ctx: TContext;
|
|
100
|
+
setAllowedUserId: (userId: number) => void;
|
|
101
|
+
persistConfig: () => Promise<void>;
|
|
102
|
+
updateStatus: (ctx: TContext) => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface TelegramUserPairingRuntimeDeps<TContext> {
|
|
106
|
+
getAllowedUserId: () => number | undefined;
|
|
107
|
+
setAllowedUserId: (userId: number) => void;
|
|
108
|
+
persistConfig: () => Promise<void>;
|
|
109
|
+
updateStatus: (ctx: TContext) => void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TelegramUserPairingRuntime<TContext> {
|
|
113
|
+
pairIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getTelegramAuthorizationState(
|
|
117
|
+
userId: number,
|
|
118
|
+
allowedUserId?: number,
|
|
119
|
+
): TelegramAuthorizationState {
|
|
120
|
+
if (allowedUserId === undefined) {
|
|
121
|
+
return { kind: "pair", userId };
|
|
122
|
+
}
|
|
123
|
+
if (userId === allowedUserId) {
|
|
124
|
+
return { kind: "allow" };
|
|
125
|
+
}
|
|
126
|
+
return { kind: "deny" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function pairTelegramUserIfNeeded<TContext>(
|
|
130
|
+
userId: number,
|
|
131
|
+
deps: TelegramUserPairingDeps<TContext>,
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
const authorization = getTelegramAuthorizationState(
|
|
134
|
+
userId,
|
|
135
|
+
deps.allowedUserId,
|
|
136
|
+
);
|
|
137
|
+
if (authorization.kind !== "pair") return false;
|
|
138
|
+
deps.setAllowedUserId(authorization.userId);
|
|
139
|
+
await deps.persistConfig();
|
|
140
|
+
deps.updateStatus(deps.ctx);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createTelegramUserPairingRuntime<TContext>(
|
|
145
|
+
deps: TelegramUserPairingRuntimeDeps<TContext>,
|
|
146
|
+
): TelegramUserPairingRuntime<TContext> {
|
|
147
|
+
return {
|
|
148
|
+
pairIfNeeded: (userId, ctx) =>
|
|
149
|
+
pairTelegramUserIfNeeded(userId, {
|
|
150
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
151
|
+
ctx,
|
|
152
|
+
setAllowedUserId: deps.setAllowedUserId,
|
|
153
|
+
persistConfig: deps.persistConfig,
|
|
154
|
+
updateStatus: deps.updateStatus,
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
}
|
package/lib/media.ts
CHANGED
|
@@ -3,59 +3,47 @@
|
|
|
3
3
|
* Normalizes inbound Telegram messages into reusable file, text, id, history, and media-group metadata
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
file_id: string;
|
|
8
|
-
file_size?: number;
|
|
9
|
-
}
|
|
6
|
+
const TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS = 1200;
|
|
10
7
|
|
|
11
|
-
export interface
|
|
8
|
+
export interface TelegramPhotoSize {
|
|
12
9
|
file_id: string;
|
|
13
|
-
|
|
14
|
-
mime_type?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TelegramVideoLike {
|
|
18
|
-
file_id: string;
|
|
19
|
-
file_name?: string;
|
|
20
|
-
mime_type?: string;
|
|
10
|
+
file_size?: number;
|
|
21
11
|
}
|
|
22
12
|
|
|
23
|
-
export interface
|
|
13
|
+
export interface TelegramDocument {
|
|
24
14
|
file_id: string;
|
|
25
15
|
file_name?: string;
|
|
26
16
|
mime_type?: string;
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
19
|
+
export type TelegramVideo = TelegramDocument;
|
|
20
|
+
export type TelegramAudio = TelegramDocument;
|
|
21
|
+
export type TelegramAnimation = TelegramDocument;
|
|
33
22
|
|
|
34
|
-
export interface
|
|
23
|
+
export interface TelegramVoice {
|
|
35
24
|
file_id: string;
|
|
36
|
-
file_name?: string;
|
|
37
25
|
mime_type?: string;
|
|
38
26
|
}
|
|
39
27
|
|
|
40
|
-
export interface
|
|
28
|
+
export interface TelegramSticker {
|
|
41
29
|
file_id: string;
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
export interface
|
|
32
|
+
export interface TelegramMediaMessage {
|
|
45
33
|
message_id: number;
|
|
46
34
|
text?: string;
|
|
47
35
|
caption?: string;
|
|
48
36
|
media_group_id?: string;
|
|
49
|
-
photo?:
|
|
50
|
-
document?:
|
|
51
|
-
video?:
|
|
52
|
-
audio?:
|
|
53
|
-
voice?:
|
|
54
|
-
animation?:
|
|
55
|
-
sticker?:
|
|
37
|
+
photo?: TelegramPhotoSize[];
|
|
38
|
+
document?: TelegramDocument;
|
|
39
|
+
video?: TelegramVideo;
|
|
40
|
+
audio?: TelegramAudio;
|
|
41
|
+
voice?: TelegramVoice;
|
|
42
|
+
animation?: TelegramAnimation;
|
|
43
|
+
sticker?: TelegramSticker;
|
|
56
44
|
}
|
|
57
45
|
|
|
58
|
-
export interface
|
|
46
|
+
export interface TelegramMediaGroupMessage {
|
|
59
47
|
message_id: number;
|
|
60
48
|
chat: { id: number };
|
|
61
49
|
media_group_id?: string;
|
|
@@ -66,6 +54,41 @@ export interface TelegramMediaGroupState<TMessage> {
|
|
|
66
54
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
67
55
|
}
|
|
68
56
|
|
|
57
|
+
export interface TelegramMediaGroupController<
|
|
58
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
59
|
+
> {
|
|
60
|
+
queueMessage: (options: {
|
|
61
|
+
message: TMessage;
|
|
62
|
+
dispatchMessages: (messages: TMessage[]) => void;
|
|
63
|
+
}) => boolean;
|
|
64
|
+
removeMessages: (messageIds: number[]) => number;
|
|
65
|
+
clear: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TelegramMediaGroupDispatchRuntimeDeps<
|
|
69
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
70
|
+
TContext,
|
|
71
|
+
> {
|
|
72
|
+
mediaGroups: TelegramMediaGroupController<TMessage>;
|
|
73
|
+
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TelegramMediaGroupDispatchRuntime<
|
|
77
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
78
|
+
TContext,
|
|
79
|
+
> {
|
|
80
|
+
handleMessage: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TelegramMediaGroupControllerOptions {
|
|
84
|
+
debounceMs?: number;
|
|
85
|
+
setTimer?: (
|
|
86
|
+
callback: () => void,
|
|
87
|
+
ms: number,
|
|
88
|
+
) => ReturnType<typeof setTimeout>;
|
|
89
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
export interface TelegramFileInfo {
|
|
70
93
|
file_id: string;
|
|
71
94
|
fileName: string;
|
|
@@ -73,8 +96,22 @@ export interface TelegramFileInfo {
|
|
|
73
96
|
isImage: boolean;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
export interface
|
|
99
|
+
export interface DownloadedTelegramFile {
|
|
77
100
|
path: string;
|
|
101
|
+
fileName?: string;
|
|
102
|
+
isImage?: boolean;
|
|
103
|
+
mimeType?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DownloadedTelegramMessageFile {
|
|
107
|
+
path: string;
|
|
108
|
+
fileName: string;
|
|
109
|
+
isImage: boolean;
|
|
110
|
+
mimeType?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface DownloadTelegramMessageFilesDeps {
|
|
114
|
+
downloadFile: (fileId: string, fileName: string) => Promise<string>;
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
export function guessExtensionFromMime(
|
|
@@ -106,43 +143,43 @@ export function guessMediaType(path: string): string | undefined {
|
|
|
106
143
|
return undefined;
|
|
107
144
|
}
|
|
108
145
|
|
|
109
|
-
|
|
146
|
+
function isImageMimeType(mimeType: string | undefined): boolean {
|
|
110
147
|
return mimeType?.toLowerCase().startsWith("image/") ?? false;
|
|
111
148
|
}
|
|
112
149
|
|
|
113
150
|
export function extractTelegramMessageText(
|
|
114
|
-
message:
|
|
151
|
+
message: TelegramMediaMessage,
|
|
115
152
|
): string {
|
|
116
153
|
return (message.text || message.caption || "").trim();
|
|
117
154
|
}
|
|
118
155
|
|
|
119
156
|
export function extractTelegramMessagesText(
|
|
120
|
-
messages:
|
|
157
|
+
messages: TelegramMediaMessage[],
|
|
121
158
|
): string {
|
|
122
159
|
return messages.map(extractTelegramMessageText).filter(Boolean).join("\n\n");
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
export function extractFirstTelegramMessageText(
|
|
126
|
-
messages:
|
|
163
|
+
messages: TelegramMediaMessage[],
|
|
127
164
|
): string {
|
|
128
165
|
return messages.map(extractTelegramMessageText).find(Boolean) ?? "";
|
|
129
166
|
}
|
|
130
167
|
|
|
131
168
|
export function collectTelegramMessageIds(
|
|
132
|
-
messages:
|
|
169
|
+
messages: TelegramMediaMessage[],
|
|
133
170
|
): number[] {
|
|
134
171
|
return [...new Set(messages.map((message) => message.message_id))];
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
export function getTelegramMediaGroupKey(
|
|
138
|
-
message:
|
|
175
|
+
message: TelegramMediaGroupMessage,
|
|
139
176
|
): string | undefined {
|
|
140
177
|
if (!message.media_group_id) return undefined;
|
|
141
178
|
return `${message.chat.id}:${message.media_group_id}`;
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
export function removePendingTelegramMediaGroupMessages<
|
|
145
|
-
TMessage extends
|
|
182
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
146
183
|
>(
|
|
147
184
|
groups: Map<string, TelegramMediaGroupState<TMessage>>,
|
|
148
185
|
messageIds: number[],
|
|
@@ -167,7 +204,7 @@ export function removePendingTelegramMediaGroupMessages<
|
|
|
167
204
|
}
|
|
168
205
|
|
|
169
206
|
export function queueTelegramMediaGroupMessage<
|
|
170
|
-
TMessage extends
|
|
207
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
171
208
|
>(options: {
|
|
172
209
|
message: TMessage;
|
|
173
210
|
groups: Map<string, TelegramMediaGroupState<TMessage>>;
|
|
@@ -191,9 +228,62 @@ export function queueTelegramMediaGroupMessage<
|
|
|
191
228
|
return true;
|
|
192
229
|
}
|
|
193
230
|
|
|
231
|
+
export function createTelegramMediaGroupController<
|
|
232
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
233
|
+
>(
|
|
234
|
+
options: TelegramMediaGroupControllerOptions = {},
|
|
235
|
+
): TelegramMediaGroupController<TMessage> {
|
|
236
|
+
const groups = new Map<string, TelegramMediaGroupState<TMessage>>();
|
|
237
|
+
const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
|
|
238
|
+
const setTimer =
|
|
239
|
+
options.setTimer ??
|
|
240
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
241
|
+
setTimeout(callback, ms));
|
|
242
|
+
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
243
|
+
return {
|
|
244
|
+
queueMessage: ({ message, dispatchMessages }) =>
|
|
245
|
+
queueTelegramMediaGroupMessage({
|
|
246
|
+
message,
|
|
247
|
+
groups,
|
|
248
|
+
debounceMs,
|
|
249
|
+
setTimer,
|
|
250
|
+
clearTimer,
|
|
251
|
+
dispatchMessages,
|
|
252
|
+
}),
|
|
253
|
+
removeMessages: (messageIds) =>
|
|
254
|
+
removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
|
|
255
|
+
clear: () => {
|
|
256
|
+
for (const state of groups.values()) {
|
|
257
|
+
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
258
|
+
}
|
|
259
|
+
groups.clear();
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createTelegramMediaGroupDispatchRuntime<
|
|
265
|
+
TMessage extends TelegramMediaGroupMessage,
|
|
266
|
+
TContext,
|
|
267
|
+
>(
|
|
268
|
+
deps: TelegramMediaGroupDispatchRuntimeDeps<TMessage, TContext>,
|
|
269
|
+
): TelegramMediaGroupDispatchRuntime<TMessage, TContext> {
|
|
270
|
+
return {
|
|
271
|
+
handleMessage: async (message, ctx) => {
|
|
272
|
+
const queuedMediaGroup = deps.mediaGroups.queueMessage({
|
|
273
|
+
message,
|
|
274
|
+
dispatchMessages: (messages) => {
|
|
275
|
+
void deps.dispatchMessages(messages, ctx);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (queuedMediaGroup) return;
|
|
279
|
+
await deps.dispatchMessages([message], ctx);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
194
284
|
export function formatTelegramHistoryText(
|
|
195
285
|
rawText: string,
|
|
196
|
-
files:
|
|
286
|
+
files: DownloadedTelegramFile[],
|
|
197
287
|
): string {
|
|
198
288
|
let summary = rawText.length > 0 ? rawText : "(no text)";
|
|
199
289
|
if (files.length > 0) {
|
|
@@ -205,8 +295,24 @@ export function formatTelegramHistoryText(
|
|
|
205
295
|
return summary;
|
|
206
296
|
}
|
|
207
297
|
|
|
298
|
+
export async function downloadTelegramMessageFiles(
|
|
299
|
+
messages: TelegramMediaMessage[],
|
|
300
|
+
deps: DownloadTelegramMessageFilesDeps,
|
|
301
|
+
): Promise<DownloadedTelegramMessageFile[]> {
|
|
302
|
+
const downloaded: DownloadedTelegramMessageFile[] = [];
|
|
303
|
+
for (const file of collectTelegramFileInfos(messages)) {
|
|
304
|
+
downloaded.push({
|
|
305
|
+
path: await deps.downloadFile(file.file_id, file.fileName),
|
|
306
|
+
fileName: file.fileName,
|
|
307
|
+
isImage: file.isImage,
|
|
308
|
+
mimeType: file.mimeType,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return downloaded;
|
|
312
|
+
}
|
|
313
|
+
|
|
208
314
|
export function collectTelegramFileInfos(
|
|
209
|
-
messages:
|
|
315
|
+
messages: TelegramMediaMessage[],
|
|
210
316
|
): TelegramFileInfo[] {
|
|
211
317
|
const files: TelegramFileInfo[] = [];
|
|
212
318
|
for (const message of messages) {
|