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