@melandlabs/integrations-telegram 0.1.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/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/adapter.d.ts +125 -0
- package/dist/adapter.js +1670 -0
- package/dist/adapter.js.map +1 -0
- package/dist/conversation-store.d.ts +48 -0
- package/dist/conversation-store.js +147 -0
- package/dist/conversation-store.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1810 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.js +565 -0
- package/dist/markdown.js.map +1 -0
- package/dist/state.d.ts +21 -0
- package/dist/state.js +38 -0
- package/dist/state.js.map +1 -0
- package/dist/tdata-converter.d.ts +21 -0
- package/dist/tdata-converter.js +640 -0
- package/dist/tdata-converter.js.map +1 -0
- package/dist/tdata-decrypter-index.d.ts +314 -0
- package/dist/tdata-decrypter-index.js +671 -0
- package/dist/tdata-decrypter-index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1810 @@
|
|
|
1
|
+
import { MessagePlatformAdapter } from '@melandlabs/integrations-channels';
|
|
2
|
+
import { delay, isEmptyMessage, timeBeforeHours } from '@melandlabs/integrations-channels/sources/types';
|
|
3
|
+
import { isTelegramContactMeta } from '@melandlabs/integrations/contacts';
|
|
4
|
+
import bigInt from 'big-integer';
|
|
5
|
+
import { TelegramClient } from 'telegram';
|
|
6
|
+
import { CustomFile } from 'telegram/client/uploads';
|
|
7
|
+
import { StringSession } from 'telegram/sessions';
|
|
8
|
+
import { Api } from 'telegram/tl';
|
|
9
|
+
import MarkdownIt from 'markdown-it';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { loadChannelDay, saveChannelMessage, clearChannelConversationFromAllDays, clearChannelForUserPrefix } from '@melandlabs/ai/store';
|
|
13
|
+
|
|
14
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
15
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
16
|
+
}) : x)(function(x) {
|
|
17
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
18
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
19
|
+
});
|
|
20
|
+
function createMarkdownIt() {
|
|
21
|
+
const md = new MarkdownIt({
|
|
22
|
+
html: false,
|
|
23
|
+
linkify: true,
|
|
24
|
+
breaks: false,
|
|
25
|
+
typographer: false
|
|
26
|
+
});
|
|
27
|
+
md.enable("strikethrough");
|
|
28
|
+
md.enable("table");
|
|
29
|
+
return md;
|
|
30
|
+
}
|
|
31
|
+
function initCellState() {
|
|
32
|
+
return {
|
|
33
|
+
text: "",
|
|
34
|
+
styles: [],
|
|
35
|
+
openStyles: [],
|
|
36
|
+
links: [],
|
|
37
|
+
linkStack: []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function finishCell(cell) {
|
|
41
|
+
closeRemainingStyles(cell);
|
|
42
|
+
return { text: cell.text, styles: cell.styles, links: cell.links };
|
|
43
|
+
}
|
|
44
|
+
function resolveTarget(state) {
|
|
45
|
+
return state.table?.currentCell ?? state;
|
|
46
|
+
}
|
|
47
|
+
function appendText(state, value) {
|
|
48
|
+
if (!value) return;
|
|
49
|
+
resolveTarget(state).text += value;
|
|
50
|
+
}
|
|
51
|
+
function openStyle(state, style) {
|
|
52
|
+
const target = resolveTarget(state);
|
|
53
|
+
target.openStyles.push({ style, start: target.text.length });
|
|
54
|
+
}
|
|
55
|
+
function closeStyle(state, style) {
|
|
56
|
+
const target = resolveTarget(state);
|
|
57
|
+
for (let i = target.openStyles.length - 1; i >= 0; i--) {
|
|
58
|
+
if (target.openStyles[i]?.style === style) {
|
|
59
|
+
const start = target.openStyles[i].start;
|
|
60
|
+
target.openStyles.splice(i, 1);
|
|
61
|
+
const end = target.text.length;
|
|
62
|
+
if (end > start) {
|
|
63
|
+
target.styles.push({ start, end, style });
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function closeRemainingStyles(target) {
|
|
70
|
+
for (let i = target.openStyles.length - 1; i >= 0; i--) {
|
|
71
|
+
const open = target.openStyles[i];
|
|
72
|
+
const end = target.text.length;
|
|
73
|
+
if (end > open.start) {
|
|
74
|
+
target.styles.push({ start: open.start, end, style: open.style });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
target.openStyles = [];
|
|
78
|
+
}
|
|
79
|
+
function getAttr(token, name) {
|
|
80
|
+
if (token.attrGet) return token.attrGet(name);
|
|
81
|
+
if (token.attrs) {
|
|
82
|
+
for (const [key, value] of token.attrs) {
|
|
83
|
+
if (key === name) return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function renderTableAsBullets(state) {
|
|
89
|
+
if (!state.table) return;
|
|
90
|
+
const headers = state.table.headers;
|
|
91
|
+
const rows = state.table.rows;
|
|
92
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
93
|
+
const row = rows[ri];
|
|
94
|
+
if (row.length === 0) continue;
|
|
95
|
+
const labelStart = state.text.length;
|
|
96
|
+
state.text += row[0].text;
|
|
97
|
+
if (state.text.length > labelStart) {
|
|
98
|
+
state.styles.push({
|
|
99
|
+
start: labelStart,
|
|
100
|
+
end: state.text.length,
|
|
101
|
+
style: "bold"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
state.text += "\n";
|
|
105
|
+
for (let ci = 1; ci < row.length; ci++) {
|
|
106
|
+
const headerText = headers[ci]?.text ?? `Column ${ci}`;
|
|
107
|
+
state.text += "\u2022 ";
|
|
108
|
+
state.text += `${headerText}: `;
|
|
109
|
+
state.text += `${row[ci].text}
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function renderTableAsCode(state) {
|
|
115
|
+
if (!state.table) return;
|
|
116
|
+
const headers = state.table.headers;
|
|
117
|
+
const rows = state.table.rows;
|
|
118
|
+
const colCount = Math.max(headers.length, ...rows.map((r) => r.length));
|
|
119
|
+
if (colCount === 0) return;
|
|
120
|
+
const widths = Array.from({ length: colCount }, () => 0);
|
|
121
|
+
const updateWidths = (cells) => {
|
|
122
|
+
for (let i = 0; i < colCount; i++) {
|
|
123
|
+
widths[i] = Math.max(widths[i], cells[i]?.text.length ?? 0);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
updateWidths(headers);
|
|
127
|
+
for (const row of rows) updateWidths(row);
|
|
128
|
+
const codeStart = state.text.length;
|
|
129
|
+
const appendRow = (cells) => {
|
|
130
|
+
state.text += "|";
|
|
131
|
+
for (let i = 0; i < colCount; i++) {
|
|
132
|
+
state.text += " ";
|
|
133
|
+
const cell = cells[i];
|
|
134
|
+
state.text += cell ? cell.text : "";
|
|
135
|
+
const pad = widths[i] - (cell?.text.length ?? 0);
|
|
136
|
+
state.text += `${" ".repeat(Math.max(0, pad))} |`;
|
|
137
|
+
}
|
|
138
|
+
state.text += "\n";
|
|
139
|
+
};
|
|
140
|
+
const appendDivider = () => {
|
|
141
|
+
state.text += "|";
|
|
142
|
+
for (let i = 0; i < colCount; i++) {
|
|
143
|
+
state.text += ` ${"-".repeat(Math.max(3, widths[i]))} |`;
|
|
144
|
+
}
|
|
145
|
+
state.text += "\n";
|
|
146
|
+
};
|
|
147
|
+
appendRow(headers);
|
|
148
|
+
appendDivider();
|
|
149
|
+
for (const row of rows) appendRow(row);
|
|
150
|
+
const codeEnd = state.text.length;
|
|
151
|
+
if (codeEnd > codeStart) {
|
|
152
|
+
state.styles.push({ start: codeStart, end: codeEnd, style: "code_block" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function processTokens(tokens, state) {
|
|
156
|
+
for (const token of tokens) {
|
|
157
|
+
switch (token.type) {
|
|
158
|
+
case "inline":
|
|
159
|
+
if (token.children) processTokens(token.children, state);
|
|
160
|
+
break;
|
|
161
|
+
case "text":
|
|
162
|
+
appendText(state, token.content ?? "");
|
|
163
|
+
break;
|
|
164
|
+
case "em_open":
|
|
165
|
+
openStyle(state, "italic");
|
|
166
|
+
break;
|
|
167
|
+
case "em_close":
|
|
168
|
+
closeStyle(state, "italic");
|
|
169
|
+
break;
|
|
170
|
+
case "strong_open":
|
|
171
|
+
openStyle(state, "bold");
|
|
172
|
+
break;
|
|
173
|
+
case "strong_close":
|
|
174
|
+
closeStyle(state, "bold");
|
|
175
|
+
break;
|
|
176
|
+
case "s_open":
|
|
177
|
+
openStyle(state, "strikethrough");
|
|
178
|
+
break;
|
|
179
|
+
case "s_close":
|
|
180
|
+
closeStyle(state, "strikethrough");
|
|
181
|
+
break;
|
|
182
|
+
case "spoiler_open":
|
|
183
|
+
if (state.spoilerEnabled) openStyle(state, "spoiler");
|
|
184
|
+
break;
|
|
185
|
+
case "spoiler_close":
|
|
186
|
+
if (state.spoilerEnabled) closeStyle(state, "spoiler");
|
|
187
|
+
break;
|
|
188
|
+
case "code_inline": {
|
|
189
|
+
const content = token.content ?? "";
|
|
190
|
+
const target = resolveTarget(state);
|
|
191
|
+
const start = target.text.length;
|
|
192
|
+
target.text += content;
|
|
193
|
+
target.styles.push({
|
|
194
|
+
start,
|
|
195
|
+
end: start + content.length,
|
|
196
|
+
style: "code"
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "link_open": {
|
|
201
|
+
const href = getAttr(token, "href") ?? "";
|
|
202
|
+
const target = resolveTarget(state);
|
|
203
|
+
target.linkStack.push({ href, labelStart: target.text.length });
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "link_close": {
|
|
207
|
+
const target = resolveTarget(state);
|
|
208
|
+
const link = target.linkStack.pop();
|
|
209
|
+
if (link?.href) {
|
|
210
|
+
const href = link.href.trim();
|
|
211
|
+
if (href) {
|
|
212
|
+
target.links.push({
|
|
213
|
+
start: link.labelStart,
|
|
214
|
+
end: target.text.length,
|
|
215
|
+
href
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "image":
|
|
222
|
+
appendText(state, token.content ?? "");
|
|
223
|
+
break;
|
|
224
|
+
case "softbreak":
|
|
225
|
+
case "hardbreak":
|
|
226
|
+
appendText(state, "\n");
|
|
227
|
+
break;
|
|
228
|
+
case "paragraph_close": {
|
|
229
|
+
if (state.listStack.length === 0 && !state.table) {
|
|
230
|
+
state.text += "\n\n";
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "heading_open":
|
|
235
|
+
if (state.headingStyle === "bold") openStyle(state, "bold");
|
|
236
|
+
break;
|
|
237
|
+
case "heading_close":
|
|
238
|
+
if (state.headingStyle === "bold") closeStyle(state, "bold");
|
|
239
|
+
if (state.listStack.length === 0 && !state.table) state.text += "\n\n";
|
|
240
|
+
break;
|
|
241
|
+
case "blockquote_open":
|
|
242
|
+
if (state.blockquotePrefix) state.text += state.blockquotePrefix;
|
|
243
|
+
openStyle(state, "blockquote");
|
|
244
|
+
break;
|
|
245
|
+
case "blockquote_close":
|
|
246
|
+
closeStyle(state, "blockquote");
|
|
247
|
+
break;
|
|
248
|
+
case "bullet_list_open":
|
|
249
|
+
if (state.listStack.length > 0) state.text += "\n";
|
|
250
|
+
state.listStack.push({ type: "bullet", index: 0 });
|
|
251
|
+
break;
|
|
252
|
+
case "bullet_list_close":
|
|
253
|
+
state.listStack.pop();
|
|
254
|
+
if (state.listStack.length === 0) state.text += "\n";
|
|
255
|
+
break;
|
|
256
|
+
case "ordered_list_open": {
|
|
257
|
+
if (state.listStack.length > 0) state.text += "\n";
|
|
258
|
+
const start = Number(getAttr(token, "start") ?? "1");
|
|
259
|
+
state.listStack.push({ type: "ordered", index: start - 1 });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "ordered_list_close":
|
|
263
|
+
state.listStack.pop();
|
|
264
|
+
if (state.listStack.length === 0) state.text += "\n";
|
|
265
|
+
break;
|
|
266
|
+
case "list_item_open": {
|
|
267
|
+
const top = state.listStack[state.listStack.length - 1];
|
|
268
|
+
if (top) {
|
|
269
|
+
top.index++;
|
|
270
|
+
const indent = " ".repeat(Math.max(0, state.listStack.length - 1));
|
|
271
|
+
const prefix = top.type === "ordered" ? `${top.index}. ` : "\u2022 ";
|
|
272
|
+
state.text += `${indent}${prefix}`;
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "list_item_close":
|
|
277
|
+
if (!state.text.endsWith("\n")) state.text += "\n";
|
|
278
|
+
break;
|
|
279
|
+
case "code_block":
|
|
280
|
+
case "fence": {
|
|
281
|
+
let code = token.content ?? "";
|
|
282
|
+
if (!code.endsWith("\n")) code += "\n";
|
|
283
|
+
const target = resolveTarget(state);
|
|
284
|
+
const start = target.text.length;
|
|
285
|
+
target.text += code;
|
|
286
|
+
target.styles.push({
|
|
287
|
+
start,
|
|
288
|
+
end: start + code.length,
|
|
289
|
+
style: "code_block"
|
|
290
|
+
});
|
|
291
|
+
if (state.listStack.length === 0) target.text += "\n";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "html_block":
|
|
295
|
+
case "html_inline":
|
|
296
|
+
appendText(state, token.content ?? "");
|
|
297
|
+
break;
|
|
298
|
+
case "table_open":
|
|
299
|
+
if (state.tableMode !== "off") {
|
|
300
|
+
state.table = {
|
|
301
|
+
headers: [],
|
|
302
|
+
rows: [],
|
|
303
|
+
currentRow: [],
|
|
304
|
+
currentCell: null,
|
|
305
|
+
inHeader: false
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
case "table_close":
|
|
310
|
+
if (state.table) {
|
|
311
|
+
if (state.tableMode === "bullets") renderTableAsBullets(state);
|
|
312
|
+
else if (state.tableMode === "code") renderTableAsCode(state);
|
|
313
|
+
}
|
|
314
|
+
state.table = null;
|
|
315
|
+
break;
|
|
316
|
+
case "thead_open":
|
|
317
|
+
if (state.table) state.table.inHeader = true;
|
|
318
|
+
break;
|
|
319
|
+
case "thead_close":
|
|
320
|
+
if (state.table) state.table.inHeader = false;
|
|
321
|
+
break;
|
|
322
|
+
case "tbody_open":
|
|
323
|
+
case "tbody_close":
|
|
324
|
+
break;
|
|
325
|
+
case "tr_open":
|
|
326
|
+
if (state.table) state.table.currentRow = [];
|
|
327
|
+
break;
|
|
328
|
+
case "tr_close":
|
|
329
|
+
if (state.table) {
|
|
330
|
+
if (state.table.inHeader) {
|
|
331
|
+
state.table.headers = state.table.currentRow;
|
|
332
|
+
} else {
|
|
333
|
+
state.table.rows.push(state.table.currentRow);
|
|
334
|
+
}
|
|
335
|
+
state.table.currentRow = [];
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case "th_open":
|
|
339
|
+
case "td_open":
|
|
340
|
+
if (state.table) state.table.currentCell = initCellState();
|
|
341
|
+
break;
|
|
342
|
+
case "th_close":
|
|
343
|
+
case "td_close":
|
|
344
|
+
if (state.table?.currentCell) {
|
|
345
|
+
state.table.currentRow.push(finishCell(state.table.currentCell));
|
|
346
|
+
state.table.currentCell = null;
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
case "hr":
|
|
350
|
+
state.text += "\u2500\u2500\u2500\n\n";
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
if (token.children) processTokens(token.children, state);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function markdownToIR(markdown, options = {}) {
|
|
359
|
+
const md = createMarkdownIt();
|
|
360
|
+
const env = { listStack: [] };
|
|
361
|
+
const tokens = md.parse(markdown ?? "", env);
|
|
362
|
+
const state = {
|
|
363
|
+
text: "",
|
|
364
|
+
styles: [],
|
|
365
|
+
openStyles: [],
|
|
366
|
+
links: [],
|
|
367
|
+
linkStack: [],
|
|
368
|
+
listStack: [],
|
|
369
|
+
headingStyle: options.headingStyle ?? "none",
|
|
370
|
+
blockquotePrefix: options.blockquotePrefix ?? "",
|
|
371
|
+
table: null,
|
|
372
|
+
tableMode: options.tableMode ?? "code",
|
|
373
|
+
spoilerEnabled: options.enableSpoilers ?? true
|
|
374
|
+
};
|
|
375
|
+
processTokens(tokens, state);
|
|
376
|
+
closeRemainingStyles(resolveTarget(state));
|
|
377
|
+
const trimmedLen = state.text.trimEnd().length;
|
|
378
|
+
let codeBlockEnd = 0;
|
|
379
|
+
for (const span of state.styles) {
|
|
380
|
+
if (span.style !== "code_block") continue;
|
|
381
|
+
if (span.end > codeBlockEnd) codeBlockEnd = span.end;
|
|
382
|
+
}
|
|
383
|
+
const finalLen = Math.max(trimmedLen, codeBlockEnd);
|
|
384
|
+
return {
|
|
385
|
+
text: finalLen === state.text.length ? state.text : state.text.slice(0, finalLen),
|
|
386
|
+
styles: mergeStyleSpans(clampStyleSpans(state.styles, finalLen)),
|
|
387
|
+
links: clampLinkSpans(state.links, finalLen)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function clampStyleSpans(spans, maxLen) {
|
|
391
|
+
return spans.map((s) => ({
|
|
392
|
+
...s,
|
|
393
|
+
start: Math.max(0, Math.min(s.start, maxLen)),
|
|
394
|
+
end: Math.max(0, Math.min(s.end, maxLen))
|
|
395
|
+
})).filter((s) => s.end > s.start);
|
|
396
|
+
}
|
|
397
|
+
function clampLinkSpans(spans, maxLen) {
|
|
398
|
+
return spans.map((s) => ({
|
|
399
|
+
...s,
|
|
400
|
+
start: Math.max(0, Math.min(s.start, maxLen)),
|
|
401
|
+
end: Math.max(0, Math.min(s.end, maxLen))
|
|
402
|
+
})).filter((s) => s.end > s.start);
|
|
403
|
+
}
|
|
404
|
+
function mergeStyleSpans(spans) {
|
|
405
|
+
if (spans.length === 0) return [];
|
|
406
|
+
const sorted = [...spans].sort((a, b) => {
|
|
407
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
408
|
+
if (a.end !== b.end) return b.end - a.end;
|
|
409
|
+
return a.style.localeCompare(b.style);
|
|
410
|
+
});
|
|
411
|
+
const merged = [];
|
|
412
|
+
for (const s of sorted) {
|
|
413
|
+
const prev = merged[merged.length - 1];
|
|
414
|
+
if (prev && prev.style === s.style && (s.start < prev.end || s.start === prev.end && s.style !== "blockquote")) {
|
|
415
|
+
prev.end = Math.max(prev.end, s.end);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
merged.push({ ...s });
|
|
419
|
+
}
|
|
420
|
+
return merged;
|
|
421
|
+
}
|
|
422
|
+
var STYLE_RANK = /* @__PURE__ */ new Map([
|
|
423
|
+
["blockquote", 0],
|
|
424
|
+
["code_block", 1],
|
|
425
|
+
["code", 2],
|
|
426
|
+
["bold", 3],
|
|
427
|
+
["italic", 4],
|
|
428
|
+
["strikethrough", 5],
|
|
429
|
+
["spoiler", 6]
|
|
430
|
+
]);
|
|
431
|
+
function sortStyleSpans(spans) {
|
|
432
|
+
return [...spans].sort((a, b) => {
|
|
433
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
434
|
+
if (a.end !== b.end) return b.end - a.end;
|
|
435
|
+
return (STYLE_RANK.get(a.style) ?? 0) - (STYLE_RANK.get(b.style) ?? 0);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function renderMarkdownWithMarkers(ir, options) {
|
|
439
|
+
const { escapeText: escFn, styleMarkers: markers, buildLink } = options;
|
|
440
|
+
const content = ir.text;
|
|
441
|
+
if (!content) return "";
|
|
442
|
+
const styled = sortStyleSpans(ir.styles.filter((s) => Boolean(markers[s.style])));
|
|
443
|
+
const boundaries = /* @__PURE__ */ new Set();
|
|
444
|
+
boundaries.add(0);
|
|
445
|
+
boundaries.add(content.length);
|
|
446
|
+
const startsAt = /* @__PURE__ */ new Map();
|
|
447
|
+
for (const span of styled) {
|
|
448
|
+
if (span.start === span.end) continue;
|
|
449
|
+
boundaries.add(span.start);
|
|
450
|
+
boundaries.add(span.end);
|
|
451
|
+
const bucket = startsAt.get(span.start);
|
|
452
|
+
if (bucket) bucket.push(span);
|
|
453
|
+
else startsAt.set(span.start, [span]);
|
|
454
|
+
}
|
|
455
|
+
for (const spans of startsAt.values()) {
|
|
456
|
+
spans.sort((a, b) => {
|
|
457
|
+
if (a.end !== b.end) return b.end - a.end;
|
|
458
|
+
return (STYLE_RANK.get(a.style) ?? 0) - (STYLE_RANK.get(b.style) ?? 0);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const linkStarts = /* @__PURE__ */ new Map();
|
|
462
|
+
if (buildLink) {
|
|
463
|
+
for (const link of ir.links) {
|
|
464
|
+
if (link.start === link.end) continue;
|
|
465
|
+
const rendered = buildLink(link, content);
|
|
466
|
+
if (!rendered) continue;
|
|
467
|
+
boundaries.add(rendered.start);
|
|
468
|
+
boundaries.add(rendered.end);
|
|
469
|
+
const bucket = linkStarts.get(rendered.start);
|
|
470
|
+
if (bucket) bucket.push(rendered);
|
|
471
|
+
else linkStarts.set(rendered.start, [rendered]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const points = [...boundaries].sort((a, b) => a - b);
|
|
475
|
+
const stack = [];
|
|
476
|
+
let out = "";
|
|
477
|
+
for (let i = 0; i < points.length; i++) {
|
|
478
|
+
const pos = points[i];
|
|
479
|
+
while (stack.length && stack[stack.length - 1]?.end === pos) {
|
|
480
|
+
const item = stack.pop();
|
|
481
|
+
if (item) out += item.close;
|
|
482
|
+
}
|
|
483
|
+
const openingItems = [];
|
|
484
|
+
const openingLinks = linkStarts.get(pos);
|
|
485
|
+
if (openingLinks) {
|
|
486
|
+
for (let idx = 0; idx < openingLinks.length; idx++) {
|
|
487
|
+
const l = openingLinks[idx];
|
|
488
|
+
openingItems.push({
|
|
489
|
+
end: l.end,
|
|
490
|
+
open: l.open,
|
|
491
|
+
close: l.close,
|
|
492
|
+
kind: "link",
|
|
493
|
+
index: idx
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const openingStyles = startsAt.get(pos);
|
|
498
|
+
if (openingStyles) {
|
|
499
|
+
for (let idx = 0; idx < openingStyles.length; idx++) {
|
|
500
|
+
const span = openingStyles[idx];
|
|
501
|
+
const marker = markers[span.style];
|
|
502
|
+
if (!marker) continue;
|
|
503
|
+
openingItems.push({
|
|
504
|
+
end: span.end,
|
|
505
|
+
open: marker.open,
|
|
506
|
+
close: marker.close,
|
|
507
|
+
kind: "style",
|
|
508
|
+
style: span.style,
|
|
509
|
+
index: idx
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (openingItems.length > 0) {
|
|
514
|
+
openingItems.sort((a, b) => {
|
|
515
|
+
if (a.end !== b.end) return b.end - a.end;
|
|
516
|
+
if (a.kind !== b.kind) return a.kind === "link" ? -1 : 1;
|
|
517
|
+
if (a.kind === "style" && b.kind === "style") {
|
|
518
|
+
return (STYLE_RANK.get(a.style ?? "bold") ?? 0) - (STYLE_RANK.get(b.style ?? "bold") ?? 0);
|
|
519
|
+
}
|
|
520
|
+
return a.index - b.index;
|
|
521
|
+
});
|
|
522
|
+
for (const item of openingItems) {
|
|
523
|
+
out += item.open;
|
|
524
|
+
stack.push({ close: item.close, end: item.end });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const next = points[i + 1];
|
|
528
|
+
if (next === void 0) break;
|
|
529
|
+
if (next > pos) out += escFn(content.slice(pos, next));
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
var TELEGRAM_STYLE_MARKERS = {
|
|
534
|
+
bold: { open: "<b>", close: "</b>" },
|
|
535
|
+
italic: { open: "<i>", close: "</i>" },
|
|
536
|
+
strikethrough: { open: "<s>", close: "</s>" },
|
|
537
|
+
code: { open: "<code>", close: "</code>" },
|
|
538
|
+
code_block: { open: "<pre><code>", close: "</code></pre>" },
|
|
539
|
+
spoiler: { open: "<tg-spoiler>", close: "</tg-spoiler>" },
|
|
540
|
+
blockquote: { open: "<blockquote>", close: "</blockquote>" }
|
|
541
|
+
};
|
|
542
|
+
function escapeHtml(text) {
|
|
543
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
544
|
+
}
|
|
545
|
+
function escapeHtmlAttr(text) {
|
|
546
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
547
|
+
}
|
|
548
|
+
function buildTelegramLink(link, text) {
|
|
549
|
+
const href = link.href.trim();
|
|
550
|
+
if (!href || link.start === link.end) return null;
|
|
551
|
+
return {
|
|
552
|
+
start: link.start,
|
|
553
|
+
end: link.end,
|
|
554
|
+
open: `<a href="${escapeHtmlAttr(href)}">`,
|
|
555
|
+
close: "</a>"
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function markdownToTelegramHtml(markdown) {
|
|
559
|
+
if (!markdown?.trim()) return "";
|
|
560
|
+
try {
|
|
561
|
+
const ir = markdownToIR(markdown, {
|
|
562
|
+
linkify: true,
|
|
563
|
+
enableSpoilers: true,
|
|
564
|
+
headingStyle: "none",
|
|
565
|
+
blockquotePrefix: "",
|
|
566
|
+
tableMode: "code"
|
|
567
|
+
});
|
|
568
|
+
return renderMarkdownWithMarkers(ir, {
|
|
569
|
+
styleMarkers: TELEGRAM_STYLE_MARKERS,
|
|
570
|
+
escapeText: escapeHtml,
|
|
571
|
+
buildLink: buildTelegramLink
|
|
572
|
+
});
|
|
573
|
+
} catch (err) {
|
|
574
|
+
console.error("[Telegram Markdown] Conversion failed, falling back to plain text:", err);
|
|
575
|
+
return escapeHtml(markdown);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
var DEBUG = process.env.DEBUG_TELEGRAM === "true";
|
|
579
|
+
var maxDialogCount = 50;
|
|
580
|
+
var maxMessageCount = 200;
|
|
581
|
+
var DEFAULT_MAX_MESSAGE_CHUNK_COUNT = 40;
|
|
582
|
+
var TELEGRAM_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
583
|
+
var dialogCacheFetchLimit = 200;
|
|
584
|
+
var CONNECT_TIMEOUT_MS = 6e4;
|
|
585
|
+
var SEND_MESSAGE_TIMEOUT_MS = 3e4;
|
|
586
|
+
function withTimeout(promise, ms, operationName) {
|
|
587
|
+
return new Promise((resolve, reject) => {
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
reject(new Error(`${operationName} timed out after ${ms}ms`));
|
|
590
|
+
}, ms);
|
|
591
|
+
promise.then((value) => {
|
|
592
|
+
clearTimeout(timer);
|
|
593
|
+
resolve(value);
|
|
594
|
+
}).catch((err) => {
|
|
595
|
+
clearTimeout(timer);
|
|
596
|
+
reject(err);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
var TelegramAdapter = class extends MessagePlatformAdapter {
|
|
601
|
+
client;
|
|
602
|
+
messages;
|
|
603
|
+
botId;
|
|
604
|
+
appId;
|
|
605
|
+
appHash;
|
|
606
|
+
session;
|
|
607
|
+
name = "Telegram";
|
|
608
|
+
ownerUserId;
|
|
609
|
+
ownerUserType;
|
|
610
|
+
fileIngester;
|
|
611
|
+
clientRegistry;
|
|
612
|
+
// Caches with TTL (1 hour default)
|
|
613
|
+
senderCache = {};
|
|
614
|
+
chatCache = {};
|
|
615
|
+
dialogs = [];
|
|
616
|
+
dialogEntityCache = {};
|
|
617
|
+
dialogsFetchedAt = 0;
|
|
618
|
+
contactMetadata = {};
|
|
619
|
+
cacheTTL = 36e5;
|
|
620
|
+
// 1 hour in milliseconds
|
|
621
|
+
botToken;
|
|
622
|
+
ownUserId;
|
|
623
|
+
// Flag indicating if using shared client from User Listener (should not disconnect when shared)
|
|
624
|
+
_isSharedClient = false;
|
|
625
|
+
asyncIteratorState = {
|
|
626
|
+
dialogs: [],
|
|
627
|
+
currentDialogIndex: 0,
|
|
628
|
+
currentMessageIndex: 0,
|
|
629
|
+
offsetDate: 0,
|
|
630
|
+
isInitialized: false
|
|
631
|
+
};
|
|
632
|
+
constructor(opts) {
|
|
633
|
+
super();
|
|
634
|
+
this.session = new StringSession(opts?.session ?? "");
|
|
635
|
+
this.botId = opts?.botId ?? "";
|
|
636
|
+
this.appId = opts?.appId ?? Number(process.env.TG_APP_ID ?? "0");
|
|
637
|
+
this.appHash = opts?.appHash ?? process.env.TG_APP_HASH ?? "";
|
|
638
|
+
const sessionKey = opts?.session ?? "";
|
|
639
|
+
const clientRegistry = opts?.clientRegistry;
|
|
640
|
+
let sharedClient;
|
|
641
|
+
if (!opts?.botToken && sessionKey && clientRegistry) {
|
|
642
|
+
sharedClient = clientRegistry.getClientBySessionKey(sessionKey);
|
|
643
|
+
}
|
|
644
|
+
if (sharedClient) {
|
|
645
|
+
this.client = sharedClient;
|
|
646
|
+
this._isSharedClient = true;
|
|
647
|
+
if (DEBUG)
|
|
648
|
+
console.log(
|
|
649
|
+
`[Bot ${opts?.botId}] [telegram] Reusing existing connection from User Listener to avoid session conflict`
|
|
650
|
+
);
|
|
651
|
+
} else {
|
|
652
|
+
this.client = new TelegramClient(this.session, this.appId, this.appHash, {
|
|
653
|
+
connectionRetries: 10,
|
|
654
|
+
timeout: 60,
|
|
655
|
+
requestRetries: 5,
|
|
656
|
+
floodSleepThreshold: 60
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
this.botToken = opts?.botToken;
|
|
660
|
+
this.messages = [];
|
|
661
|
+
this.ownerUserId = opts?.ownerUserId;
|
|
662
|
+
this.ownerUserType = opts?.ownerUserType;
|
|
663
|
+
this.fileIngester = opts?.fileIngester;
|
|
664
|
+
this.clientRegistry = clientRegistry;
|
|
665
|
+
if (opts?.cacheTTL) {
|
|
666
|
+
this.cacheTTL = opts.cacheTTL;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
primeContactMetadata(contactId, metadata) {
|
|
670
|
+
if (!contactId || !metadata) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (isTelegramContactMeta(metadata)) {
|
|
674
|
+
this.contactMetadata[contactId] = {
|
|
675
|
+
...metadata,
|
|
676
|
+
peerId: metadata.peerId ?? contactId
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async connect() {
|
|
681
|
+
try {
|
|
682
|
+
if (this._isSharedClient) {
|
|
683
|
+
if (!this.client.connected) {
|
|
684
|
+
console.warn(
|
|
685
|
+
`[Bot ${this.botId}] [telegram] Shared client disconnected, falling back to independent connection`
|
|
686
|
+
);
|
|
687
|
+
this._isSharedClient = false;
|
|
688
|
+
this.client = new TelegramClient(this.session, this.appId, this.appHash, {
|
|
689
|
+
connectionRetries: 10,
|
|
690
|
+
timeout: 60,
|
|
691
|
+
requestRetries: 5,
|
|
692
|
+
floodSleepThreshold: 60
|
|
693
|
+
});
|
|
694
|
+
} else {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (!this.client.connected) {
|
|
699
|
+
if (this.botToken) {
|
|
700
|
+
await this.client.start({
|
|
701
|
+
botAuthToken: this.botToken
|
|
702
|
+
});
|
|
703
|
+
} else {
|
|
704
|
+
await this.client.start({
|
|
705
|
+
phoneNumber: async () => await this.prompt("Please enter your phone number: "),
|
|
706
|
+
password: async () => await this.prompt("Please enter your password: "),
|
|
707
|
+
phoneCode: async () => await this.prompt("Please enter the verification code: "),
|
|
708
|
+
onError: (err) => console.error(`[Bot ${this.botId}] [telegram] Login error:`, err)
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error(`[Bot ${this.botId}] [telegram] Telegram connection error:`, error);
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Disconnect. If using shared client (from User Listener), skip disconnect
|
|
719
|
+
* to avoid disrupting User Listener's persistent connection.
|
|
720
|
+
*/
|
|
721
|
+
async disconnect() {
|
|
722
|
+
if (this._isSharedClient) {
|
|
723
|
+
if (DEBUG) console.log(`[Bot ${this.botId}] [telegram] Using shared client, skipping disconnect`);
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
if (this.client?.connected) {
|
|
727
|
+
await this.client.disconnect();
|
|
728
|
+
}
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
async prompt(question) {
|
|
732
|
+
return new Promise((resolve) => {
|
|
733
|
+
const readline = __require("readline").createInterface({
|
|
734
|
+
input: process.stdin,
|
|
735
|
+
output: process.stdout
|
|
736
|
+
});
|
|
737
|
+
readline.question(question, (answer) => {
|
|
738
|
+
readline.close();
|
|
739
|
+
resolve(answer);
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async getDialogs() {
|
|
744
|
+
if (!this.client.connected) {
|
|
745
|
+
await this.client.connect();
|
|
746
|
+
}
|
|
747
|
+
await this.refreshDialogsCache();
|
|
748
|
+
return this.dialogs;
|
|
749
|
+
}
|
|
750
|
+
async refreshDialogsCache(force = false) {
|
|
751
|
+
if (!this.client.connected) {
|
|
752
|
+
await this.client.connect();
|
|
753
|
+
}
|
|
754
|
+
const shouldRefresh = force || this.dialogs.length === 0 || Date.now() - this.dialogsFetchedAt > this.cacheTTL;
|
|
755
|
+
if (!shouldRefresh) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const dialogs = await this.client.getDialogs({
|
|
759
|
+
limit: dialogCacheFetchLimit
|
|
760
|
+
});
|
|
761
|
+
const nextDialogs = [];
|
|
762
|
+
for (const dialog of dialogs) {
|
|
763
|
+
if (!dialog.id || !dialog.entity) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const idStr = dialog.id.toString();
|
|
767
|
+
const type = dialog.isUser === true ? "private" : dialog.isChannel || dialog.isGroup ? "group" : "unknown";
|
|
768
|
+
const resolvedName = dialog.name && dialog.name.trim().length > 0 ? dialog.name : this.getChatName(dialog.entity);
|
|
769
|
+
const metadata = this.buildMetadataFromEntity(idStr, dialog.entity);
|
|
770
|
+
nextDialogs.push({
|
|
771
|
+
id: idStr,
|
|
772
|
+
name: resolvedName,
|
|
773
|
+
type,
|
|
774
|
+
metadata: metadata ?? void 0
|
|
775
|
+
});
|
|
776
|
+
this.addDialogEntityToCache(idStr, dialog.entity);
|
|
777
|
+
}
|
|
778
|
+
this.dialogs = nextDialogs;
|
|
779
|
+
this.dialogsFetchedAt = Date.now();
|
|
780
|
+
}
|
|
781
|
+
// Cache management methods
|
|
782
|
+
isCacheValid(cacheEntry) {
|
|
783
|
+
return Date.now() - cacheEntry.timestamp < this.cacheTTL;
|
|
784
|
+
}
|
|
785
|
+
getFromCache(cache, id) {
|
|
786
|
+
const idStr = id.toString();
|
|
787
|
+
const entry = cache[idStr];
|
|
788
|
+
if (entry && this.isCacheValid(entry)) {
|
|
789
|
+
return entry.entity;
|
|
790
|
+
}
|
|
791
|
+
if (entry) {
|
|
792
|
+
delete cache[idStr];
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
addToCache(cache, id, entity) {
|
|
797
|
+
cache[id.toString()] = {
|
|
798
|
+
entity,
|
|
799
|
+
timestamp: Date.now()
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
addDialogEntityToCache(id, entity) {
|
|
803
|
+
this.dialogEntityCache[id] = {
|
|
804
|
+
entity,
|
|
805
|
+
timestamp: Date.now()
|
|
806
|
+
};
|
|
807
|
+
this.registerMetadataFromEntity(id, entity);
|
|
808
|
+
}
|
|
809
|
+
registerMetadataFromEntity(id, entity) {
|
|
810
|
+
const metadata = this.buildMetadataFromEntity(id, entity);
|
|
811
|
+
if (metadata) {
|
|
812
|
+
this.contactMetadata[id] = metadata;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
buildMetadataFromEntity(id, entity) {
|
|
816
|
+
if (entity instanceof Api.User) {
|
|
817
|
+
const firstName = entity.firstName ?? null;
|
|
818
|
+
const lastName = entity.lastName ?? null;
|
|
819
|
+
const displayName = firstName && lastName ? `${firstName} ${lastName}`.trim() : firstName || lastName || entity.username || null;
|
|
820
|
+
return {
|
|
821
|
+
platform: "telegram",
|
|
822
|
+
peerId: id,
|
|
823
|
+
peerType: "user",
|
|
824
|
+
accessHash: entity.accessHash ? entity.accessHash.toString() : void 0,
|
|
825
|
+
username: entity.username ?? null,
|
|
826
|
+
firstName,
|
|
827
|
+
lastName,
|
|
828
|
+
displayName
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (entity instanceof Api.Channel) {
|
|
832
|
+
return {
|
|
833
|
+
platform: "telegram",
|
|
834
|
+
peerId: id,
|
|
835
|
+
peerType: "channel",
|
|
836
|
+
accessHash: entity.accessHash ? entity.accessHash.toString() : void 0,
|
|
837
|
+
username: entity.username ?? null,
|
|
838
|
+
displayName: entity.title || null
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (entity instanceof Api.Chat) {
|
|
842
|
+
return {
|
|
843
|
+
platform: "telegram",
|
|
844
|
+
peerId: id,
|
|
845
|
+
peerType: "chat",
|
|
846
|
+
displayName: entity.title || null
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
// Convert peer to input peer for API calls
|
|
852
|
+
peerToInputPeer(peer) {
|
|
853
|
+
if (peer instanceof Api.PeerUser) {
|
|
854
|
+
return new Api.InputPeerUser({
|
|
855
|
+
userId: peer.userId,
|
|
856
|
+
accessHash: bigInt(0)
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
if (peer instanceof Api.PeerChat) {
|
|
860
|
+
return new Api.InputPeerChat({ chatId: peer.chatId });
|
|
861
|
+
}
|
|
862
|
+
if (peer instanceof Api.PeerChannel) {
|
|
863
|
+
return new Api.InputPeerChannel({
|
|
864
|
+
channelId: peer.channelId,
|
|
865
|
+
accessHash: bigInt(0)
|
|
866
|
+
// Use BigInt literal instead
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return new Api.InputPeerEmpty();
|
|
870
|
+
}
|
|
871
|
+
// Get entity with caching
|
|
872
|
+
async getEntityWithCache(cache, entityId) {
|
|
873
|
+
let inputPeer;
|
|
874
|
+
let id;
|
|
875
|
+
if (entityId instanceof Api.PeerUser) {
|
|
876
|
+
id = entityId.userId;
|
|
877
|
+
inputPeer = this.peerToInputPeer(entityId);
|
|
878
|
+
} else if (entityId instanceof Api.PeerChat) {
|
|
879
|
+
id = entityId.chatId;
|
|
880
|
+
inputPeer = this.peerToInputPeer(entityId);
|
|
881
|
+
} else if (entityId instanceof Api.PeerChannel) {
|
|
882
|
+
id = entityId.channelId;
|
|
883
|
+
inputPeer = this.peerToInputPeer(entityId);
|
|
884
|
+
} else if (entityId instanceof Api.InputPeerUser || entityId instanceof Api.InputPeerChat || entityId instanceof Api.InputPeerChannel) {
|
|
885
|
+
if (entityId instanceof Api.InputPeerUser) {
|
|
886
|
+
id = entityId.userId;
|
|
887
|
+
} else if (entityId instanceof Api.InputPeerChat) {
|
|
888
|
+
id = entityId.chatId;
|
|
889
|
+
} else {
|
|
890
|
+
id = entityId.channelId;
|
|
891
|
+
}
|
|
892
|
+
inputPeer = entityId;
|
|
893
|
+
} else {
|
|
894
|
+
id = entityId;
|
|
895
|
+
const entity = await this.client.getEntity(entityId);
|
|
896
|
+
this.addToCache(cache, id, entity);
|
|
897
|
+
return entity;
|
|
898
|
+
}
|
|
899
|
+
const cachedEntity = this.getFromCache(cache, id);
|
|
900
|
+
if (cachedEntity) {
|
|
901
|
+
return cachedEntity;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const entity = await this.client.getEntity(inputPeer);
|
|
905
|
+
this.addToCache(cache, id, entity);
|
|
906
|
+
return entity;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.error(`[Bot ${this.botId}] [telegram] Error fetching entity ${id}:`, error);
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async sendMessage(target, id, message) {
|
|
913
|
+
await this.sendMessages(target, id, [message]);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Reply to a message
|
|
917
|
+
* @param event - Source message event
|
|
918
|
+
* @param messages - Message chain to send as reply
|
|
919
|
+
* @param quoteOrigin - Whether to quote the original message (default: false)
|
|
920
|
+
*/
|
|
921
|
+
async sendMessages(target, id, messages) {
|
|
922
|
+
await this.runWithAdapterError("sendMessages", async () => {
|
|
923
|
+
if (!this.client.connected) {
|
|
924
|
+
await withTimeout(
|
|
925
|
+
this.client.connect(),
|
|
926
|
+
CONNECT_TIMEOUT_MS,
|
|
927
|
+
`[Bot ${this.botId}] Telegram client.connect()`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (messages.length === 0) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const { textParts, images, unsupported } = this.partitionMessages(messages);
|
|
934
|
+
if (unsupported.length > 0) {
|
|
935
|
+
throw this.createAdapterError(
|
|
936
|
+
"sendMessages",
|
|
937
|
+
"invalid_request_error",
|
|
938
|
+
`Telegram send does not support message content: ${unsupported.join(", ")}`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
await this.dispatchMessages("sendMessages", id, textParts, images);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
async replyMessages(event, messages, quoteOrigin = false) {
|
|
945
|
+
await this.runWithAdapterError("replyMessages", async () => {
|
|
946
|
+
if (!this.client.connected) {
|
|
947
|
+
await withTimeout(
|
|
948
|
+
this.client.connect(),
|
|
949
|
+
CONNECT_TIMEOUT_MS,
|
|
950
|
+
`[Bot ${this.botId}] Telegram client.connect()`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (messages.length === 0) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const id = event.targetType === "group" ? event.sender.group.id : event.sender.id;
|
|
957
|
+
const { textParts, images, unsupported } = this.partitionMessages(messages);
|
|
958
|
+
if (unsupported.length > 0) {
|
|
959
|
+
throw this.createAdapterError(
|
|
960
|
+
"replyMessages",
|
|
961
|
+
"invalid_request_error",
|
|
962
|
+
`Telegram reply does not support message content: ${unsupported.join(", ")}`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
await this.dispatchMessages("replyMessages", id.toString(), textParts, images);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
async getChatsByChunk(since, chunkSize) {
|
|
969
|
+
const maxMessageChunkCount = chunkSize ?? DEFAULT_MAX_MESSAGE_CHUNK_COUNT;
|
|
970
|
+
if (!this.client.connected) {
|
|
971
|
+
if (DEBUG) console.log("[Telegram] Client not connected, connecting...");
|
|
972
|
+
await this.client.connect();
|
|
973
|
+
await delay(500);
|
|
974
|
+
}
|
|
975
|
+
const extractedMessages = [];
|
|
976
|
+
if (!this.asyncIteratorState.isInitialized) {
|
|
977
|
+
const allDialogs = await this.client.getDialogs();
|
|
978
|
+
this.asyncIteratorState.dialogs = allDialogs.filter((dialog) => dialog.entity && dialog.id);
|
|
979
|
+
this.asyncIteratorState.currentDialogIndex = 0;
|
|
980
|
+
this.asyncIteratorState.currentMessageIndex = 0;
|
|
981
|
+
this.asyncIteratorState.isInitialized = true;
|
|
982
|
+
this.asyncIteratorState.offsetDate = since;
|
|
983
|
+
if (DEBUG)
|
|
984
|
+
console.log(
|
|
985
|
+
`[Bot ${this.botId}] [telegram] Batch fetch initialization complete: ${this.asyncIteratorState.dialogs.length} valid dialogs`
|
|
986
|
+
);
|
|
987
|
+
if (this.asyncIteratorState.dialogs.length === 0) {
|
|
988
|
+
if (DEBUG) console.log(`[Bot ${this.botId}] No valid dialogs to process`);
|
|
989
|
+
this.asyncIteratorState.isInitialized = false;
|
|
990
|
+
this.asyncIteratorState.currentDialogIndex = 0;
|
|
991
|
+
this.asyncIteratorState.currentMessageIndex = 0;
|
|
992
|
+
return { messages: extractedMessages, hasMore: false };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
let { currentDialogIndex, currentMessageIndex, offsetDate } = this.asyncIteratorState;
|
|
996
|
+
const targetDialogs = this.asyncIteratorState.dialogs;
|
|
997
|
+
const MIN_MESSAGES_PER_DIALOG = 5;
|
|
998
|
+
const REQUEST_DELAY_MS = 150;
|
|
999
|
+
for (let dialogIdx = currentDialogIndex; dialogIdx < targetDialogs.length; dialogIdx++) {
|
|
1000
|
+
const dialog = targetDialogs[dialogIdx];
|
|
1001
|
+
try {
|
|
1002
|
+
if (!this.client.connected) {
|
|
1003
|
+
if (DEBUG) console.log("[Telegram] Client not connected, connecting...");
|
|
1004
|
+
await this.client.connect();
|
|
1005
|
+
await delay(500);
|
|
1006
|
+
}
|
|
1007
|
+
if (dialogIdx > currentDialogIndex) {
|
|
1008
|
+
await delay(REQUEST_DELAY_MS);
|
|
1009
|
+
}
|
|
1010
|
+
const rawMessages = await this.client.getMessages(dialog.id, {
|
|
1011
|
+
offsetDate,
|
|
1012
|
+
reverse: true
|
|
1013
|
+
});
|
|
1014
|
+
if (DEBUG)
|
|
1015
|
+
console.log(
|
|
1016
|
+
`[Bot ${this.botId}] [telegram] Fetched ${rawMessages.length} messages in the dialog id ${dialog.id}`
|
|
1017
|
+
);
|
|
1018
|
+
if (rawMessages.length === 0 && dialogIdx >= 10) {
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
const startMsgIdx = currentMessageIndex;
|
|
1022
|
+
let dialogMessageCount = 0;
|
|
1023
|
+
for (let msgIdx = startMsgIdx; msgIdx < rawMessages.length; msgIdx++) {
|
|
1024
|
+
const rawMsg = rawMessages[msgIdx];
|
|
1025
|
+
if (!(rawMsg instanceof Api.Message)) continue;
|
|
1026
|
+
const extractedInfo = await this.extractMessageInfo(rawMsg);
|
|
1027
|
+
if (extractedInfo && !isEmptyMessage(extractedInfo)) {
|
|
1028
|
+
extractedMessages.push(extractedInfo);
|
|
1029
|
+
dialogMessageCount++;
|
|
1030
|
+
if (extractedMessages.length >= maxMessageChunkCount) {
|
|
1031
|
+
this.asyncIteratorState.currentDialogIndex = dialogIdx;
|
|
1032
|
+
this.asyncIteratorState.currentMessageIndex = msgIdx + 1;
|
|
1033
|
+
if (DEBUG)
|
|
1034
|
+
console.log(
|
|
1035
|
+
`[Bot ${this.botId}] Batch full (${maxMessageChunkCount}), continue from dialog ${dialog.id} message ${msgIdx + 1}`
|
|
1036
|
+
);
|
|
1037
|
+
return {
|
|
1038
|
+
messages: extractedMessages,
|
|
1039
|
+
hasMore: true
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (dialogMessageCount < MIN_MESSAGES_PER_DIALOG && rawMessages.length > 0) {
|
|
1045
|
+
const neededCount = MIN_MESSAGES_PER_DIALOG - dialogMessageCount;
|
|
1046
|
+
if (DEBUG)
|
|
1047
|
+
console.log(
|
|
1048
|
+
`[Bot ${this.botId}] [telegram] Dialog ${dialog.id} has only ${dialogMessageCount} messages, fetching ${neededCount} more without time filter`
|
|
1049
|
+
);
|
|
1050
|
+
await delay(REQUEST_DELAY_MS);
|
|
1051
|
+
try {
|
|
1052
|
+
const additionalMessages = await this.client.getMessages(dialog.id, {
|
|
1053
|
+
limit: neededCount
|
|
1054
|
+
});
|
|
1055
|
+
for (const rawMsg of additionalMessages) {
|
|
1056
|
+
if (!(rawMsg instanceof Api.Message)) continue;
|
|
1057
|
+
const extractedInfo = await this.extractMessageInfo(rawMsg);
|
|
1058
|
+
if (extractedInfo && !isEmptyMessage(extractedInfo)) {
|
|
1059
|
+
extractedMessages.push(extractedInfo);
|
|
1060
|
+
dialogMessageCount++;
|
|
1061
|
+
if (extractedMessages.length >= maxMessageChunkCount) {
|
|
1062
|
+
this.asyncIteratorState.currentDialogIndex = dialogIdx;
|
|
1063
|
+
this.asyncIteratorState.currentMessageIndex = rawMessages.length;
|
|
1064
|
+
if (DEBUG)
|
|
1065
|
+
console.log(
|
|
1066
|
+
`[Bot ${this.botId}] Batch full (${maxMessageChunkCount}), continue from dialog ${dialog.id}`
|
|
1067
|
+
);
|
|
1068
|
+
return {
|
|
1069
|
+
messages: extractedMessages,
|
|
1070
|
+
hasMore: true
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (DEBUG)
|
|
1076
|
+
console.log(
|
|
1077
|
+
`[Bot ${this.botId}] [telegram] Fetched ${additionalMessages.length} additional messages for dialog ${dialog.id}, total: ${dialogMessageCount}`
|
|
1078
|
+
);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.warn(
|
|
1081
|
+
`[Bot ${this.botId}] [telegram] Failed to fetch additional messages for dialog ${dialog.id}:`,
|
|
1082
|
+
error
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
currentMessageIndex = 0;
|
|
1087
|
+
this.asyncIteratorState.currentMessageIndex = 0;
|
|
1088
|
+
this.asyncIteratorState.currentDialogIndex = dialogIdx + 1;
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
console.error(`[Bot ${this.botId}] Failed to process dialog ${dialog.id}:`, error);
|
|
1091
|
+
this.asyncIteratorState.currentDialogIndex = dialogIdx + 1;
|
|
1092
|
+
this.asyncIteratorState.currentMessageIndex = 0;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (DEBUG)
|
|
1096
|
+
console.log(
|
|
1097
|
+
`[Bot ${this.botId}] All dialogs processed, returning remaining ${extractedMessages.length} messages`
|
|
1098
|
+
);
|
|
1099
|
+
this.asyncIteratorState.isInitialized = false;
|
|
1100
|
+
this.asyncIteratorState.currentDialogIndex = 0;
|
|
1101
|
+
this.asyncIteratorState.currentMessageIndex = 0;
|
|
1102
|
+
return {
|
|
1103
|
+
messages: extractedMessages,
|
|
1104
|
+
hasMore: false
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async getChatsByChunkHours(hours = 8) {
|
|
1108
|
+
return this.getChatsByChunk(timeBeforeHours(hours));
|
|
1109
|
+
}
|
|
1110
|
+
async getChatsByTime(cutoffDate) {
|
|
1111
|
+
if (!this.client.connected) {
|
|
1112
|
+
await this.client.connect();
|
|
1113
|
+
}
|
|
1114
|
+
const extractedMessages = [];
|
|
1115
|
+
const dialogs = await this.client.getDialogs({});
|
|
1116
|
+
if (DEBUG) console.log(`[Bot ${this.botId}] [telegram] Fetched ${dialogs.length} dialogs`);
|
|
1117
|
+
let dialogIndex = 0;
|
|
1118
|
+
const REQUEST_DELAY_MS = 150;
|
|
1119
|
+
for (const dialog of dialogs) {
|
|
1120
|
+
if (!dialog.entity || !dialog.id) {
|
|
1121
|
+
dialogIndex += 1;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
this.addToCache(this.chatCache, dialog.id, dialog.entity);
|
|
1125
|
+
if (dialogIndex > 0) {
|
|
1126
|
+
await delay(REQUEST_DELAY_MS);
|
|
1127
|
+
}
|
|
1128
|
+
const messages = await this.client.getMessages(dialog.id, {
|
|
1129
|
+
offsetDate: cutoffDate,
|
|
1130
|
+
reverse: true
|
|
1131
|
+
});
|
|
1132
|
+
if (DEBUG)
|
|
1133
|
+
console.log(
|
|
1134
|
+
`[Bot ${this.botId}] [telegram] Fetched ${messages.length} messages in the dialog id ${dialog.id}`
|
|
1135
|
+
);
|
|
1136
|
+
if (messages.length === 0 && dialogIndex >= 10 || dialogIndex >= maxDialogCount) {
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
dialogIndex += 1;
|
|
1140
|
+
let msgIndex = 0;
|
|
1141
|
+
for (const msg of messages) {
|
|
1142
|
+
if (msgIndex >= maxMessageCount) {
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
if (msg instanceof Api.Message) {
|
|
1146
|
+
msgIndex += 1;
|
|
1147
|
+
const extractedInfo = await this.extractMessageInfo(msg);
|
|
1148
|
+
if (extractedInfo && !isEmptyMessage(extractedInfo)) {
|
|
1149
|
+
extractedMessages.push(extractedInfo);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (DEBUG)
|
|
1155
|
+
console.log(
|
|
1156
|
+
`[Bot ${this.botId}] [telegram] Fetched ${extractedMessages.length} total messages (capped at ${maxMessageCount})`
|
|
1157
|
+
);
|
|
1158
|
+
return extractedMessages;
|
|
1159
|
+
}
|
|
1160
|
+
async getChatsByDays(days = 1) {
|
|
1161
|
+
const cutoffDate = Math.floor((Date.now() - days * 24 * 60 * 60 * 1e3) / 1e3);
|
|
1162
|
+
return await this.getChatsByTime(cutoffDate);
|
|
1163
|
+
}
|
|
1164
|
+
async getChatsByHours(hours = 1) {
|
|
1165
|
+
const cutoffDate = Math.floor((Date.now() - hours * 60 * 60 * 1e3) / 1e3);
|
|
1166
|
+
return await this.getChatsByTime(cutoffDate);
|
|
1167
|
+
}
|
|
1168
|
+
// Extract detailed information from a message with caching
|
|
1169
|
+
async extractMessageInfo(message, depth = 0, maxDepth = 1) {
|
|
1170
|
+
try {
|
|
1171
|
+
const chat = await this.getEntityWithCache(this.chatCache, message.peerId) ?? await this.client.getEntity(message.peerId);
|
|
1172
|
+
if (!chat) {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
const chatType = this.determineChatType(chat);
|
|
1176
|
+
if (chatType === "private" && chat instanceof Api.User) {
|
|
1177
|
+
try {
|
|
1178
|
+
if (!this.ownUserId) {
|
|
1179
|
+
const me = await this.client.getMe();
|
|
1180
|
+
this.ownUserId = me.id;
|
|
1181
|
+
}
|
|
1182
|
+
if (chat.id.equals(this.ownUserId)) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
console.warn(
|
|
1187
|
+
`[Bot ${this.botId}] [telegram] Failed to get user info for self-message filter:`,
|
|
1188
|
+
error
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
let sender = null;
|
|
1193
|
+
if (chatType === "channel" || chatType === "group") {
|
|
1194
|
+
if (message.fromId) {
|
|
1195
|
+
sender = await this.getEntityWithCache(this.senderCache, message.fromId);
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
if (message.senderId) {
|
|
1199
|
+
sender = await this.getEntityWithCache(this.senderCache, message.senderId);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const senderName = this.getSenderName(sender);
|
|
1203
|
+
const chatName = this.getChatName(chat);
|
|
1204
|
+
const chatUserName = "username" in chat ? chat.username : null;
|
|
1205
|
+
const msgInfo = {
|
|
1206
|
+
id: message.id,
|
|
1207
|
+
// Save Telegram original message ID for deduplication
|
|
1208
|
+
sender: chatType === "private" ? senderName ?? chatName : senderName ?? chatUserName ?? `anonymous user ${message.senderId}`,
|
|
1209
|
+
chatName,
|
|
1210
|
+
chatType,
|
|
1211
|
+
text: message.message || "",
|
|
1212
|
+
timestamp: message.date,
|
|
1213
|
+
// Use message.out property to determine message direction: true means sent by me, false means received from others
|
|
1214
|
+
isOutgoing: message.out
|
|
1215
|
+
};
|
|
1216
|
+
if (msgInfo.sender.includes("anonymous user") && message.senderId) {
|
|
1217
|
+
sender = await this.getEntityWithCache(this.senderCache, message.senderId);
|
|
1218
|
+
const senderName2 = this.getSenderName(sender);
|
|
1219
|
+
if (senderName2) {
|
|
1220
|
+
msgInfo.sender = senderName2;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
let quoted = null;
|
|
1224
|
+
if (message.replyToMsgId && depth < maxDepth) {
|
|
1225
|
+
try {
|
|
1226
|
+
const quotedMsgList = await this.client.getMessages(message.peerId, {
|
|
1227
|
+
ids: message.replyToMsgId
|
|
1228
|
+
});
|
|
1229
|
+
if (quotedMsgList.length > 0 && quotedMsgList[0] instanceof Api.Message) {
|
|
1230
|
+
quoted = await this.extractMessageInfo(quotedMsgList[0], depth + 1, maxDepth);
|
|
1231
|
+
}
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const result = isEmptyMessage(quoted) ? msgInfo : { ...msgInfo, quoted };
|
|
1236
|
+
const attachments = depth === 0 ? await this.extractAttachmentsFromMessage(message) : [];
|
|
1237
|
+
return attachments.length > 0 ? { ...result, attachments } : result;
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
console.error(`[Bot ${this.botId}] [telegram] Error extracting message info:`, error);
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// Determine the type of chat
|
|
1244
|
+
determineChatType(chat) {
|
|
1245
|
+
if (chat instanceof Api.User) return "private";
|
|
1246
|
+
if (chat instanceof Api.Chat) return "group";
|
|
1247
|
+
if (chat instanceof Api.Channel) return "channel";
|
|
1248
|
+
return "unknown";
|
|
1249
|
+
}
|
|
1250
|
+
// Get appropriate chat name based on entity type
|
|
1251
|
+
getChatName(chat) {
|
|
1252
|
+
if (chat instanceof Api.User) {
|
|
1253
|
+
const name = `${chat.firstName || ""} ${chat.lastName || ""}`.trim();
|
|
1254
|
+
if (name) return name;
|
|
1255
|
+
if (chat.username) return `@${chat.username}`;
|
|
1256
|
+
return "Private Chat";
|
|
1257
|
+
}
|
|
1258
|
+
if (chat instanceof Api.Chat) {
|
|
1259
|
+
if (chat.title) return chat.title;
|
|
1260
|
+
const chatId = chat.id?.toString();
|
|
1261
|
+
if (chatId) {
|
|
1262
|
+
console.warn(`[Bot ${this.botId}] [telegram] Chat ${chatId} missing title, using ID as fallback`);
|
|
1263
|
+
return `Chat ${chatId}`;
|
|
1264
|
+
}
|
|
1265
|
+
return "Unnamed Chat";
|
|
1266
|
+
}
|
|
1267
|
+
if (chat instanceof Api.Channel) {
|
|
1268
|
+
if (chat.title) return chat.title;
|
|
1269
|
+
if (chat.username) return `@${chat.username}`;
|
|
1270
|
+
const channelId = chat.id?.toString();
|
|
1271
|
+
if (channelId) {
|
|
1272
|
+
console.warn(
|
|
1273
|
+
`[Bot ${this.botId}] [telegram] Channel ${channelId} missing title and username, using ID as fallback`
|
|
1274
|
+
);
|
|
1275
|
+
return `Channel ${channelId}`;
|
|
1276
|
+
}
|
|
1277
|
+
return "Unnamed Chat";
|
|
1278
|
+
}
|
|
1279
|
+
console.warn(`[Bot ${this.botId}] [telegram] Unknown entity type in getChatName:`, chat?.className);
|
|
1280
|
+
return "Unknown Chat";
|
|
1281
|
+
}
|
|
1282
|
+
getSenderName(sender) {
|
|
1283
|
+
if (!sender) return null;
|
|
1284
|
+
if (sender instanceof Api.User) {
|
|
1285
|
+
if (sender.firstName || sender.lastName) {
|
|
1286
|
+
return `${sender.firstName || ""} ${sender.lastName || ""}`.trim();
|
|
1287
|
+
}
|
|
1288
|
+
return sender.username ?? "";
|
|
1289
|
+
}
|
|
1290
|
+
if (sender instanceof Api.Channel) {
|
|
1291
|
+
return sender.title;
|
|
1292
|
+
}
|
|
1293
|
+
if (sender instanceof Api.Chat) {
|
|
1294
|
+
return sender.title;
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
// Clear cache manually if needed
|
|
1299
|
+
clearCache() {
|
|
1300
|
+
this.senderCache = {};
|
|
1301
|
+
this.chatCache = {};
|
|
1302
|
+
}
|
|
1303
|
+
isImageMessage(message) {
|
|
1304
|
+
return typeof message === "object" && message !== null && "url" in message && !("length" in message) && !("name" in message) && typeof message.url === "string" && message.url.length > 0;
|
|
1305
|
+
}
|
|
1306
|
+
partitionMessages(messages) {
|
|
1307
|
+
const textParts = [];
|
|
1308
|
+
const images = [];
|
|
1309
|
+
const unsupported = [];
|
|
1310
|
+
for (const message of messages) {
|
|
1311
|
+
if (this.isImageMessage(message)) {
|
|
1312
|
+
images.push(message);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const text = opencontextMessageToTgText(message);
|
|
1316
|
+
if (text.trim().length > 0) {
|
|
1317
|
+
textParts.push(text);
|
|
1318
|
+
} else if (typeof message !== "string") {
|
|
1319
|
+
unsupported.push(describeUnsupportedMessage(message));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return { textParts, images, unsupported };
|
|
1323
|
+
}
|
|
1324
|
+
inferFileName(image) {
|
|
1325
|
+
if (image.id && image.id.trim().length > 0) {
|
|
1326
|
+
return image.id;
|
|
1327
|
+
}
|
|
1328
|
+
if (image.path) {
|
|
1329
|
+
const segments = image.path.split("/");
|
|
1330
|
+
const candidate = segments[segments.length - 1];
|
|
1331
|
+
if (candidate) return candidate;
|
|
1332
|
+
}
|
|
1333
|
+
if (image.url) {
|
|
1334
|
+
try {
|
|
1335
|
+
const url = new URL(image.url);
|
|
1336
|
+
const filename = url.pathname.split("/").pop();
|
|
1337
|
+
if (filename && filename.trim().length > 0) {
|
|
1338
|
+
return filename;
|
|
1339
|
+
}
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
console.warn(`[Bot ${this.botId}] [telegram] Failed to parse filename from URL`, error);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return `image-${Date.now()}.jpg`;
|
|
1345
|
+
}
|
|
1346
|
+
decodeBase64Payload(image) {
|
|
1347
|
+
if (!image.base64) {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
const base64String = image.base64.includes(",") ? image.base64.split(",").pop() : image.base64;
|
|
1351
|
+
if (!base64String) {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
return Buffer.from(base64String, "base64");
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
console.error(`[Bot ${this.botId}] [telegram] Failed to decode base64 image payload`, error);
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async downloadMediaBuffer(message) {
|
|
1362
|
+
try {
|
|
1363
|
+
const result = await this.client.downloadMedia(message);
|
|
1364
|
+
if (!result) return null;
|
|
1365
|
+
if (Buffer.isBuffer(result)) {
|
|
1366
|
+
return result;
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof result === "string") {
|
|
1369
|
+
const fs = await import('fs/promises');
|
|
1370
|
+
return await fs.readFile(result);
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
console.error(`[Bot ${this.botId}] [telegram] Failed to download media buffer`, error);
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async extractAttachmentsFromMessage(message) {
|
|
1379
|
+
if (!this.ownerUserId || !this.ownerUserType) {
|
|
1380
|
+
return [];
|
|
1381
|
+
}
|
|
1382
|
+
if (!message.media) {
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
const attachments = [];
|
|
1386
|
+
const pushAttachment = async (buffer, fileName, mimeType) => {
|
|
1387
|
+
if (!this.ownerUserId || !this.ownerUserType) {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const downloadPayload = async () => ({
|
|
1391
|
+
data: buffer,
|
|
1392
|
+
contentType: mimeType,
|
|
1393
|
+
sizeBytes: buffer.length
|
|
1394
|
+
});
|
|
1395
|
+
if (this.fileIngester) {
|
|
1396
|
+
const result = await this.fileIngester.ingestExternal({
|
|
1397
|
+
source: "telegram",
|
|
1398
|
+
userId: this.ownerUserId,
|
|
1399
|
+
maxSizeBytes: TELEGRAM_MAX_ATTACHMENT_BYTES,
|
|
1400
|
+
mimeTypeHint: mimeType,
|
|
1401
|
+
originalFileName: fileName,
|
|
1402
|
+
downloadAttachment: downloadPayload
|
|
1403
|
+
});
|
|
1404
|
+
if (!result.success) {
|
|
1405
|
+
console.warn(
|
|
1406
|
+
`[Bot ${this.botId}] [telegram] Skipped attachment due to ${result.reason} mimeType=${mimeType} fileName=${fileName}`
|
|
1407
|
+
);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (!result.attachment) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const ingested = result.attachment;
|
|
1414
|
+
attachments.push({
|
|
1415
|
+
name: ingested.name,
|
|
1416
|
+
url: ingested.url,
|
|
1417
|
+
downloadUrl: ingested.downloadUrl,
|
|
1418
|
+
contentType: ingested.contentType,
|
|
1419
|
+
sizeBytes: ingested.sizeBytes,
|
|
1420
|
+
blobPath: ingested.blobPath,
|
|
1421
|
+
source: "telegram"
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
if (message.media instanceof Api.MessageMediaPhoto) {
|
|
1426
|
+
const buffer = await this.downloadMediaBuffer(message);
|
|
1427
|
+
if (buffer) {
|
|
1428
|
+
const fileName = `telegram-photo-${message.id ?? Date.now()}.jpg`;
|
|
1429
|
+
await pushAttachment(buffer, fileName, "image/jpeg");
|
|
1430
|
+
}
|
|
1431
|
+
return attachments;
|
|
1432
|
+
}
|
|
1433
|
+
if (message.media instanceof Api.MessageMediaDocument) {
|
|
1434
|
+
const document = message.media.document;
|
|
1435
|
+
if (!document || !(document instanceof Api.Document)) {
|
|
1436
|
+
return attachments;
|
|
1437
|
+
}
|
|
1438
|
+
const buffer = await this.downloadMediaBuffer(message);
|
|
1439
|
+
if (!buffer) {
|
|
1440
|
+
return attachments;
|
|
1441
|
+
}
|
|
1442
|
+
const mimeType = document.mimeType ?? "application/octet-stream";
|
|
1443
|
+
let fileName = `telegram-file-${document.id ?? message.id ?? Date.now()}`;
|
|
1444
|
+
for (const attr of document.attributes ?? []) {
|
|
1445
|
+
if (attr instanceof Api.DocumentAttributeFilename) {
|
|
1446
|
+
fileName = attr.fileName;
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
await pushAttachment(buffer, fileName, mimeType);
|
|
1451
|
+
return attachments;
|
|
1452
|
+
}
|
|
1453
|
+
return attachments;
|
|
1454
|
+
}
|
|
1455
|
+
async resolveTelegramFile(image) {
|
|
1456
|
+
if (image.base64) {
|
|
1457
|
+
const buffer = this.decodeBase64Payload(image);
|
|
1458
|
+
if (buffer) {
|
|
1459
|
+
const name = this.inferFileName(image);
|
|
1460
|
+
return new CustomFile(name, buffer.length, "", buffer);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (image.path) {
|
|
1464
|
+
return image.path;
|
|
1465
|
+
}
|
|
1466
|
+
if (image.url) {
|
|
1467
|
+
return image.url;
|
|
1468
|
+
}
|
|
1469
|
+
throw new Error("Unable to resolve Telegram file source from image payload");
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Build InputPeer based on contactId and metadata
|
|
1473
|
+
* If metadata lacks accessHash, will try to resolve entity from Telegram API
|
|
1474
|
+
*/
|
|
1475
|
+
async buildInputPeerFromMetadata(contactId) {
|
|
1476
|
+
const metadata = this.contactMetadata[contactId];
|
|
1477
|
+
if (!metadata) {
|
|
1478
|
+
return await this.resolveEntityToInputPeer(contactId);
|
|
1479
|
+
}
|
|
1480
|
+
const peerId = bigInt(metadata.peerId);
|
|
1481
|
+
if (metadata.peerType === "user") {
|
|
1482
|
+
if (metadata.accessHash) {
|
|
1483
|
+
return new Api.InputPeerUser({
|
|
1484
|
+
userId: peerId,
|
|
1485
|
+
accessHash: bigInt(metadata.accessHash)
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return await this.resolveEntityToInputPeer(contactId, peerId, "user");
|
|
1489
|
+
}
|
|
1490
|
+
if (metadata.peerType === "channel") {
|
|
1491
|
+
if (metadata.accessHash) {
|
|
1492
|
+
return new Api.InputPeerChannel({
|
|
1493
|
+
channelId: peerId,
|
|
1494
|
+
accessHash: bigInt(metadata.accessHash)
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return await this.resolveEntityToInputPeer(contactId, peerId, "channel");
|
|
1498
|
+
}
|
|
1499
|
+
if (metadata.peerType === "chat") {
|
|
1500
|
+
if (peerId.isNegative()) {
|
|
1501
|
+
return await this.resolveEntityToInputPeer(contactId, peerId, "channel");
|
|
1502
|
+
}
|
|
1503
|
+
return new Api.InputPeerChat({
|
|
1504
|
+
chatId: peerId
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
return await this.resolveEntityToInputPeer(contactId);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Try to resolve entity from Telegram API and build InputPeer
|
|
1511
|
+
* Updates contactMetadata cache after successful resolution
|
|
1512
|
+
*/
|
|
1513
|
+
async resolveEntityToInputPeer(contactId, fallbackPeerId, fallbackType) {
|
|
1514
|
+
try {
|
|
1515
|
+
if (!this.client.connected) {
|
|
1516
|
+
await this.client.connect();
|
|
1517
|
+
}
|
|
1518
|
+
const entity = await this.client.getEntity(contactId);
|
|
1519
|
+
if (!entity) {
|
|
1520
|
+
throw new Error(`Entity not found for contactId: ${contactId}`);
|
|
1521
|
+
}
|
|
1522
|
+
const metadata = this.buildMetadataFromEntity(contactId, entity);
|
|
1523
|
+
if (metadata) {
|
|
1524
|
+
this.contactMetadata[contactId] = metadata;
|
|
1525
|
+
}
|
|
1526
|
+
if (entity instanceof Api.User) {
|
|
1527
|
+
const userId = entity.id.valueOf();
|
|
1528
|
+
const accessHash = entity.accessHash ? bigInt(entity.accessHash) : bigInt(0);
|
|
1529
|
+
const inputPeer = new Api.InputPeerUser({
|
|
1530
|
+
userId: bigInt(userId),
|
|
1531
|
+
accessHash
|
|
1532
|
+
});
|
|
1533
|
+
return inputPeer;
|
|
1534
|
+
}
|
|
1535
|
+
if (entity instanceof Api.Channel) {
|
|
1536
|
+
const channelId = entity.id.valueOf();
|
|
1537
|
+
const accessHash = entity.accessHash ? bigInt(entity.accessHash) : bigInt(0);
|
|
1538
|
+
const inputPeer = new Api.InputPeerChannel({
|
|
1539
|
+
channelId: bigInt(channelId),
|
|
1540
|
+
accessHash
|
|
1541
|
+
});
|
|
1542
|
+
return inputPeer;
|
|
1543
|
+
}
|
|
1544
|
+
if (entity instanceof Api.Chat) {
|
|
1545
|
+
const chatId = entity.id.valueOf();
|
|
1546
|
+
const inputPeer = new Api.InputPeerChat({
|
|
1547
|
+
chatId: bigInt(chatId)
|
|
1548
|
+
});
|
|
1549
|
+
return inputPeer;
|
|
1550
|
+
}
|
|
1551
|
+
throw new Error(`Unsupported entity type for contactId: ${contactId}`);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
console.error(`[Bot ${this.botId}] [telegram] Failed to resolve entity for ${contactId}:`, error);
|
|
1554
|
+
throw new Error(
|
|
1555
|
+
`Cannot resolve Telegram peer for ${contactId}. The contact may not exist or may not be accessible.`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async dispatchMessages(operation, peer, textParts, images) {
|
|
1560
|
+
const text = textParts.join("\n").trim();
|
|
1561
|
+
const inputPeer = await this.buildInputPeerFromMetadata(peer);
|
|
1562
|
+
if (images.length === 0) {
|
|
1563
|
+
if (text.length === 0) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
await withTimeout(
|
|
1567
|
+
this.client.sendMessage(inputPeer, {
|
|
1568
|
+
message: markdownToTelegramHtml(text),
|
|
1569
|
+
parseMode: "html"
|
|
1570
|
+
}),
|
|
1571
|
+
SEND_MESSAGE_TIMEOUT_MS,
|
|
1572
|
+
`[Bot ${this.botId}] Telegram client.sendMessage()`
|
|
1573
|
+
);
|
|
1574
|
+
if (DEBUG) {
|
|
1575
|
+
console.log(`[Bot ${this.botId}] [telegram] Message sent successfully to ${peer}`);
|
|
1576
|
+
}
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const caption = text.length > 0 ? text : void 0;
|
|
1580
|
+
let captionDelivered = false;
|
|
1581
|
+
for (let index = 0; index < images.length; index++) {
|
|
1582
|
+
const image = images[index];
|
|
1583
|
+
let fileLike;
|
|
1584
|
+
try {
|
|
1585
|
+
fileLike = await this.resolveTelegramFile(image);
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
console.error(`[Bot ${this.botId}] [telegram] Failed to resolve file for upload`, error);
|
|
1588
|
+
throw this.toAdapterError(operation, error, {
|
|
1589
|
+
fallbackCode: "invalid_request_error"
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
const options = {
|
|
1593
|
+
file: fileLike
|
|
1594
|
+
};
|
|
1595
|
+
if (index === 0 && caption) {
|
|
1596
|
+
options.caption = caption;
|
|
1597
|
+
}
|
|
1598
|
+
await withTimeout(
|
|
1599
|
+
this.client.sendFile(inputPeer, options),
|
|
1600
|
+
SEND_MESSAGE_TIMEOUT_MS,
|
|
1601
|
+
`[Bot ${this.botId}] Telegram client.sendFile()`
|
|
1602
|
+
);
|
|
1603
|
+
if (index === 0 && caption) {
|
|
1604
|
+
captionDelivered = true;
|
|
1605
|
+
}
|
|
1606
|
+
if (DEBUG) {
|
|
1607
|
+
console.log(`[Bot ${this.botId}] [telegram] File sent successfully to ${peer}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (caption && !captionDelivered) {
|
|
1611
|
+
await withTimeout(
|
|
1612
|
+
this.client.sendMessage(inputPeer, {
|
|
1613
|
+
message: markdownToTelegramHtml(text),
|
|
1614
|
+
parseMode: "html"
|
|
1615
|
+
}),
|
|
1616
|
+
SEND_MESSAGE_TIMEOUT_MS,
|
|
1617
|
+
`[Bot ${this.botId}] Telegram client.sendMessage()`
|
|
1618
|
+
);
|
|
1619
|
+
if (DEBUG) {
|
|
1620
|
+
console.log(`[Bot ${this.botId}] [telegram] Caption message sent successfully to ${peer}`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
function opencontextMessageToTgText(message) {
|
|
1626
|
+
if (typeof message === "string") {
|
|
1627
|
+
return message;
|
|
1628
|
+
}
|
|
1629
|
+
if ("text" in message) {
|
|
1630
|
+
return message.text;
|
|
1631
|
+
}
|
|
1632
|
+
if ("target" in message) {
|
|
1633
|
+
return `@${message.target}`;
|
|
1634
|
+
}
|
|
1635
|
+
if ("nodes" in message) {
|
|
1636
|
+
return message.nodes.map((node) => opencontextMessageToTgText(node)).join("");
|
|
1637
|
+
}
|
|
1638
|
+
return "";
|
|
1639
|
+
}
|
|
1640
|
+
function describeUnsupportedMessage(message) {
|
|
1641
|
+
if (!message || typeof message !== "object") return typeof message;
|
|
1642
|
+
if ("name" in message && "url" in message) return "file";
|
|
1643
|
+
if ("length" in message && "url" in message) return "voice";
|
|
1644
|
+
if ("origin" in message) return "quote";
|
|
1645
|
+
if ("time" in message && "id" in message) return "source";
|
|
1646
|
+
if ("display" in message && "nodes" in message) return "forward";
|
|
1647
|
+
if ("type" in message && "name" in message) return "emoji";
|
|
1648
|
+
return "content";
|
|
1649
|
+
}
|
|
1650
|
+
function tgMessageToopencontextMessage(message) {
|
|
1651
|
+
const messages = [];
|
|
1652
|
+
if (!message.message) {
|
|
1653
|
+
if (message.media) {
|
|
1654
|
+
messages.push("[Media content]");
|
|
1655
|
+
}
|
|
1656
|
+
return messages;
|
|
1657
|
+
}
|
|
1658
|
+
messages.push(message.message);
|
|
1659
|
+
if (message.entities) {
|
|
1660
|
+
for (const entity of message.entities) {
|
|
1661
|
+
if (entity instanceof Api.MessageEntityMention) {
|
|
1662
|
+
const mentionText = message.message.substr(entity.offset, entity.length);
|
|
1663
|
+
messages.push({ target: mentionText.replace("@", "") });
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return messages;
|
|
1668
|
+
}
|
|
1669
|
+
function getAppMemoryDir(userId) {
|
|
1670
|
+
const base = join(homedir(), ".opencontext", "data", "memory");
|
|
1671
|
+
return userId ? join(base, userId) : base;
|
|
1672
|
+
}
|
|
1673
|
+
var TelegramConversationStore = class {
|
|
1674
|
+
cache = /* @__PURE__ */ new Map();
|
|
1675
|
+
loadedPairs = /* @__PURE__ */ new Set();
|
|
1676
|
+
DEFAULT_MAX_MESSAGES = 50;
|
|
1677
|
+
EXPIRY_HOURS = 24;
|
|
1678
|
+
PREFIX = "telegram";
|
|
1679
|
+
memoryDir;
|
|
1680
|
+
cleanupInterval = null;
|
|
1681
|
+
constructor(userId, memoryDir) {
|
|
1682
|
+
this.memoryDir = memoryDir ?? getAppMemoryDir(userId);
|
|
1683
|
+
this.startCleanupInterval();
|
|
1684
|
+
}
|
|
1685
|
+
pairKey(userId, accountId) {
|
|
1686
|
+
return `${userId}::${accountId}`;
|
|
1687
|
+
}
|
|
1688
|
+
ensureLoaded(userId, accountId) {
|
|
1689
|
+
const pk = this.pairKey(userId, accountId);
|
|
1690
|
+
if (this.loadedPairs.has(pk)) return;
|
|
1691
|
+
const key = this.buildKey(userId, accountId);
|
|
1692
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1693
|
+
const rawMsgs = loadChannelDay(this.memoryDir, this.PREFIX, today, key, "");
|
|
1694
|
+
this.cache.set(key, {
|
|
1695
|
+
userId,
|
|
1696
|
+
accountId,
|
|
1697
|
+
messages: rawMsgs.map(({ timestamp: _t, ...rest }) => rest),
|
|
1698
|
+
maxMessages: this.DEFAULT_MAX_MESSAGES,
|
|
1699
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
1700
|
+
});
|
|
1701
|
+
this.loadedPairs.add(pk);
|
|
1702
|
+
}
|
|
1703
|
+
getConversation(userId, accountId) {
|
|
1704
|
+
const key = this.buildKey(userId, accountId);
|
|
1705
|
+
if (!this.cache.has(key)) {
|
|
1706
|
+
this.cache.set(key, {
|
|
1707
|
+
userId,
|
|
1708
|
+
accountId,
|
|
1709
|
+
messages: [],
|
|
1710
|
+
maxMessages: this.DEFAULT_MAX_MESSAGES,
|
|
1711
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
1712
|
+
});
|
|
1713
|
+
console.log(`[TelegramConversationStore] Created new conversation for ${key}`);
|
|
1714
|
+
}
|
|
1715
|
+
this.ensureLoaded(userId, accountId);
|
|
1716
|
+
return this.cache.get(key);
|
|
1717
|
+
}
|
|
1718
|
+
addMessage(userId, accountId, role, content) {
|
|
1719
|
+
const key = this.buildKey(userId, accountId);
|
|
1720
|
+
const conversation = this.getConversation(userId, accountId);
|
|
1721
|
+
const msg = { role, content };
|
|
1722
|
+
conversation.messages.push(msg);
|
|
1723
|
+
conversation.lastUpdated = /* @__PURE__ */ new Date();
|
|
1724
|
+
saveChannelMessage(this.memoryDir, this.PREFIX, key, "", {
|
|
1725
|
+
...msg,
|
|
1726
|
+
timestamp: Date.now()
|
|
1727
|
+
});
|
|
1728
|
+
console.log(`[TelegramConversationStore] Added ${role} message for ${key}`);
|
|
1729
|
+
}
|
|
1730
|
+
getConversationHistory(userId, accountId) {
|
|
1731
|
+
const conversation = this.getConversation(userId, accountId);
|
|
1732
|
+
return [...conversation.messages];
|
|
1733
|
+
}
|
|
1734
|
+
clearConversation(userId, accountId) {
|
|
1735
|
+
const key = this.buildKey(userId, accountId);
|
|
1736
|
+
const pk = this.pairKey(userId, accountId);
|
|
1737
|
+
this.cache.delete(key);
|
|
1738
|
+
this.loadedPairs.delete(pk);
|
|
1739
|
+
clearChannelConversationFromAllDays(this.memoryDir, this.PREFIX, key, "");
|
|
1740
|
+
console.log(`[TelegramConversationStore] Cleared conversation: ${key}`);
|
|
1741
|
+
}
|
|
1742
|
+
clearUserConversations(userId) {
|
|
1743
|
+
const prefix = `telegram:self-chat:${userId}:`;
|
|
1744
|
+
let clearedCount = 0;
|
|
1745
|
+
for (const key of this.cache.keys()) {
|
|
1746
|
+
if (key.startsWith(prefix)) {
|
|
1747
|
+
this.cache.delete(key);
|
|
1748
|
+
clearedCount++;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const pk of [...this.loadedPairs]) {
|
|
1752
|
+
if (pk.startsWith(`${userId}::`)) {
|
|
1753
|
+
this.loadedPairs.delete(pk);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
clearChannelForUserPrefix(this.memoryDir, this.PREFIX, prefix);
|
|
1757
|
+
console.log(`[TelegramConversationStore] Cleared ${clearedCount} conversation(s) for user ${userId}`);
|
|
1758
|
+
}
|
|
1759
|
+
clearExpiredConversations() {
|
|
1760
|
+
const now = /* @__PURE__ */ new Date();
|
|
1761
|
+
let clearedCount = 0;
|
|
1762
|
+
for (const [key, conversation] of this.cache.entries()) {
|
|
1763
|
+
const hoursSinceLastUpdate = (now.getTime() - conversation.lastUpdated.getTime()) / (1e3 * 60 * 60);
|
|
1764
|
+
if (hoursSinceLastUpdate > this.EXPIRY_HOURS) {
|
|
1765
|
+
this.cache.delete(key);
|
|
1766
|
+
clearedCount++;
|
|
1767
|
+
clearChannelConversationFromAllDays(this.memoryDir, this.PREFIX, key, "");
|
|
1768
|
+
console.log(
|
|
1769
|
+
`[TelegramConversationStore] Cleared expired conversation: ${key} (${hoursSinceLastUpdate.toFixed(1)}h old)`
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (clearedCount > 0) {
|
|
1774
|
+
console.log(`[TelegramConversationStore] Cleanup: ${clearedCount} expired conversation(s) cleared`);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
getStats() {
|
|
1778
|
+
let totalMessages = 0;
|
|
1779
|
+
let oldest = null;
|
|
1780
|
+
let newest = null;
|
|
1781
|
+
for (const conversation of this.cache.values()) {
|
|
1782
|
+
totalMessages += conversation.messages.length;
|
|
1783
|
+
if (!oldest || conversation.lastUpdated < oldest) oldest = conversation.lastUpdated;
|
|
1784
|
+
if (!newest || conversation.lastUpdated > newest) newest = conversation.lastUpdated;
|
|
1785
|
+
}
|
|
1786
|
+
return {
|
|
1787
|
+
totalConversations: this.cache.size,
|
|
1788
|
+
totalMessages,
|
|
1789
|
+
oldestConversation: oldest,
|
|
1790
|
+
newestConversation: newest
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
startCleanupInterval() {
|
|
1794
|
+
if (this.cleanupInterval) return;
|
|
1795
|
+
this.cleanupInterval = setInterval(() => this.clearExpiredConversations(), 60 * 60 * 1e3);
|
|
1796
|
+
}
|
|
1797
|
+
stopCleanupInterval() {
|
|
1798
|
+
if (this.cleanupInterval) {
|
|
1799
|
+
clearInterval(this.cleanupInterval);
|
|
1800
|
+
this.cleanupInterval = null;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
buildKey(userId, accountId) {
|
|
1804
|
+
return `telegram:self-chat:${userId}:${accountId}`;
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
export { CONNECT_TIMEOUT_MS, TelegramAdapter, TelegramConversationStore, markdownToTelegramHtml, opencontextMessageToTgText, tgMessageToopencontextMessage, withTimeout };
|
|
1809
|
+
//# sourceMappingURL=index.js.map
|
|
1810
|
+
//# sourceMappingURL=index.js.map
|