cclawd 1.0.2 → 1.0.4

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.
Files changed (68) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/plugin-sdk/active-listener-CN-tMEvN.js +35 -0
  3. package/dist/plugin-sdk/api-key-rotation-CimGYMBc.js +176 -0
  4. package/dist/plugin-sdk/audio-preflight-C-xXBoE2.js +51 -0
  5. package/dist/plugin-sdk/audio-transcription-runner-CTIHpebA.js +2173 -0
  6. package/dist/plugin-sdk/audit-membership-runtime-BFatB2LJ.js +58 -0
  7. package/dist/plugin-sdk/channel-activity-DO0FEzyj.js +95 -0
  8. package/dist/plugin-sdk/channel-web-Da-__nUF.js +2238 -0
  9. package/dist/plugin-sdk/commands-registry-6no2NNrY.js +1118 -0
  10. package/dist/plugin-sdk/compact.runtime-CCoclu5e.js +35 -0
  11. package/dist/plugin-sdk/config-B9ODwgpz.js +37426 -0
  12. package/dist/plugin-sdk/deliver-B1fFpKjV.js +1757 -0
  13. package/dist/plugin-sdk/deliver-runtime-DB-VRMe1.js +15 -0
  14. package/dist/plugin-sdk/deps-send-discord.runtime-DklqycYG.js +15 -0
  15. package/dist/plugin-sdk/deps-send-imessage.runtime-Chs8zeon.js +14 -0
  16. package/dist/plugin-sdk/deps-send-signal.runtime-clW9aSJP.js +13 -0
  17. package/dist/plugin-sdk/deps-send-slack.runtime-BUx0LYY1.js +13 -0
  18. package/dist/plugin-sdk/deps-send-telegram.runtime-LECSHgMG.js +16 -0
  19. package/dist/plugin-sdk/deps-send-whatsapp.runtime-D2d65fw0.js +40 -0
  20. package/dist/plugin-sdk/diagnostic-CxIvS-C2.js +315 -0
  21. package/dist/plugin-sdk/dispatch-BqlR4dPx.js +105863 -0
  22. package/dist/plugin-sdk/env-b9k1PHMI.js +34 -0
  23. package/dist/plugin-sdk/fetch-PoxzAANT.js +326 -0
  24. package/dist/plugin-sdk/fetch-guard-4UVSZ0uS.js +164 -0
  25. package/dist/plugin-sdk/image-Ch6M4tnJ.js +2420 -0
  26. package/dist/plugin-sdk/image-runtime-CSh2o5wY.js +8 -0
  27. package/dist/plugin-sdk/index.js +35 -35
  28. package/dist/plugin-sdk/ir-CugsqGH8.js +1312 -0
  29. package/dist/plugin-sdk/local-roots-adnEg9zb.js +217 -0
  30. package/dist/plugin-sdk/logger-D6zRubj0.js +1164 -0
  31. package/dist/plugin-sdk/login-CYvkQ0At.js +54 -0
  32. package/dist/plugin-sdk/login-qr-ll4NtaT5.js +316 -0
  33. package/dist/plugin-sdk/manager-CHy8IclH.js +3959 -0
  34. package/dist/plugin-sdk/manager-runtime-C70EkEr7.js +11 -0
  35. package/dist/plugin-sdk/outbound-Wzs2iN7X.js +216 -0
  36. package/dist/plugin-sdk/outbound-attachment-khXJwucX.js +17 -0
  37. package/dist/plugin-sdk/paths-BtVqCdw4.js +3063 -0
  38. package/dist/plugin-sdk/pi-model-discovery-Dh4ziodY.js +131 -0
  39. package/dist/plugin-sdk/pi-model-discovery-runtime-b83Xe-HT.js +8 -0
  40. package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-C1z5CDBF.js +349 -0
  41. package/dist/plugin-sdk/proxy-fetch-CJEmoBxi.js +54 -0
  42. package/dist/plugin-sdk/pw-ai-Dj3Cvlzl.js +1990 -0
  43. package/dist/plugin-sdk/qmd-manager-egHUAseQ.js +1581 -0
  44. package/dist/plugin-sdk/resolve-outbound-target-BiICvIKs.js +38 -0
  45. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DNApufzW.js +9 -0
  46. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-CBmtfIQ8.js +13 -0
  47. package/dist/plugin-sdk/send-CScblaI4.js +532 -0
  48. package/dist/plugin-sdk/send-CeHhnld6.js +407 -0
  49. package/dist/plugin-sdk/send-DP_c8JfR.js +3277 -0
  50. package/dist/plugin-sdk/send-Dc5fI6e8.js +495 -0
  51. package/dist/plugin-sdk/send-l-77_s1_.js +2507 -0
  52. package/dist/plugin-sdk/session-CkOKZaqa.js +166 -0
  53. package/dist/plugin-sdk/signal.js +2 -2
  54. package/dist/plugin-sdk/skill-commands-BohYCgkq.js +336 -0
  55. package/dist/plugin-sdk/slash-commands.runtime-DpLfVTM6.js +8 -0
  56. package/dist/plugin-sdk/slash-dispatch.runtime-CASMHwpm.js +35 -0
  57. package/dist/plugin-sdk/slash-skill-commands.runtime-D7rrJEci.js +9 -0
  58. package/dist/plugin-sdk/sqlite-CJE3X7Mv.js +1005 -0
  59. package/dist/plugin-sdk/subagent-registry-runtime-B1oo5bih.js +35 -0
  60. package/dist/plugin-sdk/tables-D5VgpTmm.js +53 -0
  61. package/dist/plugin-sdk/target-errors-C6zZ_OpA.js +191 -0
  62. package/dist/plugin-sdk/tokens-DUnJnpMS.js +50 -0
  63. package/dist/plugin-sdk/web-TfUM1nSi.js +39 -0
  64. package/dist/plugin-sdk/whatsapp-actions-DuWJ0j1r.js +71 -0
  65. package/extensions/mfa-auth/index.ts +36 -17
  66. package/extensions/mfa-auth/src/auth-manager.ts +4 -0
  67. package/extensions/mfa-auth/src/notification-service.ts +5 -1
  68. package/package.json +1 -1
@@ -0,0 +1,2507 @@
1
+ import { E as redactSensitiveText, It as normalizeAccountId } from "./paths-BtVqCdw4.js";
2
+ import { As as isGifMedia, Bs as collectErrorGraphCandidates, Ds as getFileExtension, Hl as withFileLock$1, Hs as formatErrorMessage, Ms as normalizeMimeType, Us as formatUncaughtError, Vs as extractErrorCode, Ws as readErrorName, Zo as writeJsonAtomic, _i as listChannelPlugins, a as readConfigFileSnapshotForWrite, c as writeConfigFile, gi as getChannelPlugin, js as kindFromMime, jt as resolveTelegramPreviewStreamMode, r as loadConfig, sa as resolveTelegramAccount } from "./config-B9ODwgpz.js";
3
+ import { c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, o as resolveOAuthDir } from "./paths-eFexkPEh.js";
4
+ import { I as danger, O as safeParseJson, R as logVerbose, a as createSubsystemLogger, p as CONFIG_DIR } from "./logger-D6zRubj0.js";
5
+ import { i as createTelegramRetryRunner, n as recordChannelActivity } from "./channel-activity-DO0FEzyj.js";
6
+ import { t as buildOutboundMediaLoadOptions } from "./load-options-CE0_U2E0.js";
7
+ import { n as normalizePollInput } from "./polls-CL1LDouN.js";
8
+ import { i as resolveMarkdownTableMode, n as markdownToIR, t as chunkMarkdownIR, v as loadWebMedia } from "./ir-CugsqGH8.js";
9
+ import { t as renderMarkdownWithMarkers } from "./render-CKf6NJ1M.js";
10
+ import { n as makeProxyFetch } from "./proxy-fetch-CJEmoBxi.js";
11
+ import { n as normalizeTelegramLookupTarget, r as parseTelegramTarget, t as normalizeTelegramChatId } from "./targets-s9KeyATC.js";
12
+ import { t as resolveTelegramFetch } from "./fetch-PoxzAANT.js";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+ import crypto, { randomBytes } from "node:crypto";
17
+ import json5 from "json5";
18
+ import { Bot, HttpError, InputFile } from "grammy";
19
+ //#region src/channels/allow-from.ts
20
+ function mergeDmAllowFromSources(params) {
21
+ const storeEntries = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom ?? [];
22
+ return [...params.allowFrom ?? [], ...storeEntries].map((value) => String(value).trim()).filter(Boolean);
23
+ }
24
+ function resolveGroupAllowFromSources(params) {
25
+ const explicitGroupAllowFrom = Array.isArray(params.groupAllowFrom) && params.groupAllowFrom.length > 0 ? params.groupAllowFrom : void 0;
26
+ return (explicitGroupAllowFrom ? explicitGroupAllowFrom : params.fallbackToAllowFrom === false ? [] : params.allowFrom ?? []).map((value) => String(value).trim()).filter(Boolean);
27
+ }
28
+ function firstDefined(...values) {
29
+ for (const value of values) if (typeof value !== "undefined") return value;
30
+ }
31
+ function isSenderIdAllowed(allow, senderId, allowWhenEmpty) {
32
+ if (!allow.hasEntries) return allowWhenEmpty;
33
+ if (allow.hasWildcard) return true;
34
+ if (!senderId) return false;
35
+ return allow.entries.includes(senderId);
36
+ }
37
+ //#endregion
38
+ //#region src/channels/plugins/pairing.ts
39
+ function listPairingChannels() {
40
+ return listChannelPlugins().filter((plugin) => plugin.pairing).map((plugin) => plugin.id);
41
+ }
42
+ function getPairingAdapter(channelId) {
43
+ return getChannelPlugin(channelId)?.pairing ?? null;
44
+ }
45
+ //#endregion
46
+ //#region src/plugin-sdk/json-store.ts
47
+ async function readJsonFileWithFallback(filePath, fallback) {
48
+ try {
49
+ const parsed = safeParseJson(await fs.promises.readFile(filePath, "utf-8"));
50
+ if (parsed == null) return {
51
+ value: fallback,
52
+ exists: true
53
+ };
54
+ return {
55
+ value: parsed,
56
+ exists: true
57
+ };
58
+ } catch (err) {
59
+ if (err.code === "ENOENT") return {
60
+ value: fallback,
61
+ exists: false
62
+ };
63
+ return {
64
+ value: fallback,
65
+ exists: false
66
+ };
67
+ }
68
+ }
69
+ async function writeJsonFileAtomically(filePath, value) {
70
+ await writeJsonAtomic(filePath, value, {
71
+ mode: 384,
72
+ trailingNewline: true,
73
+ ensureDirMode: 448
74
+ });
75
+ }
76
+ //#endregion
77
+ //#region src/pairing/pairing-store.ts
78
+ const PAIRING_CODE_LENGTH = 8;
79
+ const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
80
+ const PAIRING_PENDING_TTL_MS = 3600 * 1e3;
81
+ const PAIRING_PENDING_MAX = 3;
82
+ const PAIRING_STORE_LOCK_OPTIONS = {
83
+ retries: {
84
+ retries: 10,
85
+ factor: 2,
86
+ minTimeout: 100,
87
+ maxTimeout: 1e4,
88
+ randomize: true
89
+ },
90
+ stale: 3e4
91
+ };
92
+ const allowFromReadCache = /* @__PURE__ */ new Map();
93
+ function resolveCredentialsDir(env = process.env) {
94
+ return resolveOAuthDir(env, resolveStateDir(env, () => resolveRequiredHomeDir(env, os.homedir)));
95
+ }
96
+ /** Sanitize channel ID for use in filenames (prevent path traversal). */
97
+ function safeChannelKey(channel) {
98
+ const raw = String(channel).trim().toLowerCase();
99
+ if (!raw) throw new Error("invalid pairing channel");
100
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
101
+ if (!safe || safe === "_") throw new Error("invalid pairing channel");
102
+ return safe;
103
+ }
104
+ function resolvePairingPath(channel, env = process.env) {
105
+ return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-pairing.json`);
106
+ }
107
+ function safeAccountKey(accountId) {
108
+ const raw = String(accountId).trim().toLowerCase();
109
+ if (!raw) throw new Error("invalid pairing account id");
110
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
111
+ if (!safe || safe === "_") throw new Error("invalid pairing account id");
112
+ return safe;
113
+ }
114
+ function resolveAllowFromPath(channel, env = process.env, accountId) {
115
+ const base = safeChannelKey(channel);
116
+ const normalizedAccountId = typeof accountId === "string" ? accountId.trim() : "";
117
+ if (!normalizedAccountId) return path.join(resolveCredentialsDir(env), `${base}-allowFrom.json`);
118
+ return path.join(resolveCredentialsDir(env), `${base}-${safeAccountKey(normalizedAccountId)}-allowFrom.json`);
119
+ }
120
+ async function readJsonFile(filePath, fallback) {
121
+ return await readJsonFileWithFallback(filePath, fallback);
122
+ }
123
+ async function writeJsonFile(filePath, value) {
124
+ await writeJsonFileAtomically(filePath, value);
125
+ }
126
+ async function readPairingRequests(filePath) {
127
+ const { value } = await readJsonFile(filePath, {
128
+ version: 1,
129
+ requests: []
130
+ });
131
+ return Array.isArray(value.requests) ? value.requests : [];
132
+ }
133
+ async function ensureJsonFile(filePath, fallback) {
134
+ try {
135
+ await fs.promises.access(filePath);
136
+ } catch {
137
+ await writeJsonFile(filePath, fallback);
138
+ }
139
+ }
140
+ async function withFileLock(filePath, fallback, fn) {
141
+ await ensureJsonFile(filePath, fallback);
142
+ return await withFileLock$1(filePath, PAIRING_STORE_LOCK_OPTIONS, async () => {
143
+ return await fn();
144
+ });
145
+ }
146
+ function parseTimestamp(value) {
147
+ if (!value) return null;
148
+ const parsed = Date.parse(value);
149
+ if (!Number.isFinite(parsed)) return null;
150
+ return parsed;
151
+ }
152
+ function isExpired(entry, nowMs) {
153
+ const createdAt = parseTimestamp(entry.createdAt);
154
+ if (!createdAt) return true;
155
+ return nowMs - createdAt > PAIRING_PENDING_TTL_MS;
156
+ }
157
+ function pruneExpiredRequests(reqs, nowMs) {
158
+ const kept = [];
159
+ let removed = false;
160
+ for (const req of reqs) {
161
+ if (isExpired(req, nowMs)) {
162
+ removed = true;
163
+ continue;
164
+ }
165
+ kept.push(req);
166
+ }
167
+ return {
168
+ requests: kept,
169
+ removed
170
+ };
171
+ }
172
+ function resolveLastSeenAt(entry) {
173
+ return parseTimestamp(entry.lastSeenAt) ?? parseTimestamp(entry.createdAt) ?? 0;
174
+ }
175
+ function pruneExcessRequests(reqs, maxPending) {
176
+ if (maxPending <= 0 || reqs.length <= maxPending) return {
177
+ requests: reqs,
178
+ removed: false
179
+ };
180
+ return {
181
+ requests: reqs.slice().toSorted((a, b) => resolveLastSeenAt(a) - resolveLastSeenAt(b)).slice(-maxPending),
182
+ removed: true
183
+ };
184
+ }
185
+ function randomCode() {
186
+ let out = "";
187
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
188
+ const idx = crypto.randomInt(0, 32);
189
+ out += PAIRING_CODE_ALPHABET[idx];
190
+ }
191
+ return out;
192
+ }
193
+ function generateUniqueCode(existing) {
194
+ for (let attempt = 0; attempt < 500; attempt += 1) {
195
+ const code = randomCode();
196
+ if (!existing.has(code)) return code;
197
+ }
198
+ throw new Error("failed to generate unique pairing code");
199
+ }
200
+ function normalizePairingAccountId(accountId) {
201
+ return accountId?.trim().toLowerCase() || "";
202
+ }
203
+ function requestMatchesAccountId(entry, normalizedAccountId) {
204
+ if (!normalizedAccountId) return true;
205
+ return String(entry.meta?.accountId ?? "").trim().toLowerCase() === normalizedAccountId;
206
+ }
207
+ function shouldIncludeLegacyAllowFromEntries(normalizedAccountId) {
208
+ return !normalizedAccountId || normalizedAccountId === "default";
209
+ }
210
+ function resolveAllowFromAccountId(accountId) {
211
+ return normalizePairingAccountId(accountId) || "default";
212
+ }
213
+ function normalizeId(value) {
214
+ return String(value).trim();
215
+ }
216
+ function normalizeAllowEntry(channel, entry) {
217
+ const trimmed = entry.trim();
218
+ if (!trimmed) return "";
219
+ if (trimmed === "*") return "";
220
+ const adapter = getPairingAdapter(channel);
221
+ const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed;
222
+ return String(normalized).trim();
223
+ }
224
+ function normalizeAllowFromList(channel, store) {
225
+ return dedupePreserveOrder((Array.isArray(store.allowFrom) ? store.allowFrom : []).map((v) => normalizeAllowEntry(channel, String(v))).filter(Boolean));
226
+ }
227
+ function normalizeAllowFromInput(channel, entry) {
228
+ return normalizeAllowEntry(channel, normalizeId(entry));
229
+ }
230
+ function dedupePreserveOrder(entries) {
231
+ const seen = /* @__PURE__ */ new Set();
232
+ const out = [];
233
+ for (const entry of entries) {
234
+ const normalized = String(entry).trim();
235
+ if (!normalized || seen.has(normalized)) continue;
236
+ seen.add(normalized);
237
+ out.push(normalized);
238
+ }
239
+ return out;
240
+ }
241
+ async function readAllowFromStateForPath(channel, filePath) {
242
+ return (await readAllowFromStateForPathWithExists(channel, filePath)).entries;
243
+ }
244
+ function cloneAllowFromCacheEntry(entry) {
245
+ return {
246
+ exists: entry.exists,
247
+ mtimeMs: entry.mtimeMs,
248
+ size: entry.size,
249
+ entries: entry.entries.slice()
250
+ };
251
+ }
252
+ function setAllowFromReadCache(filePath, entry) {
253
+ allowFromReadCache.set(filePath, cloneAllowFromCacheEntry(entry));
254
+ }
255
+ function resolveAllowFromReadCacheHit(params) {
256
+ const cached = allowFromReadCache.get(params.filePath);
257
+ if (!cached) return null;
258
+ if (cached.exists !== params.exists) return null;
259
+ if (!params.exists) return cloneAllowFromCacheEntry(cached);
260
+ if (cached.mtimeMs !== params.mtimeMs || cached.size !== params.size) return null;
261
+ return cloneAllowFromCacheEntry(cached);
262
+ }
263
+ function resolveAllowFromReadCacheOrMissing(filePath, stat) {
264
+ const cached = resolveAllowFromReadCacheHit({
265
+ filePath,
266
+ exists: Boolean(stat),
267
+ mtimeMs: stat?.mtimeMs ?? null,
268
+ size: stat?.size ?? null
269
+ });
270
+ if (cached) return {
271
+ entries: cached.entries,
272
+ exists: cached.exists
273
+ };
274
+ if (!stat) {
275
+ setAllowFromReadCache(filePath, {
276
+ exists: false,
277
+ mtimeMs: null,
278
+ size: null,
279
+ entries: []
280
+ });
281
+ return {
282
+ entries: [],
283
+ exists: false
284
+ };
285
+ }
286
+ return null;
287
+ }
288
+ async function readAllowFromStateForPathWithExists(channel, filePath) {
289
+ let stat = null;
290
+ try {
291
+ stat = await fs.promises.stat(filePath);
292
+ } catch (err) {
293
+ if (err.code !== "ENOENT") throw err;
294
+ }
295
+ const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
296
+ if (cachedOrMissing) return cachedOrMissing;
297
+ if (!stat) return {
298
+ entries: [],
299
+ exists: false
300
+ };
301
+ const { value, exists } = await readJsonFile(filePath, {
302
+ version: 1,
303
+ allowFrom: []
304
+ });
305
+ const entries = normalizeAllowFromList(channel, value);
306
+ setAllowFromReadCache(filePath, {
307
+ exists,
308
+ mtimeMs: stat.mtimeMs,
309
+ size: stat.size,
310
+ entries
311
+ });
312
+ return {
313
+ entries,
314
+ exists
315
+ };
316
+ }
317
+ function readAllowFromStateForPathSync(channel, filePath) {
318
+ return readAllowFromStateForPathSyncWithExists(channel, filePath).entries;
319
+ }
320
+ function readAllowFromStateForPathSyncWithExists(channel, filePath) {
321
+ let stat = null;
322
+ try {
323
+ stat = fs.statSync(filePath);
324
+ } catch (err) {
325
+ if (err.code !== "ENOENT") return {
326
+ entries: [],
327
+ exists: false
328
+ };
329
+ }
330
+ const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
331
+ if (cachedOrMissing) return cachedOrMissing;
332
+ if (!stat) return {
333
+ entries: [],
334
+ exists: false
335
+ };
336
+ let raw = "";
337
+ try {
338
+ raw = fs.readFileSync(filePath, "utf8");
339
+ } catch (err) {
340
+ if (err.code === "ENOENT") return {
341
+ entries: [],
342
+ exists: false
343
+ };
344
+ return {
345
+ entries: [],
346
+ exists: false
347
+ };
348
+ }
349
+ try {
350
+ const entries = normalizeAllowFromList(channel, JSON.parse(raw));
351
+ setAllowFromReadCache(filePath, {
352
+ exists: true,
353
+ mtimeMs: stat.mtimeMs,
354
+ size: stat.size,
355
+ entries
356
+ });
357
+ return {
358
+ entries,
359
+ exists: true
360
+ };
361
+ } catch {
362
+ setAllowFromReadCache(filePath, {
363
+ exists: true,
364
+ mtimeMs: stat.mtimeMs,
365
+ size: stat.size,
366
+ entries: []
367
+ });
368
+ return {
369
+ entries: [],
370
+ exists: true
371
+ };
372
+ }
373
+ }
374
+ async function readAllowFromState(params) {
375
+ const { value } = await readJsonFile(params.filePath, {
376
+ version: 1,
377
+ allowFrom: []
378
+ });
379
+ return {
380
+ current: normalizeAllowFromList(params.channel, value),
381
+ normalized: normalizeAllowFromInput(params.channel, params.entry) || null
382
+ };
383
+ }
384
+ async function writeAllowFromState(filePath, allowFrom) {
385
+ await writeJsonFile(filePath, {
386
+ version: 1,
387
+ allowFrom
388
+ });
389
+ let stat = null;
390
+ try {
391
+ stat = await fs.promises.stat(filePath);
392
+ } catch {}
393
+ setAllowFromReadCache(filePath, {
394
+ exists: true,
395
+ mtimeMs: stat?.mtimeMs ?? null,
396
+ size: stat?.size ?? null,
397
+ entries: allowFrom.slice()
398
+ });
399
+ }
400
+ async function readNonDefaultAccountAllowFrom(params) {
401
+ const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
402
+ return await readAllowFromStateForPath(params.channel, scopedPath);
403
+ }
404
+ function readNonDefaultAccountAllowFromSync(params) {
405
+ const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
406
+ return readAllowFromStateForPathSync(params.channel, scopedPath);
407
+ }
408
+ async function updateAllowFromStoreEntry(params) {
409
+ const env = params.env ?? process.env;
410
+ const filePath = resolveAllowFromPath(params.channel, env, params.accountId);
411
+ return await withFileLock(filePath, {
412
+ version: 1,
413
+ allowFrom: []
414
+ }, async () => {
415
+ const { current, normalized } = await readAllowFromState({
416
+ channel: params.channel,
417
+ entry: params.entry,
418
+ filePath
419
+ });
420
+ if (!normalized) return {
421
+ changed: false,
422
+ allowFrom: current
423
+ };
424
+ const next = params.apply(current, normalized);
425
+ if (!next) return {
426
+ changed: false,
427
+ allowFrom: current
428
+ };
429
+ await writeAllowFromState(filePath, next);
430
+ return {
431
+ changed: true,
432
+ allowFrom: next
433
+ };
434
+ });
435
+ }
436
+ async function readChannelAllowFromStore(channel, env = process.env, accountId) {
437
+ const resolvedAccountId = resolveAllowFromAccountId(accountId);
438
+ if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return await readNonDefaultAccountAllowFrom({
439
+ channel,
440
+ env,
441
+ accountId: resolvedAccountId
442
+ });
443
+ const scopedEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
444
+ const legacyEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env));
445
+ return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
446
+ }
447
+ function readChannelAllowFromStoreSync(channel, env = process.env, accountId) {
448
+ const resolvedAccountId = resolveAllowFromAccountId(accountId);
449
+ if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return readNonDefaultAccountAllowFromSync({
450
+ channel,
451
+ env,
452
+ accountId: resolvedAccountId
453
+ });
454
+ const scopedEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
455
+ const legacyEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env));
456
+ return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
457
+ }
458
+ async function updateChannelAllowFromStore(params) {
459
+ return await updateAllowFromStoreEntry({
460
+ channel: params.channel,
461
+ entry: params.entry,
462
+ accountId: params.accountId,
463
+ env: params.env,
464
+ apply: params.apply
465
+ });
466
+ }
467
+ async function mutateChannelAllowFromStoreEntry(params, apply) {
468
+ return await updateChannelAllowFromStore({
469
+ ...params,
470
+ apply
471
+ });
472
+ }
473
+ async function addChannelAllowFromStoreEntry(params) {
474
+ return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
475
+ if (current.includes(normalized)) return null;
476
+ return [...current, normalized];
477
+ });
478
+ }
479
+ async function removeChannelAllowFromStoreEntry(params) {
480
+ return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
481
+ const next = current.filter((entry) => entry !== normalized);
482
+ if (next.length === current.length) return null;
483
+ return next;
484
+ });
485
+ }
486
+ async function upsertChannelPairingRequest(params) {
487
+ const env = params.env ?? process.env;
488
+ const filePath = resolvePairingPath(params.channel, env);
489
+ return await withFileLock(filePath, {
490
+ version: 1,
491
+ requests: []
492
+ }, async () => {
493
+ const now = (/* @__PURE__ */ new Date()).toISOString();
494
+ const nowMs = Date.now();
495
+ const id = normalizeId(params.id);
496
+ const normalizedAccountId = normalizePairingAccountId(params.accountId) || "default";
497
+ const meta = {
498
+ ...params.meta && typeof params.meta === "object" ? Object.fromEntries(Object.entries(params.meta).map(([k, v]) => [k, String(v ?? "").trim()]).filter(([_, v]) => Boolean(v))) : void 0,
499
+ accountId: normalizedAccountId
500
+ };
501
+ let reqs = await readPairingRequests(filePath);
502
+ const { requests: prunedExpired, removed: expiredRemoved } = pruneExpiredRequests(reqs, nowMs);
503
+ reqs = prunedExpired;
504
+ const normalizedMatchingAccountId = normalizedAccountId;
505
+ const existingIdx = reqs.findIndex((r) => {
506
+ if (r.id !== id) return false;
507
+ return requestMatchesAccountId(r, normalizedMatchingAccountId);
508
+ });
509
+ const existingCodes = new Set(reqs.map((req) => String(req.code ?? "").trim().toUpperCase()));
510
+ if (existingIdx >= 0) {
511
+ const existing = reqs[existingIdx];
512
+ const code = (existing && typeof existing.code === "string" ? existing.code.trim() : "") || generateUniqueCode(existingCodes);
513
+ const next = {
514
+ id,
515
+ code,
516
+ createdAt: existing?.createdAt ?? now,
517
+ lastSeenAt: now,
518
+ meta: meta ?? existing?.meta
519
+ };
520
+ reqs[existingIdx] = next;
521
+ const { requests: capped } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
522
+ await writeJsonFile(filePath, {
523
+ version: 1,
524
+ requests: capped
525
+ });
526
+ return {
527
+ code,
528
+ created: false
529
+ };
530
+ }
531
+ const { requests: capped, removed: cappedRemoved } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
532
+ reqs = capped;
533
+ if (PAIRING_PENDING_MAX > 0 && reqs.length >= PAIRING_PENDING_MAX) {
534
+ if (expiredRemoved || cappedRemoved) await writeJsonFile(filePath, {
535
+ version: 1,
536
+ requests: reqs
537
+ });
538
+ return {
539
+ code: "",
540
+ created: false
541
+ };
542
+ }
543
+ const code = generateUniqueCode(existingCodes);
544
+ const next = {
545
+ id,
546
+ code,
547
+ createdAt: now,
548
+ lastSeenAt: now,
549
+ ...meta ? { meta } : {}
550
+ };
551
+ await writeJsonFile(filePath, {
552
+ version: 1,
553
+ requests: [...reqs, next]
554
+ });
555
+ return {
556
+ code,
557
+ created: true
558
+ };
559
+ });
560
+ }
561
+ //#endregion
562
+ //#region src/media/audio.ts
563
+ const TELEGRAM_VOICE_AUDIO_EXTENSIONS = new Set([
564
+ ".oga",
565
+ ".ogg",
566
+ ".opus",
567
+ ".mp3",
568
+ ".m4a"
569
+ ]);
570
+ /**
571
+ * MIME types compatible with voice messages.
572
+ * Telegram sendVoice supports OGG/Opus, MP3, and M4A.
573
+ * https://core.telegram.org/bots/api#sendvoice
574
+ */
575
+ const TELEGRAM_VOICE_MIME_TYPES = new Set([
576
+ "audio/ogg",
577
+ "audio/opus",
578
+ "audio/mpeg",
579
+ "audio/mp3",
580
+ "audio/mp4",
581
+ "audio/x-m4a",
582
+ "audio/m4a"
583
+ ]);
584
+ function isTelegramVoiceCompatibleAudio(opts) {
585
+ const mime = normalizeMimeType(opts.contentType);
586
+ if (mime && TELEGRAM_VOICE_MIME_TYPES.has(mime)) return true;
587
+ const fileName = opts.fileName?.trim();
588
+ if (!fileName) return false;
589
+ const ext = getFileExtension(fileName);
590
+ if (!ext) return false;
591
+ return TELEGRAM_VOICE_AUDIO_EXTENSIONS.has(ext);
592
+ }
593
+ /**
594
+ * Backward-compatible alias used across plugin/runtime call sites.
595
+ * Keeps existing behavior while making Telegram-specific policy explicit.
596
+ */
597
+ function isVoiceCompatibleAudio(opts) {
598
+ return isTelegramVoiceCompatibleAudio(opts);
599
+ }
600
+ //#endregion
601
+ //#region src/channels/location.ts
602
+ function resolveLocation(location) {
603
+ const source = location.source ?? (location.isLive ? "live" : location.name || location.address ? "place" : "pin");
604
+ const isLive = Boolean(location.isLive ?? source === "live");
605
+ return {
606
+ ...location,
607
+ source,
608
+ isLive
609
+ };
610
+ }
611
+ function formatAccuracy(accuracy) {
612
+ if (!Number.isFinite(accuracy)) return "";
613
+ return ` ±${Math.round(accuracy ?? 0)}m`;
614
+ }
615
+ function formatCoords(latitude, longitude) {
616
+ return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
617
+ }
618
+ function formatLocationText(location) {
619
+ const resolved = resolveLocation(location);
620
+ const coords = formatCoords(resolved.latitude, resolved.longitude);
621
+ const accuracy = formatAccuracy(resolved.accuracy);
622
+ const caption = resolved.caption?.trim();
623
+ let header = "";
624
+ if (resolved.source === "live" || resolved.isLive) header = `🛰 Live location: ${coords}${accuracy}`;
625
+ else if (resolved.name || resolved.address) header = `📍 ${[resolved.name, resolved.address].filter(Boolean).join(" — ")} (${coords}${accuracy})`;
626
+ else header = `📍 ${coords}${accuracy}`;
627
+ return caption ? `${header}\n${caption}` : header;
628
+ }
629
+ function toLocationContext(location) {
630
+ const resolved = resolveLocation(location);
631
+ return {
632
+ LocationLat: resolved.latitude,
633
+ LocationLon: resolved.longitude,
634
+ LocationAccuracy: resolved.accuracy,
635
+ LocationName: resolved.name,
636
+ LocationAddress: resolved.address,
637
+ LocationSource: resolved.source,
638
+ LocationIsLive: resolved.isLive
639
+ };
640
+ }
641
+ //#endregion
642
+ //#region src/telegram/bot-access.ts
643
+ const warnedInvalidEntries = /* @__PURE__ */ new Set();
644
+ const log = createSubsystemLogger("telegram/bot-access");
645
+ function warnInvalidAllowFromEntries(entries) {
646
+ if (process.env.VITEST || false) return;
647
+ for (const entry of entries) {
648
+ if (warnedInvalidEntries.has(entry)) continue;
649
+ warnedInvalidEntries.add(entry);
650
+ log.warn([
651
+ "Invalid allowFrom entry:",
652
+ JSON.stringify(entry),
653
+ "- allowFrom/groupAllowFrom authorization requires numeric Telegram sender IDs only.",
654
+ "If you had \"@username\" entries, re-run onboarding (it resolves @username to IDs) or replace them manually."
655
+ ].join(" "));
656
+ }
657
+ }
658
+ const normalizeAllowFrom = (list) => {
659
+ const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
660
+ const hasWildcard = entries.includes("*");
661
+ const normalized = entries.filter((value) => value !== "*").map((value) => value.replace(/^(telegram|tg):/i, ""));
662
+ const invalidEntries = normalized.filter((value) => !/^\d+$/.test(value));
663
+ if (invalidEntries.length > 0) warnInvalidAllowFromEntries([...new Set(invalidEntries)]);
664
+ return {
665
+ entries: normalized.filter((value) => /^\d+$/.test(value)),
666
+ hasWildcard,
667
+ hasEntries: entries.length > 0,
668
+ invalidEntries
669
+ };
670
+ };
671
+ const normalizeDmAllowFromWithStore = (params) => normalizeAllowFrom(mergeDmAllowFromSources(params));
672
+ const isSenderAllowed = (params) => {
673
+ const { allow, senderId } = params;
674
+ return isSenderIdAllowed(allow, senderId, true);
675
+ };
676
+ const resolveSenderAllowMatch = (params) => {
677
+ const { allow, senderId } = params;
678
+ if (allow.hasWildcard) return {
679
+ allowed: true,
680
+ matchKey: "*",
681
+ matchSource: "wildcard"
682
+ };
683
+ if (!allow.hasEntries) return { allowed: false };
684
+ if (senderId && allow.entries.includes(senderId)) return {
685
+ allowed: true,
686
+ matchKey: senderId,
687
+ matchSource: "id"
688
+ };
689
+ return { allowed: false };
690
+ };
691
+ //#endregion
692
+ //#region src/telegram/bot/helpers.ts
693
+ const TELEGRAM_GENERAL_TOPIC_ID = 1;
694
+ async function resolveTelegramGroupAllowFromContext(params) {
695
+ const accountId = normalizeAccountId(params.accountId);
696
+ const threadSpec = resolveTelegramThreadSpec({
697
+ isGroup: params.isGroup ?? false,
698
+ isForum: params.isForum,
699
+ messageThreadId: params.messageThreadId
700
+ });
701
+ const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : void 0;
702
+ const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : void 0;
703
+ const threadIdForConfig = resolvedThreadId ?? dmThreadId;
704
+ const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, accountId).catch(() => []);
705
+ const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig(params.chatId, threadIdForConfig);
706
+ const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
707
+ return {
708
+ resolvedThreadId,
709
+ dmThreadId,
710
+ storeAllowFrom,
711
+ groupConfig,
712
+ topicConfig,
713
+ groupAllowOverride,
714
+ effectiveGroupAllow: normalizeAllowFrom(groupAllowOverride ?? params.groupAllowFrom),
715
+ hasGroupAllowOverride: typeof groupAllowOverride !== "undefined"
716
+ };
717
+ }
718
+ /**
719
+ * Resolve the thread ID for Telegram forum topics.
720
+ * For non-forum groups, returns undefined even if messageThreadId is present
721
+ * (reply threads in regular groups should not create separate sessions).
722
+ * For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
723
+ */
724
+ function resolveTelegramForumThreadId(params) {
725
+ if (!params.isForum) return;
726
+ if (params.messageThreadId == null) return TELEGRAM_GENERAL_TOPIC_ID;
727
+ return params.messageThreadId;
728
+ }
729
+ function resolveTelegramThreadSpec(params) {
730
+ if (params.isGroup) return {
731
+ id: resolveTelegramForumThreadId({
732
+ isForum: params.isForum,
733
+ messageThreadId: params.messageThreadId
734
+ }),
735
+ scope: params.isForum ? "forum" : "none"
736
+ };
737
+ if (params.messageThreadId == null) return { scope: "dm" };
738
+ return {
739
+ id: params.messageThreadId,
740
+ scope: "dm"
741
+ };
742
+ }
743
+ /**
744
+ * Build thread params for Telegram API calls (messages, media).
745
+ *
746
+ * IMPORTANT: Thread IDs behave differently based on chat type:
747
+ * - DMs (private chats): Include message_thread_id when present (DM topics)
748
+ * - Forum topics: Skip thread_id=1 (General topic), include others
749
+ * - Regular groups: Thread IDs are ignored by Telegram
750
+ *
751
+ * General forum topic (id=1) must be treated like a regular supergroup send:
752
+ * Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
753
+ *
754
+ * @param thread - Thread specification with ID and scope
755
+ * @returns API params object or undefined if thread_id should be omitted
756
+ */
757
+ function buildTelegramThreadParams(thread) {
758
+ if (thread?.id == null) return;
759
+ const normalized = Math.trunc(thread.id);
760
+ if (thread.scope === "dm") return normalized > 0 ? { message_thread_id: normalized } : void 0;
761
+ if (normalized === TELEGRAM_GENERAL_TOPIC_ID) return;
762
+ return { message_thread_id: normalized };
763
+ }
764
+ /**
765
+ * Build thread params for typing indicators (sendChatAction).
766
+ * Empirically, General topic (id=1) needs message_thread_id for typing to appear.
767
+ */
768
+ function buildTypingThreadParams(messageThreadId) {
769
+ if (messageThreadId == null) return;
770
+ return { message_thread_id: Math.trunc(messageThreadId) };
771
+ }
772
+ function resolveTelegramStreamMode(telegramCfg) {
773
+ return resolveTelegramPreviewStreamMode(telegramCfg);
774
+ }
775
+ function buildTelegramGroupPeerId(chatId, messageThreadId) {
776
+ return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
777
+ }
778
+ /**
779
+ * Resolve the direct-message peer identifier for Telegram routing/session keys.
780
+ *
781
+ * In some Telegram DM deliveries (for example certain business/chat bridge flows),
782
+ * `chat.id` can differ from the actual sender user id. Prefer sender id when present
783
+ * so per-peer DM scopes isolate users correctly.
784
+ */
785
+ function resolveTelegramDirectPeerId(params) {
786
+ const senderId = params.senderId != null ? String(params.senderId).trim() : "";
787
+ if (senderId) return senderId;
788
+ return String(params.chatId);
789
+ }
790
+ function buildTelegramGroupFrom(chatId, messageThreadId) {
791
+ return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
792
+ }
793
+ /**
794
+ * Build parentPeer for forum topic binding inheritance.
795
+ * When a message comes from a forum topic, the peer ID includes the topic suffix
796
+ * (e.g., `-1001234567890:topic:99`). To allow bindings configured for the base
797
+ * group ID to match, we provide the parent group as `parentPeer` so the routing
798
+ * layer can fall back to it when the exact peer doesn't match.
799
+ */
800
+ function buildTelegramParentPeer(params) {
801
+ if (!params.isGroup || params.resolvedThreadId == null) return;
802
+ return {
803
+ kind: "group",
804
+ id: String(params.chatId)
805
+ };
806
+ }
807
+ function buildSenderName(msg) {
808
+ return [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || msg.from?.username || void 0;
809
+ }
810
+ function resolveTelegramMediaPlaceholder(msg) {
811
+ if (!msg) return;
812
+ if (msg.photo) return "<media:image>";
813
+ if (msg.video || msg.video_note) return "<media:video>";
814
+ if (msg.audio || msg.voice) return "<media:audio>";
815
+ if (msg.document) return "<media:document>";
816
+ if (msg.sticker) return "<media:sticker>";
817
+ }
818
+ function buildSenderLabel(msg, senderId) {
819
+ const name = buildSenderName(msg);
820
+ const username = msg.from?.username ? `@${msg.from.username}` : void 0;
821
+ let label = name;
822
+ if (name && username) label = `${name} (${username})`;
823
+ else if (!name && username) label = username;
824
+ const fallbackId = (senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : void 0) ?? (msg.from?.id != null ? String(msg.from.id) : void 0);
825
+ const idPart = fallbackId ? `id:${fallbackId}` : void 0;
826
+ if (label && idPart) return `${label} ${idPart}`;
827
+ if (label) return label;
828
+ return idPart ?? "id:unknown";
829
+ }
830
+ function buildGroupLabel(msg, chatId, messageThreadId) {
831
+ const title = msg.chat?.title;
832
+ const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
833
+ if (title) return `${title} id:${chatId}${topicSuffix}`;
834
+ return `group:${chatId}${topicSuffix}`;
835
+ }
836
+ function getTelegramTextParts(msg) {
837
+ return {
838
+ text: msg.text ?? msg.caption ?? "",
839
+ entities: msg.entities ?? msg.caption_entities ?? []
840
+ };
841
+ }
842
+ function isTelegramMentionWordChar(char) {
843
+ return char != null && /[a-z0-9_]/i.test(char);
844
+ }
845
+ function hasStandaloneTelegramMention(text, mention) {
846
+ let startIndex = 0;
847
+ while (startIndex < text.length) {
848
+ const idx = text.indexOf(mention, startIndex);
849
+ if (idx === -1) return false;
850
+ const prev = idx > 0 ? text[idx - 1] : void 0;
851
+ const next = text[idx + mention.length];
852
+ if (!isTelegramMentionWordChar(prev) && !isTelegramMentionWordChar(next)) return true;
853
+ startIndex = idx + 1;
854
+ }
855
+ return false;
856
+ }
857
+ function hasBotMention(msg, botUsername) {
858
+ const { text, entities } = getTelegramTextParts(msg);
859
+ const mention = `@${botUsername}`.toLowerCase();
860
+ if (hasStandaloneTelegramMention(text.toLowerCase(), mention)) return true;
861
+ for (const ent of entities) {
862
+ if (ent.type !== "mention") continue;
863
+ if (text.slice(ent.offset, ent.offset + ent.length).toLowerCase() === mention) return true;
864
+ }
865
+ return false;
866
+ }
867
+ function expandTextLinks(text, entities) {
868
+ if (!text || !entities?.length) return text;
869
+ const textLinks = entities.filter((entity) => entity.type === "text_link" && Boolean(entity.url)).toSorted((a, b) => b.offset - a.offset);
870
+ if (textLinks.length === 0) return text;
871
+ let result = text;
872
+ for (const entity of textLinks) {
873
+ const markdown = `[${text.slice(entity.offset, entity.offset + entity.length)}](${entity.url})`;
874
+ result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
875
+ }
876
+ return result;
877
+ }
878
+ function resolveTelegramReplyId(raw) {
879
+ if (!raw) return;
880
+ const parsed = Number(raw);
881
+ if (!Number.isFinite(parsed)) return;
882
+ return parsed;
883
+ }
884
+ function describeReplyTarget(msg) {
885
+ const reply = msg.reply_to_message;
886
+ const externalReply = msg.external_reply;
887
+ const quoteText = msg.quote?.text ?? externalReply?.quote?.text;
888
+ let body = "";
889
+ let kind = "reply";
890
+ if (typeof quoteText === "string") {
891
+ body = quoteText.trim();
892
+ if (body) kind = "quote";
893
+ }
894
+ const replyLike = reply ?? externalReply;
895
+ if (!body && replyLike) {
896
+ body = (replyLike.text ?? replyLike.caption ?? "").trim();
897
+ if (!body) {
898
+ body = resolveTelegramMediaPlaceholder(replyLike) ?? "";
899
+ if (!body) {
900
+ const locationData = extractTelegramLocation(replyLike);
901
+ if (locationData) body = formatLocationText(locationData);
902
+ }
903
+ }
904
+ }
905
+ if (!body) return null;
906
+ const senderLabel = (replyLike ? buildSenderName(replyLike) : void 0) ?? "unknown sender";
907
+ const forwardedFrom = replyLike?.forward_origin ? resolveForwardOrigin(replyLike.forward_origin) ?? void 0 : void 0;
908
+ return {
909
+ id: replyLike?.message_id ? String(replyLike.message_id) : void 0,
910
+ sender: senderLabel,
911
+ body,
912
+ kind,
913
+ forwardedFrom
914
+ };
915
+ }
916
+ function normalizeForwardedUserLabel(user) {
917
+ const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
918
+ const username = user.username?.trim() || void 0;
919
+ const id = String(user.id);
920
+ return {
921
+ display: (name && username ? `${name} (@${username})` : name || (username ? `@${username}` : void 0)) || `user:${id}`,
922
+ name: name || void 0,
923
+ username,
924
+ id
925
+ };
926
+ }
927
+ function normalizeForwardedChatLabel(chat, fallbackKind) {
928
+ const title = chat.title?.trim() || void 0;
929
+ const username = chat.username?.trim() || void 0;
930
+ const id = String(chat.id);
931
+ return {
932
+ display: title || (username ? `@${username}` : void 0) || `${fallbackKind}:${id}`,
933
+ title,
934
+ username,
935
+ id
936
+ };
937
+ }
938
+ function buildForwardedContextFromUser(params) {
939
+ const { display, name, username, id } = normalizeForwardedUserLabel(params.user);
940
+ if (!display) return null;
941
+ return {
942
+ from: display,
943
+ date: params.date,
944
+ fromType: params.type,
945
+ fromId: id,
946
+ fromUsername: username,
947
+ fromTitle: name
948
+ };
949
+ }
950
+ function buildForwardedContextFromHiddenName(params) {
951
+ const trimmed = params.name?.trim();
952
+ if (!trimmed) return null;
953
+ return {
954
+ from: trimmed,
955
+ date: params.date,
956
+ fromType: params.type,
957
+ fromTitle: trimmed
958
+ };
959
+ }
960
+ function buildForwardedContextFromChat(params) {
961
+ const fallbackKind = params.type === "channel" ? "channel" : "chat";
962
+ const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
963
+ if (!display) return null;
964
+ const signature = params.signature?.trim() || void 0;
965
+ const from = signature ? `${display} (${signature})` : display;
966
+ const chatType = params.chat.type?.trim() || void 0;
967
+ return {
968
+ from,
969
+ date: params.date,
970
+ fromType: params.type,
971
+ fromId: id,
972
+ fromUsername: username,
973
+ fromTitle: title,
974
+ fromSignature: signature,
975
+ fromChatType: chatType,
976
+ fromMessageId: params.messageId
977
+ };
978
+ }
979
+ function resolveForwardOrigin(origin) {
980
+ switch (origin.type) {
981
+ case "user": return buildForwardedContextFromUser({
982
+ user: origin.sender_user,
983
+ date: origin.date,
984
+ type: "user"
985
+ });
986
+ case "hidden_user": return buildForwardedContextFromHiddenName({
987
+ name: origin.sender_user_name,
988
+ date: origin.date,
989
+ type: "hidden_user"
990
+ });
991
+ case "chat": return buildForwardedContextFromChat({
992
+ chat: origin.sender_chat,
993
+ date: origin.date,
994
+ type: "chat",
995
+ signature: origin.author_signature
996
+ });
997
+ case "channel": return buildForwardedContextFromChat({
998
+ chat: origin.chat,
999
+ date: origin.date,
1000
+ type: "channel",
1001
+ signature: origin.author_signature,
1002
+ messageId: origin.message_id
1003
+ });
1004
+ default: return null;
1005
+ }
1006
+ }
1007
+ /** Extract forwarded message origin info from Telegram message. */
1008
+ function normalizeForwardedContext(msg) {
1009
+ if (!msg.forward_origin) return null;
1010
+ return resolveForwardOrigin(msg.forward_origin);
1011
+ }
1012
+ function extractTelegramLocation(msg) {
1013
+ const { venue, location } = msg;
1014
+ if (venue) return {
1015
+ latitude: venue.location.latitude,
1016
+ longitude: venue.location.longitude,
1017
+ accuracy: venue.location.horizontal_accuracy,
1018
+ name: venue.title,
1019
+ address: venue.address,
1020
+ source: "place",
1021
+ isLive: false
1022
+ };
1023
+ if (location) {
1024
+ const isLive = typeof location.live_period === "number" && location.live_period > 0;
1025
+ return {
1026
+ latitude: location.latitude,
1027
+ longitude: location.longitude,
1028
+ accuracy: location.horizontal_accuracy,
1029
+ source: isLive ? "live" : "pin",
1030
+ isLive
1031
+ };
1032
+ }
1033
+ return null;
1034
+ }
1035
+ //#endregion
1036
+ //#region src/infra/diagnostic-flags.ts
1037
+ const DIAGNOSTICS_ENV = "OPENCLAW_DIAGNOSTICS";
1038
+ function normalizeFlag(value) {
1039
+ return value.trim().toLowerCase();
1040
+ }
1041
+ function parseEnvFlags(raw) {
1042
+ if (!raw) return [];
1043
+ const trimmed = raw.trim();
1044
+ if (!trimmed) return [];
1045
+ const lowered = trimmed.toLowerCase();
1046
+ if ([
1047
+ "0",
1048
+ "false",
1049
+ "off",
1050
+ "none"
1051
+ ].includes(lowered)) return [];
1052
+ if ([
1053
+ "1",
1054
+ "true",
1055
+ "all",
1056
+ "*"
1057
+ ].includes(lowered)) return ["*"];
1058
+ return trimmed.split(/[,\s]+/).map(normalizeFlag).filter(Boolean);
1059
+ }
1060
+ function uniqueFlags(flags) {
1061
+ const seen = /* @__PURE__ */ new Set();
1062
+ const out = [];
1063
+ for (const flag of flags) {
1064
+ const normalized = normalizeFlag(flag);
1065
+ if (!normalized || seen.has(normalized)) continue;
1066
+ seen.add(normalized);
1067
+ out.push(normalized);
1068
+ }
1069
+ return out;
1070
+ }
1071
+ function resolveDiagnosticFlags(cfg, env = process.env) {
1072
+ const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : [];
1073
+ const envFlags = parseEnvFlags(env[DIAGNOSTICS_ENV]);
1074
+ return uniqueFlags([...configFlags, ...envFlags]);
1075
+ }
1076
+ function matchesDiagnosticFlag(flag, enabledFlags) {
1077
+ const target = normalizeFlag(flag);
1078
+ if (!target) return false;
1079
+ for (const raw of enabledFlags) {
1080
+ const enabled = normalizeFlag(raw);
1081
+ if (!enabled) continue;
1082
+ if (enabled === "*" || enabled === "all") return true;
1083
+ if (enabled.endsWith(".*")) {
1084
+ const prefix = enabled.slice(0, -2);
1085
+ if (target === prefix || target.startsWith(`${prefix}.`)) return true;
1086
+ }
1087
+ if (enabled.endsWith("*")) {
1088
+ const prefix = enabled.slice(0, -1);
1089
+ if (target.startsWith(prefix)) return true;
1090
+ }
1091
+ if (enabled === target) return true;
1092
+ }
1093
+ return false;
1094
+ }
1095
+ function isDiagnosticFlagEnabled(flag, cfg, env = process.env) {
1096
+ return matchesDiagnosticFlag(flag, resolveDiagnosticFlags(cfg, env));
1097
+ }
1098
+ //#endregion
1099
+ //#region src/telegram/api-logging.ts
1100
+ const fallbackLogger = createSubsystemLogger("telegram/api");
1101
+ function resolveTelegramApiLogger(runtime, logger) {
1102
+ if (logger) return logger;
1103
+ if (runtime?.error) return runtime.error;
1104
+ return (message) => fallbackLogger.error(message);
1105
+ }
1106
+ async function withTelegramApiErrorLogging({ operation, fn, runtime, logger, shouldLog }) {
1107
+ try {
1108
+ return await fn();
1109
+ } catch (err) {
1110
+ if (!shouldLog || shouldLog(err)) {
1111
+ const errText = formatErrorMessage(err);
1112
+ resolveTelegramApiLogger(runtime, logger)(danger(`telegram ${operation} failed: ${errText}`));
1113
+ }
1114
+ throw err;
1115
+ }
1116
+ }
1117
+ function splitTelegramCaption(text) {
1118
+ const trimmed = text?.trim() ?? "";
1119
+ if (!trimmed) return {
1120
+ caption: void 0,
1121
+ followUpText: void 0
1122
+ };
1123
+ if (trimmed.length > 1024) return {
1124
+ caption: void 0,
1125
+ followUpText: trimmed
1126
+ };
1127
+ return {
1128
+ caption: trimmed,
1129
+ followUpText: void 0
1130
+ };
1131
+ }
1132
+ //#endregion
1133
+ //#region src/telegram/format.ts
1134
+ function escapeHtml(text) {
1135
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1136
+ }
1137
+ function escapeHtmlAttr(text) {
1138
+ return escapeHtml(text).replace(/"/g, "&quot;");
1139
+ }
1140
+ /**
1141
+ * File extensions that share TLDs and commonly appear in code/documentation.
1142
+ * These are wrapped in <code> tags to prevent Telegram from generating
1143
+ * spurious domain registrar previews.
1144
+ *
1145
+ * Only includes extensions that are:
1146
+ * 1. Commonly used as file extensions in code/docs
1147
+ * 2. Rarely used as intentional domain references
1148
+ *
1149
+ * Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
1150
+ */
1151
+ const FILE_EXTENSIONS_WITH_TLD = new Set([
1152
+ "md",
1153
+ "go",
1154
+ "py",
1155
+ "pl",
1156
+ "sh",
1157
+ "am",
1158
+ "at",
1159
+ "be",
1160
+ "cc"
1161
+ ]);
1162
+ /** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
1163
+ function isAutoLinkedFileRef(href, label) {
1164
+ if (href.replace(/^https?:\/\//i, "") !== label) return false;
1165
+ const dotIndex = label.lastIndexOf(".");
1166
+ if (dotIndex < 1) return false;
1167
+ const ext = label.slice(dotIndex + 1).toLowerCase();
1168
+ if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) return false;
1169
+ const segments = label.split("/");
1170
+ if (segments.length > 1) {
1171
+ for (let i = 0; i < segments.length - 1; i++) if (segments[i].includes(".")) return false;
1172
+ }
1173
+ return true;
1174
+ }
1175
+ function buildTelegramLink(link, text) {
1176
+ const href = link.href.trim();
1177
+ if (!href) return null;
1178
+ if (link.start === link.end) return null;
1179
+ if (isAutoLinkedFileRef(href, text.slice(link.start, link.end))) return null;
1180
+ const safeHref = escapeHtmlAttr(href);
1181
+ return {
1182
+ start: link.start,
1183
+ end: link.end,
1184
+ open: `<a href="${safeHref}">`,
1185
+ close: "</a>"
1186
+ };
1187
+ }
1188
+ function renderTelegramHtml(ir) {
1189
+ return renderMarkdownWithMarkers(ir, {
1190
+ styleMarkers: {
1191
+ bold: {
1192
+ open: "<b>",
1193
+ close: "</b>"
1194
+ },
1195
+ italic: {
1196
+ open: "<i>",
1197
+ close: "</i>"
1198
+ },
1199
+ strikethrough: {
1200
+ open: "<s>",
1201
+ close: "</s>"
1202
+ },
1203
+ code: {
1204
+ open: "<code>",
1205
+ close: "</code>"
1206
+ },
1207
+ code_block: {
1208
+ open: "<pre><code>",
1209
+ close: "</code></pre>"
1210
+ },
1211
+ spoiler: {
1212
+ open: "<tg-spoiler>",
1213
+ close: "</tg-spoiler>"
1214
+ },
1215
+ blockquote: {
1216
+ open: "<blockquote>",
1217
+ close: "</blockquote>"
1218
+ }
1219
+ },
1220
+ escapeText: escapeHtml,
1221
+ buildLink: buildTelegramLink
1222
+ });
1223
+ }
1224
+ function markdownToTelegramHtml(markdown, options = {}) {
1225
+ const html = renderTelegramHtml(markdownToIR(markdown ?? "", {
1226
+ linkify: true,
1227
+ enableSpoilers: true,
1228
+ headingStyle: "none",
1229
+ blockquotePrefix: "",
1230
+ tableMode: options.tableMode
1231
+ }));
1232
+ if (options.wrapFileRefs !== false) return wrapFileReferencesInHtml(html);
1233
+ return html;
1234
+ }
1235
+ /**
1236
+ * Wraps standalone file references (with TLD extensions) in <code> tags.
1237
+ * This prevents Telegram from treating them as URLs and generating
1238
+ * irrelevant domain registrar previews.
1239
+ *
1240
+ * Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
1241
+ * Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
1242
+ */
1243
+ /** Escape regex metacharacters in a string */
1244
+ function escapeRegex(str) {
1245
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1246
+ }
1247
+ const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
1248
+ const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
1249
+ const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
1250
+ const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
1251
+ const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
1252
+ function wrapStandaloneFileRef(match, prefix, filename) {
1253
+ if (filename.startsWith("//")) return match;
1254
+ if (/https?:\/\/$/i.test(prefix)) return match;
1255
+ return `${prefix}<code>${escapeHtml(filename)}</code>`;
1256
+ }
1257
+ function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
1258
+ if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) return text;
1259
+ return text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef).replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
1260
+ }
1261
+ function wrapFileReferencesInHtml(html) {
1262
+ AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
1263
+ const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
1264
+ if (!isAutoLinkedFileRef(`http://${label}`, label)) return _match;
1265
+ return `<code>${escapeHtml(label)}</code>`;
1266
+ });
1267
+ let codeDepth = 0;
1268
+ let preDepth = 0;
1269
+ let anchorDepth = 0;
1270
+ let result = "";
1271
+ let lastIndex = 0;
1272
+ HTML_TAG_PATTERN.lastIndex = 0;
1273
+ let match;
1274
+ while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
1275
+ const tagStart = match.index;
1276
+ const tagEnd = HTML_TAG_PATTERN.lastIndex;
1277
+ const isClosing = match[1] === "</";
1278
+ const tagName = match[2].toLowerCase();
1279
+ const textBefore = deLinkified.slice(lastIndex, tagStart);
1280
+ result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
1281
+ if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
1282
+ else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
1283
+ else if (tagName === "a") anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
1284
+ result += deLinkified.slice(tagStart, tagEnd);
1285
+ lastIndex = tagEnd;
1286
+ }
1287
+ const remainingText = deLinkified.slice(lastIndex);
1288
+ result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
1289
+ return result;
1290
+ }
1291
+ function renderTelegramHtmlText(text, options = {}) {
1292
+ if ((options.textMode ?? "markdown") === "html") return text;
1293
+ return markdownToTelegramHtml(text, { tableMode: options.tableMode });
1294
+ }
1295
+ function splitTelegramChunkByHtmlLimit(chunk, htmlLimit, renderedHtmlLength) {
1296
+ const currentTextLength = chunk.text.length;
1297
+ if (currentTextLength <= 1) return [chunk];
1298
+ const proportionalLimit = Math.floor(currentTextLength * htmlLimit / Math.max(renderedHtmlLength, 1));
1299
+ const candidateLimit = Math.min(currentTextLength - 1, proportionalLimit);
1300
+ const split = splitMarkdownIRPreserveWhitespace(chunk, Number.isFinite(candidateLimit) && candidateLimit > 0 ? candidateLimit : Math.max(1, Math.floor(currentTextLength / 2)));
1301
+ if (split.length > 1) return split;
1302
+ return splitMarkdownIRPreserveWhitespace(chunk, Math.max(1, Math.floor(currentTextLength / 2)));
1303
+ }
1304
+ function sliceStyleSpans(styles, start, end) {
1305
+ return styles.flatMap((span) => {
1306
+ if (span.end <= start || span.start >= end) return [];
1307
+ const nextStart = Math.max(span.start, start) - start;
1308
+ const nextEnd = Math.min(span.end, end) - start;
1309
+ if (nextEnd <= nextStart) return [];
1310
+ return [{
1311
+ ...span,
1312
+ start: nextStart,
1313
+ end: nextEnd
1314
+ }];
1315
+ });
1316
+ }
1317
+ function sliceLinkSpans(links, start, end) {
1318
+ return links.flatMap((link) => {
1319
+ if (link.end <= start || link.start >= end) return [];
1320
+ const nextStart = Math.max(link.start, start) - start;
1321
+ const nextEnd = Math.min(link.end, end) - start;
1322
+ if (nextEnd <= nextStart) return [];
1323
+ return [{
1324
+ ...link,
1325
+ start: nextStart,
1326
+ end: nextEnd
1327
+ }];
1328
+ });
1329
+ }
1330
+ function splitMarkdownIRPreserveWhitespace(ir, limit) {
1331
+ if (!ir.text) return [];
1332
+ const normalizedLimit = Math.max(1, Math.floor(limit));
1333
+ if (normalizedLimit <= 0 || ir.text.length <= normalizedLimit) return [ir];
1334
+ const chunks = [];
1335
+ let cursor = 0;
1336
+ while (cursor < ir.text.length) {
1337
+ const end = Math.min(ir.text.length, cursor + normalizedLimit);
1338
+ chunks.push({
1339
+ text: ir.text.slice(cursor, end),
1340
+ styles: sliceStyleSpans(ir.styles, cursor, end),
1341
+ links: sliceLinkSpans(ir.links, cursor, end)
1342
+ });
1343
+ cursor = end;
1344
+ }
1345
+ return chunks;
1346
+ }
1347
+ function renderTelegramChunksWithinHtmlLimit(ir, limit) {
1348
+ const normalizedLimit = Math.max(1, Math.floor(limit));
1349
+ const pending = chunkMarkdownIR(ir, normalizedLimit);
1350
+ const rendered = [];
1351
+ while (pending.length > 0) {
1352
+ const chunk = pending.shift();
1353
+ if (!chunk) continue;
1354
+ const html = wrapFileReferencesInHtml(renderTelegramHtml(chunk));
1355
+ if (html.length <= normalizedLimit || chunk.text.length <= 1) {
1356
+ rendered.push({
1357
+ html,
1358
+ text: chunk.text
1359
+ });
1360
+ continue;
1361
+ }
1362
+ const split = splitTelegramChunkByHtmlLimit(chunk, normalizedLimit, html.length);
1363
+ if (split.length <= 1) {
1364
+ rendered.push({
1365
+ html,
1366
+ text: chunk.text
1367
+ });
1368
+ continue;
1369
+ }
1370
+ pending.unshift(...split);
1371
+ }
1372
+ return rendered;
1373
+ }
1374
+ function markdownToTelegramChunks(markdown, limit, options = {}) {
1375
+ return renderTelegramChunksWithinHtmlLimit(markdownToIR(markdown ?? "", {
1376
+ linkify: true,
1377
+ enableSpoilers: true,
1378
+ headingStyle: "none",
1379
+ blockquotePrefix: "",
1380
+ tableMode: options.tableMode
1381
+ }), limit);
1382
+ }
1383
+ //#endregion
1384
+ //#region src/telegram/network-errors.ts
1385
+ const RECOVERABLE_ERROR_CODES = new Set([
1386
+ "ECONNRESET",
1387
+ "ECONNREFUSED",
1388
+ "EPIPE",
1389
+ "ETIMEDOUT",
1390
+ "ESOCKETTIMEDOUT",
1391
+ "ENETUNREACH",
1392
+ "EHOSTUNREACH",
1393
+ "ENOTFOUND",
1394
+ "EAI_AGAIN",
1395
+ "UND_ERR_CONNECT_TIMEOUT",
1396
+ "UND_ERR_HEADERS_TIMEOUT",
1397
+ "UND_ERR_BODY_TIMEOUT",
1398
+ "UND_ERR_SOCKET",
1399
+ "UND_ERR_ABORTED",
1400
+ "ECONNABORTED",
1401
+ "ERR_NETWORK"
1402
+ ]);
1403
+ /**
1404
+ * Error codes that are safe to retry for non-idempotent send operations (e.g. sendMessage).
1405
+ *
1406
+ * These represent failures that occur *before* the request reaches Telegram's servers,
1407
+ * meaning the message was definitely not delivered and it is safe to retry.
1408
+ *
1409
+ * Contrast with RECOVERABLE_ERROR_CODES which includes codes like ECONNRESET and ETIMEDOUT
1410
+ * that can fire *after* Telegram has already received and delivered a message — retrying
1411
+ * those would cause duplicate messages.
1412
+ */
1413
+ const PRE_CONNECT_ERROR_CODES = new Set([
1414
+ "ECONNREFUSED",
1415
+ "ENOTFOUND",
1416
+ "EAI_AGAIN",
1417
+ "ENETUNREACH",
1418
+ "EHOSTUNREACH"
1419
+ ]);
1420
+ const RECOVERABLE_ERROR_NAMES = new Set([
1421
+ "AbortError",
1422
+ "TimeoutError",
1423
+ "ConnectTimeoutError",
1424
+ "HeadersTimeoutError",
1425
+ "BodyTimeoutError"
1426
+ ]);
1427
+ const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
1428
+ const GRAMMY_NETWORK_REQUEST_FAILED_AFTER_RE = /^network request(?:\s+for\s+["']?[^"']+["']?)?\s+failed\s+after\b.*[!.]?$/i;
1429
+ const RECOVERABLE_MESSAGE_SNIPPETS = [
1430
+ "undici",
1431
+ "network error",
1432
+ "network request",
1433
+ "client network socket disconnected",
1434
+ "socket hang up",
1435
+ "getaddrinfo",
1436
+ "timeout",
1437
+ "timed out"
1438
+ ];
1439
+ function collectTelegramErrorCandidates(err) {
1440
+ return collectErrorGraphCandidates(err, (current) => {
1441
+ const nested = [current.cause, current.reason];
1442
+ if (Array.isArray(current.errors)) nested.push(...current.errors);
1443
+ if (readErrorName(current) === "HttpError") nested.push(current.error);
1444
+ return nested;
1445
+ });
1446
+ }
1447
+ function normalizeCode(code) {
1448
+ return code?.trim().toUpperCase() ?? "";
1449
+ }
1450
+ function getErrorCode(err) {
1451
+ const direct = extractErrorCode(err);
1452
+ if (direct) return direct;
1453
+ if (!err || typeof err !== "object") return;
1454
+ const errno = err.errno;
1455
+ if (typeof errno === "string") return errno;
1456
+ if (typeof errno === "number") return String(errno);
1457
+ }
1458
+ /**
1459
+ * Returns true if the error is safe to retry for a non-idempotent Telegram send operation
1460
+ * (e.g. sendMessage). Only matches errors that are guaranteed to have occurred *before*
1461
+ * the request reached Telegram's servers, preventing duplicate message delivery.
1462
+ *
1463
+ * Use this instead of isRecoverableTelegramNetworkError for sendMessage/sendPhoto/etc.
1464
+ * calls where a retry would create a duplicate visible message.
1465
+ */
1466
+ function isSafeToRetrySendError(err) {
1467
+ if (!err) return false;
1468
+ for (const candidate of collectTelegramErrorCandidates(err)) {
1469
+ const code = normalizeCode(getErrorCode(candidate));
1470
+ if (code && PRE_CONNECT_ERROR_CODES.has(code)) return true;
1471
+ }
1472
+ return false;
1473
+ }
1474
+ function isRecoverableTelegramNetworkError(err, options = {}) {
1475
+ if (!err) return false;
1476
+ const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch : options.context !== "send";
1477
+ for (const candidate of collectTelegramErrorCandidates(err)) {
1478
+ const code = normalizeCode(getErrorCode(candidate));
1479
+ if (code && RECOVERABLE_ERROR_CODES.has(code)) return true;
1480
+ const name = readErrorName(candidate);
1481
+ if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true;
1482
+ const message = formatErrorMessage(candidate).trim().toLowerCase();
1483
+ if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) return true;
1484
+ if (message && GRAMMY_NETWORK_REQUEST_FAILED_AFTER_RE.test(message)) return true;
1485
+ if (allowMessageMatch && message) {
1486
+ if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) return true;
1487
+ }
1488
+ }
1489
+ return false;
1490
+ }
1491
+ //#endregion
1492
+ //#region src/telegram/sent-message-cache.ts
1493
+ /**
1494
+ * In-memory cache of sent message IDs per chat.
1495
+ * Used to identify bot's own messages for reaction filtering ("own" mode).
1496
+ */
1497
+ const TTL_MS = 1440 * 60 * 1e3;
1498
+ const sentMessages = /* @__PURE__ */ new Map();
1499
+ function getChatKey(chatId) {
1500
+ return String(chatId);
1501
+ }
1502
+ function cleanupExpired(entry) {
1503
+ const now = Date.now();
1504
+ for (const [msgId, timestamp] of entry.timestamps) if (now - timestamp > TTL_MS) entry.timestamps.delete(msgId);
1505
+ }
1506
+ /**
1507
+ * Record a message ID as sent by the bot.
1508
+ */
1509
+ function recordSentMessage(chatId, messageId) {
1510
+ const key = getChatKey(chatId);
1511
+ let entry = sentMessages.get(key);
1512
+ if (!entry) {
1513
+ entry = { timestamps: /* @__PURE__ */ new Map() };
1514
+ sentMessages.set(key, entry);
1515
+ }
1516
+ entry.timestamps.set(messageId, Date.now());
1517
+ if (entry.timestamps.size > 100) cleanupExpired(entry);
1518
+ }
1519
+ /**
1520
+ * Check if a message was sent by the bot.
1521
+ */
1522
+ function wasSentByBot(chatId, messageId) {
1523
+ const key = getChatKey(chatId);
1524
+ const entry = sentMessages.get(key);
1525
+ if (!entry) return false;
1526
+ cleanupExpired(entry);
1527
+ return entry.timestamps.has(messageId);
1528
+ }
1529
+ //#endregion
1530
+ //#region src/cron/store.ts
1531
+ const DEFAULT_CRON_DIR = path.join(CONFIG_DIR, "cron");
1532
+ const DEFAULT_CRON_STORE_PATH = path.join(DEFAULT_CRON_DIR, "jobs.json");
1533
+ const serializedStoreCache = /* @__PURE__ */ new Map();
1534
+ function resolveCronStorePath(storePath) {
1535
+ if (storePath?.trim()) {
1536
+ const raw = storePath.trim();
1537
+ if (raw.startsWith("~")) return path.resolve(expandHomePrefix(raw));
1538
+ return path.resolve(raw);
1539
+ }
1540
+ return DEFAULT_CRON_STORE_PATH;
1541
+ }
1542
+ async function loadCronStore(storePath) {
1543
+ try {
1544
+ const raw = await fs.promises.readFile(storePath, "utf-8");
1545
+ let parsed;
1546
+ try {
1547
+ parsed = json5.parse(raw);
1548
+ } catch (err) {
1549
+ throw new Error(`Failed to parse cron store at ${storePath}: ${String(err)}`, { cause: err });
1550
+ }
1551
+ const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1552
+ const store = {
1553
+ version: 1,
1554
+ jobs: (Array.isArray(parsedRecord.jobs) ? parsedRecord.jobs : []).filter(Boolean)
1555
+ };
1556
+ serializedStoreCache.set(storePath, JSON.stringify(store, null, 2));
1557
+ return store;
1558
+ } catch (err) {
1559
+ if (err?.code === "ENOENT") {
1560
+ serializedStoreCache.delete(storePath);
1561
+ return {
1562
+ version: 1,
1563
+ jobs: []
1564
+ };
1565
+ }
1566
+ throw err;
1567
+ }
1568
+ }
1569
+ async function setSecureFileMode(filePath) {
1570
+ await fs.promises.chmod(filePath, 384).catch(() => void 0);
1571
+ }
1572
+ async function saveCronStore(storePath, store, opts) {
1573
+ const storeDir = path.dirname(storePath);
1574
+ await fs.promises.mkdir(storeDir, {
1575
+ recursive: true,
1576
+ mode: 448
1577
+ });
1578
+ await fs.promises.chmod(storeDir, 448).catch(() => void 0);
1579
+ const json = JSON.stringify(store, null, 2);
1580
+ const cached = serializedStoreCache.get(storePath);
1581
+ if (cached === json) return;
1582
+ let previous = cached ?? null;
1583
+ if (previous === null) try {
1584
+ previous = await fs.promises.readFile(storePath, "utf-8");
1585
+ } catch (err) {
1586
+ if (err.code !== "ENOENT") throw err;
1587
+ }
1588
+ if (previous === json) {
1589
+ serializedStoreCache.set(storePath, json);
1590
+ return;
1591
+ }
1592
+ const tmp = `${storePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
1593
+ await fs.promises.writeFile(tmp, json, {
1594
+ encoding: "utf-8",
1595
+ mode: 384
1596
+ });
1597
+ await setSecureFileMode(tmp);
1598
+ if (previous !== null && !opts?.skipBackup) try {
1599
+ const backupPath = `${storePath}.bak`;
1600
+ await fs.promises.copyFile(storePath, backupPath);
1601
+ await setSecureFileMode(backupPath);
1602
+ } catch {}
1603
+ await renameWithRetry(tmp, storePath);
1604
+ await setSecureFileMode(storePath);
1605
+ serializedStoreCache.set(storePath, json);
1606
+ }
1607
+ const RENAME_MAX_RETRIES = 3;
1608
+ const RENAME_BASE_DELAY_MS = 50;
1609
+ async function renameWithRetry(src, dest) {
1610
+ for (let attempt = 0; attempt <= RENAME_MAX_RETRIES; attempt++) try {
1611
+ await fs.promises.rename(src, dest);
1612
+ return;
1613
+ } catch (err) {
1614
+ const code = err.code;
1615
+ if (code === "EBUSY" && attempt < RENAME_MAX_RETRIES) {
1616
+ await new Promise((resolve) => setTimeout(resolve, RENAME_BASE_DELAY_MS * 2 ** attempt));
1617
+ continue;
1618
+ }
1619
+ if (code === "EPERM" || code === "EEXIST") {
1620
+ await fs.promises.copyFile(src, dest);
1621
+ await fs.promises.unlink(src).catch(() => {});
1622
+ return;
1623
+ }
1624
+ throw err;
1625
+ }
1626
+ }
1627
+ //#endregion
1628
+ //#region src/telegram/target-writeback.ts
1629
+ const writebackLogger = createSubsystemLogger("telegram/target-writeback");
1630
+ function asObjectRecord(value) {
1631
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1632
+ return value;
1633
+ }
1634
+ function normalizeTelegramLookupTargetForMatch(raw) {
1635
+ const normalized = normalizeTelegramLookupTarget(raw);
1636
+ if (!normalized) return;
1637
+ return normalized.startsWith("@") ? normalized.toLowerCase() : normalized;
1638
+ }
1639
+ function normalizeTelegramTargetForMatch(raw) {
1640
+ const parsed = parseTelegramTarget(raw);
1641
+ const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
1642
+ if (!normalized) return;
1643
+ return `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`;
1644
+ }
1645
+ function buildResolvedTelegramTarget(params) {
1646
+ const { raw, parsed, resolvedChatId } = params;
1647
+ if (parsed.messageThreadId == null) return resolvedChatId;
1648
+ return raw.includes(":topic:") ? `${resolvedChatId}:topic:${parsed.messageThreadId}` : `${resolvedChatId}:${parsed.messageThreadId}`;
1649
+ }
1650
+ function resolveLegacyRewrite(params) {
1651
+ const parsed = parseTelegramTarget(params.raw);
1652
+ if (normalizeTelegramChatId(parsed.chatId)) return null;
1653
+ const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
1654
+ if (!normalized) return null;
1655
+ return {
1656
+ matchKey: `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`,
1657
+ resolvedTarget: buildResolvedTelegramTarget({
1658
+ raw: params.raw,
1659
+ parsed,
1660
+ resolvedChatId: params.resolvedChatId
1661
+ })
1662
+ };
1663
+ }
1664
+ function rewriteTargetIfMatch(params) {
1665
+ if (typeof params.rawValue !== "string" && typeof params.rawValue !== "number") return null;
1666
+ const value = String(params.rawValue).trim();
1667
+ if (!value) return null;
1668
+ if (normalizeTelegramTargetForMatch(value) !== params.matchKey) return null;
1669
+ return params.resolvedTarget;
1670
+ }
1671
+ function replaceTelegramDefaultToTargets(params) {
1672
+ let changed = false;
1673
+ const telegram = asObjectRecord(params.cfg.channels?.telegram);
1674
+ if (!telegram) return changed;
1675
+ const maybeReplace = (holder, key) => {
1676
+ const nextTarget = rewriteTargetIfMatch({
1677
+ rawValue: holder[key],
1678
+ matchKey: params.matchKey,
1679
+ resolvedTarget: params.resolvedTarget
1680
+ });
1681
+ if (!nextTarget) return;
1682
+ holder[key] = nextTarget;
1683
+ changed = true;
1684
+ };
1685
+ maybeReplace(telegram, "defaultTo");
1686
+ const accounts = asObjectRecord(telegram.accounts);
1687
+ if (!accounts) return changed;
1688
+ for (const accountId of Object.keys(accounts)) {
1689
+ const account = asObjectRecord(accounts[accountId]);
1690
+ if (!account) continue;
1691
+ maybeReplace(account, "defaultTo");
1692
+ }
1693
+ return changed;
1694
+ }
1695
+ async function maybePersistResolvedTelegramTarget(params) {
1696
+ const raw = params.rawTarget.trim();
1697
+ if (!raw) return;
1698
+ const rewrite = resolveLegacyRewrite({
1699
+ raw,
1700
+ resolvedChatId: params.resolvedChatId
1701
+ });
1702
+ if (!rewrite) return;
1703
+ const { matchKey, resolvedTarget } = rewrite;
1704
+ try {
1705
+ const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
1706
+ const nextConfig = structuredClone(snapshot.config ?? {});
1707
+ if (replaceTelegramDefaultToTargets({
1708
+ cfg: nextConfig,
1709
+ matchKey,
1710
+ resolvedTarget
1711
+ })) {
1712
+ await writeConfigFile(nextConfig, writeOptions);
1713
+ if (params.verbose) writebackLogger.warn(`resolved Telegram defaultTo target ${raw} -> ${resolvedTarget}`);
1714
+ }
1715
+ } catch (err) {
1716
+ if (params.verbose) writebackLogger.warn(`failed to persist Telegram defaultTo target ${raw}: ${String(err)}`);
1717
+ }
1718
+ try {
1719
+ const storePath = resolveCronStorePath(params.cfg.cron?.store);
1720
+ const store = await loadCronStore(storePath);
1721
+ let cronChanged = false;
1722
+ for (const job of store.jobs) {
1723
+ if (job.delivery?.channel !== "telegram") continue;
1724
+ const nextTarget = rewriteTargetIfMatch({
1725
+ rawValue: job.delivery.to,
1726
+ matchKey,
1727
+ resolvedTarget
1728
+ });
1729
+ if (!nextTarget) continue;
1730
+ job.delivery.to = nextTarget;
1731
+ cronChanged = true;
1732
+ }
1733
+ if (cronChanged) {
1734
+ await saveCronStore(storePath, store);
1735
+ if (params.verbose) writebackLogger.warn(`resolved Telegram cron delivery target ${raw} -> ${resolvedTarget}`);
1736
+ }
1737
+ } catch (err) {
1738
+ if (params.verbose) writebackLogger.warn(`failed to persist Telegram cron target ${raw}: ${String(err)}`);
1739
+ }
1740
+ }
1741
+ //#endregion
1742
+ //#region src/telegram/voice.ts
1743
+ function resolveTelegramVoiceDecision(opts) {
1744
+ if (!opts.wantsVoice) return { useVoice: false };
1745
+ if (isTelegramVoiceCompatibleAudio(opts)) return { useVoice: true };
1746
+ return {
1747
+ useVoice: false,
1748
+ reason: `media is ${opts.contentType ?? "unknown"} (${opts.fileName ?? "unknown"})`
1749
+ };
1750
+ }
1751
+ function resolveTelegramVoiceSend(opts) {
1752
+ const decision = resolveTelegramVoiceDecision(opts);
1753
+ if (decision.reason && opts.logFallback) opts.logFallback(`Telegram voice requested but ${decision.reason}; sending as audio file instead.`);
1754
+ return { useVoice: decision.useVoice };
1755
+ }
1756
+ //#endregion
1757
+ //#region src/telegram/send.ts
1758
+ function resolveTelegramMessageIdOrThrow(result, context) {
1759
+ if (typeof result?.message_id === "number" && Number.isFinite(result.message_id)) return Math.trunc(result.message_id);
1760
+ throw new Error(`Telegram ${context} returned no message_id`);
1761
+ }
1762
+ const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
1763
+ const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
1764
+ const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
1765
+ const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
1766
+ const sendLogger = createSubsystemLogger("telegram/send");
1767
+ const diagLogger = createSubsystemLogger("telegram/diagnostic");
1768
+ const telegramClientOptionsCache = /* @__PURE__ */ new Map();
1769
+ const MAX_TELEGRAM_CLIENT_OPTIONS_CACHE_SIZE = 64;
1770
+ function createTelegramHttpLogger(cfg) {
1771
+ if (!isDiagnosticFlagEnabled("telegram.http", cfg)) return () => {};
1772
+ return (label, err) => {
1773
+ if (!(err instanceof HttpError)) return;
1774
+ const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
1775
+ diagLogger.warn(`telegram http error (${label}): ${detail}`);
1776
+ };
1777
+ }
1778
+ function shouldUseTelegramClientOptionsCache() {
1779
+ return !process.env.VITEST && true;
1780
+ }
1781
+ function buildTelegramClientOptionsCacheKey(params) {
1782
+ const proxyKey = params.account.config.proxy?.trim() ?? "";
1783
+ const autoSelectFamily = params.account.config.network?.autoSelectFamily;
1784
+ const autoSelectFamilyKey = typeof autoSelectFamily === "boolean" ? String(autoSelectFamily) : "default";
1785
+ const dnsResultOrderKey = params.account.config.network?.dnsResultOrder ?? "default";
1786
+ const timeoutSecondsKey = typeof params.timeoutSeconds === "number" ? String(params.timeoutSeconds) : "default";
1787
+ return `${params.account.accountId}::${proxyKey}::${autoSelectFamilyKey}::${dnsResultOrderKey}::${timeoutSecondsKey}`;
1788
+ }
1789
+ function setCachedTelegramClientOptions(cacheKey, clientOptions) {
1790
+ telegramClientOptionsCache.set(cacheKey, clientOptions);
1791
+ if (telegramClientOptionsCache.size > MAX_TELEGRAM_CLIENT_OPTIONS_CACHE_SIZE) {
1792
+ const oldestKey = telegramClientOptionsCache.keys().next().value;
1793
+ if (oldestKey !== void 0) telegramClientOptionsCache.delete(oldestKey);
1794
+ }
1795
+ return clientOptions;
1796
+ }
1797
+ function resolveTelegramClientOptions(account) {
1798
+ const timeoutSeconds = typeof account.config.timeoutSeconds === "number" && Number.isFinite(account.config.timeoutSeconds) ? Math.max(1, Math.floor(account.config.timeoutSeconds)) : void 0;
1799
+ const cacheKey = shouldUseTelegramClientOptionsCache() ? buildTelegramClientOptionsCacheKey({
1800
+ account,
1801
+ timeoutSeconds
1802
+ }) : null;
1803
+ if (cacheKey && telegramClientOptionsCache.has(cacheKey)) return telegramClientOptionsCache.get(cacheKey);
1804
+ const proxyUrl = account.config.proxy?.trim();
1805
+ const fetchImpl = resolveTelegramFetch(proxyUrl ? makeProxyFetch(proxyUrl) : void 0, { network: account.config.network });
1806
+ const clientOptions = fetchImpl || timeoutSeconds ? {
1807
+ ...fetchImpl ? { fetch: fetchImpl } : {},
1808
+ ...timeoutSeconds ? { timeoutSeconds } : {}
1809
+ } : void 0;
1810
+ if (cacheKey) return setCachedTelegramClientOptions(cacheKey, clientOptions);
1811
+ return clientOptions;
1812
+ }
1813
+ function resolveToken(explicit, params) {
1814
+ if (explicit?.trim()) return explicit.trim();
1815
+ if (!params.token) throw new Error(`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
1816
+ return params.token.trim();
1817
+ }
1818
+ async function resolveChatId(to, params) {
1819
+ const numericChatId = normalizeTelegramChatId(to);
1820
+ if (numericChatId) return numericChatId;
1821
+ const lookupTarget = normalizeTelegramLookupTarget(to);
1822
+ const getChat = params.api.getChat;
1823
+ if (!lookupTarget || typeof getChat !== "function") throw new Error("Telegram recipient must be a numeric chat ID");
1824
+ try {
1825
+ const chat = await getChat.call(params.api, lookupTarget);
1826
+ const resolved = normalizeTelegramChatId(String(chat?.id ?? ""));
1827
+ if (!resolved) throw new Error(`resolved chat id is not numeric (${String(chat?.id ?? "")})`);
1828
+ if (params.verbose) sendLogger.warn(`telegram recipient ${lookupTarget} resolved to numeric chat id ${resolved}`);
1829
+ return resolved;
1830
+ } catch (err) {
1831
+ const detail = formatErrorMessage(err);
1832
+ throw new Error(`Telegram recipient ${lookupTarget} could not be resolved to a numeric chat ID (${detail})`, { cause: err });
1833
+ }
1834
+ }
1835
+ async function resolveAndPersistChatId(params) {
1836
+ const chatId = await resolveChatId(params.lookupTarget, {
1837
+ api: params.api,
1838
+ verbose: params.verbose
1839
+ });
1840
+ await maybePersistResolvedTelegramTarget({
1841
+ cfg: params.cfg,
1842
+ rawTarget: params.persistTarget,
1843
+ resolvedChatId: chatId,
1844
+ verbose: params.verbose
1845
+ });
1846
+ return chatId;
1847
+ }
1848
+ function normalizeMessageId(raw) {
1849
+ if (typeof raw === "number" && Number.isFinite(raw)) return Math.trunc(raw);
1850
+ if (typeof raw === "string") {
1851
+ const value = raw.trim();
1852
+ if (!value) throw new Error("Message id is required for Telegram actions");
1853
+ const parsed = Number.parseInt(value, 10);
1854
+ if (Number.isFinite(parsed)) return parsed;
1855
+ }
1856
+ throw new Error("Message id is required for Telegram actions");
1857
+ }
1858
+ function isTelegramThreadNotFoundError(err) {
1859
+ return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
1860
+ }
1861
+ function isTelegramMessageNotModifiedError(err) {
1862
+ return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
1863
+ }
1864
+ function hasMessageThreadIdParam(params) {
1865
+ if (!params) return false;
1866
+ const value = params.message_thread_id;
1867
+ if (typeof value === "number") return Number.isFinite(value);
1868
+ if (typeof value === "string") return value.trim().length > 0;
1869
+ return false;
1870
+ }
1871
+ function removeMessageThreadIdParam(params) {
1872
+ if (!params || !hasMessageThreadIdParam(params)) return params;
1873
+ const next = { ...params };
1874
+ delete next.message_thread_id;
1875
+ return Object.keys(next).length > 0 ? next : void 0;
1876
+ }
1877
+ function isTelegramHtmlParseError(err) {
1878
+ return PARSE_ERR_RE.test(formatErrorMessage(err));
1879
+ }
1880
+ function buildTelegramThreadReplyParams(params) {
1881
+ const messageThreadId = params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
1882
+ const threadScope = params.chatType === "direct" ? "dm" : "forum";
1883
+ const threadIdParams = buildTelegramThreadParams(messageThreadId != null ? {
1884
+ id: messageThreadId,
1885
+ scope: threadScope
1886
+ } : void 0);
1887
+ const threadParams = threadIdParams ? { ...threadIdParams } : {};
1888
+ if (params.replyToMessageId != null) {
1889
+ const replyToMessageId = Math.trunc(params.replyToMessageId);
1890
+ if (params.quoteText?.trim()) threadParams.reply_parameters = {
1891
+ message_id: replyToMessageId,
1892
+ quote: params.quoteText.trim()
1893
+ };
1894
+ else threadParams.reply_to_message_id = replyToMessageId;
1895
+ }
1896
+ return threadParams;
1897
+ }
1898
+ async function withTelegramHtmlParseFallback(params) {
1899
+ try {
1900
+ return await params.requestHtml(params.label);
1901
+ } catch (err) {
1902
+ if (!isTelegramHtmlParseError(err)) throw err;
1903
+ if (params.verbose) sendLogger.warn(`telegram ${params.label} failed with HTML parse error, retrying as plain text: ${formatErrorMessage(err)}`);
1904
+ return await params.requestPlain(`${params.label}-plain`);
1905
+ }
1906
+ }
1907
+ function resolveTelegramApiContext(opts) {
1908
+ const cfg = opts.cfg ?? loadConfig();
1909
+ const account = resolveTelegramAccount({
1910
+ cfg,
1911
+ accountId: opts.accountId
1912
+ });
1913
+ const token = resolveToken(opts.token, account);
1914
+ const client = resolveTelegramClientOptions(account);
1915
+ return {
1916
+ cfg,
1917
+ account,
1918
+ api: opts.api ?? new Bot(token, client ? { client } : void 0).api
1919
+ };
1920
+ }
1921
+ function createTelegramRequestWithDiag(params) {
1922
+ const request = createTelegramRetryRunner({
1923
+ retry: params.retry,
1924
+ configRetry: params.account.config.retry,
1925
+ verbose: params.verbose,
1926
+ ...params.shouldRetry ? { shouldRetry: params.shouldRetry } : {},
1927
+ ...params.strictShouldRetry ? { strictShouldRetry: true } : {}
1928
+ });
1929
+ const logHttpError = createTelegramHttpLogger(params.cfg);
1930
+ return (fn, label, options) => {
1931
+ const runRequest = () => request(fn, label);
1932
+ return (params.useApiErrorLogging === false ? runRequest() : withTelegramApiErrorLogging({
1933
+ operation: label ?? "request",
1934
+ fn: runRequest,
1935
+ ...options?.shouldLog ? { shouldLog: options.shouldLog } : {}
1936
+ })).catch((err) => {
1937
+ logHttpError(label ?? "request", err);
1938
+ throw err;
1939
+ });
1940
+ };
1941
+ }
1942
+ function wrapTelegramChatNotFoundError(err, params) {
1943
+ if (!CHAT_NOT_FOUND_RE.test(formatErrorMessage(err))) return err;
1944
+ return new Error([
1945
+ `Telegram send failed: chat not found (chat_id=${params.chatId}).`,
1946
+ "Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
1947
+ `Input was: ${JSON.stringify(params.input)}.`
1948
+ ].join(" "));
1949
+ }
1950
+ async function withTelegramThreadFallback(params, label, verbose, attempt) {
1951
+ try {
1952
+ return await attempt(params, label);
1953
+ } catch (err) {
1954
+ if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) throw err;
1955
+ if (verbose) sendLogger.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
1956
+ return await attempt(removeMessageThreadIdParam(params), `${label}-threadless`);
1957
+ }
1958
+ }
1959
+ function createRequestWithChatNotFound(params) {
1960
+ return async (fn, label) => params.requestWithDiag(fn, label).catch((err) => {
1961
+ throw wrapTelegramChatNotFoundError(err, {
1962
+ chatId: params.chatId,
1963
+ input: params.input
1964
+ });
1965
+ });
1966
+ }
1967
+ function createTelegramNonIdempotentRequestWithDiag(params) {
1968
+ return createTelegramRequestWithDiag({
1969
+ cfg: params.cfg,
1970
+ account: params.account,
1971
+ retry: params.retry,
1972
+ verbose: params.verbose,
1973
+ useApiErrorLogging: params.useApiErrorLogging,
1974
+ shouldRetry: (err) => isSafeToRetrySendError(err),
1975
+ strictShouldRetry: true
1976
+ });
1977
+ }
1978
+ function buildInlineKeyboard(buttons) {
1979
+ if (!buttons?.length) return;
1980
+ const rows = buttons.map((row) => row.filter((button) => button?.text && button?.callback_data).map((button) => ({
1981
+ text: button.text,
1982
+ callback_data: button.callback_data,
1983
+ ...button.style ? { style: button.style } : {}
1984
+ }))).filter((row) => row.length > 0);
1985
+ if (rows.length === 0) return;
1986
+ return { inline_keyboard: rows };
1987
+ }
1988
+ async function sendMessageTelegram(to, text, opts = {}) {
1989
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
1990
+ const target = parseTelegramTarget(to);
1991
+ const chatId = await resolveAndPersistChatId({
1992
+ cfg,
1993
+ api,
1994
+ lookupTarget: target.chatId,
1995
+ persistTarget: to,
1996
+ verbose: opts.verbose
1997
+ });
1998
+ const mediaUrl = opts.mediaUrl?.trim();
1999
+ const mediaMaxBytes = opts.maxBytes ?? (typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb : 100) * 1024 * 1024;
2000
+ const replyMarkup = buildInlineKeyboard(opts.buttons);
2001
+ const threadParams = buildTelegramThreadReplyParams({
2002
+ targetMessageThreadId: target.messageThreadId,
2003
+ messageThreadId: opts.messageThreadId,
2004
+ chatType: target.chatType,
2005
+ replyToMessageId: opts.replyToMessageId,
2006
+ quoteText: opts.quoteText
2007
+ });
2008
+ const hasThreadParams = Object.keys(threadParams).length > 0;
2009
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2010
+ requestWithDiag: createTelegramNonIdempotentRequestWithDiag({
2011
+ cfg,
2012
+ account,
2013
+ retry: opts.retry,
2014
+ verbose: opts.verbose
2015
+ }),
2016
+ chatId,
2017
+ input: to
2018
+ });
2019
+ const textMode = opts.textMode ?? "markdown";
2020
+ const tableMode = resolveMarkdownTableMode({
2021
+ cfg,
2022
+ channel: "telegram",
2023
+ accountId: account.accountId
2024
+ });
2025
+ const renderHtmlText = (value) => renderTelegramHtmlText(value, {
2026
+ textMode,
2027
+ tableMode
2028
+ });
2029
+ const linkPreviewOptions = account.config.linkPreview ?? true ? void 0 : { is_disabled: true };
2030
+ const sendTelegramText = async (rawText, params, fallbackText) => {
2031
+ return await withTelegramThreadFallback(params, "message", opts.verbose, async (effectiveParams, label) => {
2032
+ const htmlText = renderHtmlText(rawText);
2033
+ const baseParams = effectiveParams ? { ...effectiveParams } : {};
2034
+ if (linkPreviewOptions) baseParams.link_preview_options = linkPreviewOptions;
2035
+ const hasBaseParams = Object.keys(baseParams).length > 0;
2036
+ const sendParams = {
2037
+ parse_mode: "HTML",
2038
+ ...baseParams,
2039
+ ...opts.silent === true ? { disable_notification: true } : {}
2040
+ };
2041
+ return await withTelegramHtmlParseFallback({
2042
+ label,
2043
+ verbose: opts.verbose,
2044
+ requestHtml: (retryLabel) => requestWithChatNotFound(() => api.sendMessage(chatId, htmlText, sendParams), retryLabel),
2045
+ requestPlain: (retryLabel) => {
2046
+ const plainParams = hasBaseParams ? baseParams : void 0;
2047
+ return requestWithChatNotFound(() => plainParams ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) : api.sendMessage(chatId, fallbackText ?? rawText), retryLabel);
2048
+ }
2049
+ });
2050
+ });
2051
+ };
2052
+ if (mediaUrl) {
2053
+ const media = await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({
2054
+ maxBytes: mediaMaxBytes,
2055
+ mediaLocalRoots: opts.mediaLocalRoots
2056
+ }));
2057
+ const kind = kindFromMime(media.contentType ?? void 0);
2058
+ const isGif = isGifMedia({
2059
+ contentType: media.contentType,
2060
+ fileName: media.fileName
2061
+ });
2062
+ const isVideoNote = kind === "video" && opts.asVideoNote === true;
2063
+ const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind ?? "document")) ?? "file";
2064
+ const file = new InputFile(media.buffer, fileName);
2065
+ let caption;
2066
+ let followUpText;
2067
+ if (isVideoNote) {
2068
+ caption = void 0;
2069
+ followUpText = text.trim() ? text : void 0;
2070
+ } else {
2071
+ const split = splitTelegramCaption(text);
2072
+ caption = split.caption;
2073
+ followUpText = split.followUpText;
2074
+ }
2075
+ const htmlCaption = caption ? renderHtmlText(caption) : void 0;
2076
+ const needsSeparateText = Boolean(followUpText);
2077
+ const baseMediaParams = {
2078
+ ...hasThreadParams ? threadParams : {},
2079
+ ...!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}
2080
+ };
2081
+ const mediaParams = {
2082
+ ...htmlCaption ? {
2083
+ caption: htmlCaption,
2084
+ parse_mode: "HTML"
2085
+ } : {},
2086
+ ...baseMediaParams,
2087
+ ...opts.silent === true ? { disable_notification: true } : {}
2088
+ };
2089
+ const sendMedia = async (label, sender) => await withTelegramThreadFallback(mediaParams, label, opts.verbose, async (effectiveParams, retryLabel) => requestWithChatNotFound(() => sender(effectiveParams), retryLabel));
2090
+ const mediaSender = (() => {
2091
+ if (isGif) return {
2092
+ label: "animation",
2093
+ sender: (effectiveParams) => api.sendAnimation(chatId, file, effectiveParams)
2094
+ };
2095
+ if (kind === "image") return {
2096
+ label: "photo",
2097
+ sender: (effectiveParams) => api.sendPhoto(chatId, file, effectiveParams)
2098
+ };
2099
+ if (kind === "video") {
2100
+ if (isVideoNote) return {
2101
+ label: "video_note",
2102
+ sender: (effectiveParams) => api.sendVideoNote(chatId, file, effectiveParams)
2103
+ };
2104
+ return {
2105
+ label: "video",
2106
+ sender: (effectiveParams) => api.sendVideo(chatId, file, effectiveParams)
2107
+ };
2108
+ }
2109
+ if (kind === "audio") {
2110
+ const { useVoice } = resolveTelegramVoiceSend({
2111
+ wantsVoice: opts.asVoice === true,
2112
+ contentType: media.contentType,
2113
+ fileName,
2114
+ logFallback: logVerbose
2115
+ });
2116
+ if (useVoice) return {
2117
+ label: "voice",
2118
+ sender: (effectiveParams) => api.sendVoice(chatId, file, effectiveParams)
2119
+ };
2120
+ return {
2121
+ label: "audio",
2122
+ sender: (effectiveParams) => api.sendAudio(chatId, file, effectiveParams)
2123
+ };
2124
+ }
2125
+ return {
2126
+ label: "document",
2127
+ sender: (effectiveParams) => api.sendDocument(chatId, file, effectiveParams)
2128
+ };
2129
+ })();
2130
+ const result = await sendMedia(mediaSender.label, mediaSender.sender);
2131
+ const mediaMessageId = resolveTelegramMessageIdOrThrow(result, "media send");
2132
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2133
+ recordSentMessage(chatId, mediaMessageId);
2134
+ recordChannelActivity({
2135
+ channel: "telegram",
2136
+ accountId: account.accountId,
2137
+ direction: "outbound"
2138
+ });
2139
+ if (needsSeparateText && followUpText) {
2140
+ const textParams = hasThreadParams || replyMarkup ? {
2141
+ ...threadParams,
2142
+ ...replyMarkup ? { reply_markup: replyMarkup } : {}
2143
+ } : void 0;
2144
+ const textMessageId = resolveTelegramMessageIdOrThrow(await sendTelegramText(followUpText, textParams), "text follow-up send");
2145
+ recordSentMessage(chatId, textMessageId);
2146
+ return {
2147
+ messageId: String(textMessageId),
2148
+ chatId: resolvedChatId
2149
+ };
2150
+ }
2151
+ return {
2152
+ messageId: String(mediaMessageId),
2153
+ chatId: resolvedChatId
2154
+ };
2155
+ }
2156
+ if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
2157
+ const res = await sendTelegramText(text, hasThreadParams || replyMarkup ? {
2158
+ ...threadParams,
2159
+ ...replyMarkup ? { reply_markup: replyMarkup } : {}
2160
+ } : void 0, opts.plainText);
2161
+ const messageId = resolveTelegramMessageIdOrThrow(res, "text send");
2162
+ recordSentMessage(chatId, messageId);
2163
+ recordChannelActivity({
2164
+ channel: "telegram",
2165
+ accountId: account.accountId,
2166
+ direction: "outbound"
2167
+ });
2168
+ return {
2169
+ messageId: String(messageId),
2170
+ chatId: String(res?.chat?.id ?? chatId)
2171
+ };
2172
+ }
2173
+ async function sendTypingTelegram(to, opts = {}) {
2174
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2175
+ const target = parseTelegramTarget(to);
2176
+ const chatId = await resolveAndPersistChatId({
2177
+ cfg,
2178
+ api,
2179
+ lookupTarget: target.chatId,
2180
+ persistTarget: to,
2181
+ verbose: opts.verbose
2182
+ });
2183
+ const requestWithDiag = createTelegramRequestWithDiag({
2184
+ cfg,
2185
+ account,
2186
+ retry: opts.retry,
2187
+ verbose: opts.verbose,
2188
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2189
+ });
2190
+ const threadParams = buildTypingThreadParams(target.messageThreadId ?? opts.messageThreadId);
2191
+ await requestWithDiag(() => api.sendChatAction(chatId, "typing", threadParams), "typing");
2192
+ return { ok: true };
2193
+ }
2194
+ async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, opts = {}) {
2195
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2196
+ const rawTarget = String(chatIdInput);
2197
+ const chatId = await resolveAndPersistChatId({
2198
+ cfg,
2199
+ api,
2200
+ lookupTarget: rawTarget,
2201
+ persistTarget: rawTarget,
2202
+ verbose: opts.verbose
2203
+ });
2204
+ const messageId = normalizeMessageId(messageIdInput);
2205
+ const requestWithDiag = createTelegramRequestWithDiag({
2206
+ cfg,
2207
+ account,
2208
+ retry: opts.retry,
2209
+ verbose: opts.verbose,
2210
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2211
+ });
2212
+ const remove = opts.remove === true;
2213
+ const trimmedEmoji = emoji.trim();
2214
+ const reactions = remove || !trimmedEmoji ? [] : [{
2215
+ type: "emoji",
2216
+ emoji: trimmedEmoji
2217
+ }];
2218
+ if (typeof api.setMessageReaction !== "function") throw new Error("Telegram reactions are unavailable in this bot API.");
2219
+ try {
2220
+ await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
2221
+ } catch (err) {
2222
+ const msg = err instanceof Error ? err.message : String(err);
2223
+ if (/REACTION_INVALID/i.test(msg)) return {
2224
+ ok: false,
2225
+ warning: `Reaction unavailable: ${trimmedEmoji}`
2226
+ };
2227
+ throw err;
2228
+ }
2229
+ return { ok: true };
2230
+ }
2231
+ async function deleteMessageTelegram(chatIdInput, messageIdInput, opts = {}) {
2232
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2233
+ const rawTarget = String(chatIdInput);
2234
+ const chatId = await resolveAndPersistChatId({
2235
+ cfg,
2236
+ api,
2237
+ lookupTarget: rawTarget,
2238
+ persistTarget: rawTarget,
2239
+ verbose: opts.verbose
2240
+ });
2241
+ const messageId = normalizeMessageId(messageIdInput);
2242
+ await createTelegramRequestWithDiag({
2243
+ cfg,
2244
+ account,
2245
+ retry: opts.retry,
2246
+ verbose: opts.verbose,
2247
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2248
+ })(() => api.deleteMessage(chatId, messageId), "deleteMessage");
2249
+ logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
2250
+ return { ok: true };
2251
+ }
2252
+ async function editMessageReplyMarkupTelegram(chatIdInput, messageIdInput, buttons, opts = {}) {
2253
+ const { cfg, account, api } = resolveTelegramApiContext({
2254
+ ...opts,
2255
+ cfg: opts.cfg
2256
+ });
2257
+ const rawTarget = String(chatIdInput);
2258
+ const chatId = await resolveAndPersistChatId({
2259
+ cfg,
2260
+ api,
2261
+ lookupTarget: rawTarget,
2262
+ persistTarget: rawTarget,
2263
+ verbose: opts.verbose
2264
+ });
2265
+ const messageId = normalizeMessageId(messageIdInput);
2266
+ const requestWithDiag = createTelegramRequestWithDiag({
2267
+ cfg,
2268
+ account,
2269
+ retry: opts.retry,
2270
+ verbose: opts.verbose
2271
+ });
2272
+ const replyMarkup = buildInlineKeyboard(buttons) ?? { inline_keyboard: [] };
2273
+ try {
2274
+ await requestWithDiag(() => api.editMessageReplyMarkup(chatId, messageId, { reply_markup: replyMarkup }), "editMessageReplyMarkup", { shouldLog: (err) => !isTelegramMessageNotModifiedError(err) });
2275
+ } catch (err) {
2276
+ if (!isTelegramMessageNotModifiedError(err)) throw err;
2277
+ }
2278
+ logVerbose(`[telegram] Edited reply markup for message ${messageId} in chat ${chatId}`);
2279
+ return {
2280
+ ok: true,
2281
+ messageId: String(messageId),
2282
+ chatId
2283
+ };
2284
+ }
2285
+ async function editMessageTelegram(chatIdInput, messageIdInput, text, opts = {}) {
2286
+ const { cfg, account, api } = resolveTelegramApiContext({
2287
+ ...opts,
2288
+ cfg: opts.cfg
2289
+ });
2290
+ const rawTarget = String(chatIdInput);
2291
+ const chatId = await resolveAndPersistChatId({
2292
+ cfg,
2293
+ api,
2294
+ lookupTarget: rawTarget,
2295
+ persistTarget: rawTarget,
2296
+ verbose: opts.verbose
2297
+ });
2298
+ const messageId = normalizeMessageId(messageIdInput);
2299
+ const requestWithDiag = createTelegramRequestWithDiag({
2300
+ cfg,
2301
+ account,
2302
+ retry: opts.retry,
2303
+ verbose: opts.verbose
2304
+ });
2305
+ const requestWithEditShouldLog = (fn, label, shouldLog) => requestWithDiag(fn, label, shouldLog ? { shouldLog } : void 0);
2306
+ const htmlText = renderTelegramHtmlText(text, {
2307
+ textMode: opts.textMode ?? "markdown",
2308
+ tableMode: resolveMarkdownTableMode({
2309
+ cfg,
2310
+ channel: "telegram",
2311
+ accountId: account.accountId
2312
+ })
2313
+ });
2314
+ const shouldTouchButtons = opts.buttons !== void 0;
2315
+ const builtKeyboard = shouldTouchButtons ? buildInlineKeyboard(opts.buttons) : void 0;
2316
+ const replyMarkup = shouldTouchButtons ? builtKeyboard ?? { inline_keyboard: [] } : void 0;
2317
+ const editParams = { parse_mode: "HTML" };
2318
+ if (opts.linkPreview === false) editParams.link_preview_options = { is_disabled: true };
2319
+ if (replyMarkup !== void 0) editParams.reply_markup = replyMarkup;
2320
+ const plainParams = {};
2321
+ if (opts.linkPreview === false) plainParams.link_preview_options = { is_disabled: true };
2322
+ if (replyMarkup !== void 0) plainParams.reply_markup = replyMarkup;
2323
+ try {
2324
+ await withTelegramHtmlParseFallback({
2325
+ label: "editMessage",
2326
+ verbose: opts.verbose,
2327
+ requestHtml: (retryLabel) => requestWithEditShouldLog(() => api.editMessageText(chatId, messageId, htmlText, editParams), retryLabel, (err) => !isTelegramMessageNotModifiedError(err)),
2328
+ requestPlain: (retryLabel) => requestWithEditShouldLog(() => Object.keys(plainParams).length > 0 ? api.editMessageText(chatId, messageId, text, plainParams) : api.editMessageText(chatId, messageId, text), retryLabel, (plainErr) => !isTelegramMessageNotModifiedError(plainErr))
2329
+ });
2330
+ } catch (err) {
2331
+ if (isTelegramMessageNotModifiedError(err)) {} else throw err;
2332
+ }
2333
+ logVerbose(`[telegram] Edited message ${messageId} in chat ${chatId}`);
2334
+ return {
2335
+ ok: true,
2336
+ messageId: String(messageId),
2337
+ chatId
2338
+ };
2339
+ }
2340
+ function inferFilename(kind) {
2341
+ switch (kind) {
2342
+ case "image": return "image.jpg";
2343
+ case "video": return "video.mp4";
2344
+ case "audio": return "audio.ogg";
2345
+ default: return "file.bin";
2346
+ }
2347
+ }
2348
+ /**
2349
+ * Send a sticker to a Telegram chat by file_id.
2350
+ * @param to - Chat ID or username (e.g., "123456789" or "@username")
2351
+ * @param fileId - Telegram file_id of the sticker to send
2352
+ * @param opts - Optional configuration
2353
+ */
2354
+ async function sendStickerTelegram(to, fileId, opts = {}) {
2355
+ if (!fileId?.trim()) throw new Error("Telegram sticker file_id is required");
2356
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2357
+ const target = parseTelegramTarget(to);
2358
+ const chatId = await resolveAndPersistChatId({
2359
+ cfg,
2360
+ api,
2361
+ lookupTarget: target.chatId,
2362
+ persistTarget: to,
2363
+ verbose: opts.verbose
2364
+ });
2365
+ const threadParams = buildTelegramThreadReplyParams({
2366
+ targetMessageThreadId: target.messageThreadId,
2367
+ messageThreadId: opts.messageThreadId,
2368
+ chatType: target.chatType,
2369
+ replyToMessageId: opts.replyToMessageId
2370
+ });
2371
+ const hasThreadParams = Object.keys(threadParams).length > 0;
2372
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2373
+ requestWithDiag: createTelegramRequestWithDiag({
2374
+ cfg,
2375
+ account,
2376
+ retry: opts.retry,
2377
+ verbose: opts.verbose,
2378
+ useApiErrorLogging: false
2379
+ }),
2380
+ chatId,
2381
+ input: to
2382
+ });
2383
+ const result = await withTelegramThreadFallback(hasThreadParams ? threadParams : void 0, "sticker", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label));
2384
+ const messageId = resolveTelegramMessageIdOrThrow(result, "sticker send");
2385
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2386
+ recordSentMessage(chatId, messageId);
2387
+ recordChannelActivity({
2388
+ channel: "telegram",
2389
+ accountId: account.accountId,
2390
+ direction: "outbound"
2391
+ });
2392
+ return {
2393
+ messageId: String(messageId),
2394
+ chatId: resolvedChatId
2395
+ };
2396
+ }
2397
+ /**
2398
+ * Send a poll to a Telegram chat.
2399
+ * @param to - Chat ID or username (e.g., "123456789" or "@username")
2400
+ * @param poll - Poll input with question, options, maxSelections, and optional durationHours
2401
+ * @param opts - Optional configuration
2402
+ */
2403
+ async function sendPollTelegram(to, poll, opts = {}) {
2404
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2405
+ const target = parseTelegramTarget(to);
2406
+ const chatId = await resolveAndPersistChatId({
2407
+ cfg,
2408
+ api,
2409
+ lookupTarget: target.chatId,
2410
+ persistTarget: to,
2411
+ verbose: opts.verbose
2412
+ });
2413
+ const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
2414
+ const threadParams = buildTelegramThreadReplyParams({
2415
+ targetMessageThreadId: target.messageThreadId,
2416
+ messageThreadId: opts.messageThreadId,
2417
+ chatType: target.chatType,
2418
+ replyToMessageId: opts.replyToMessageId
2419
+ });
2420
+ const pollOptions = normalizedPoll.options;
2421
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2422
+ requestWithDiag: createTelegramNonIdempotentRequestWithDiag({
2423
+ cfg,
2424
+ account,
2425
+ retry: opts.retry,
2426
+ verbose: opts.verbose
2427
+ }),
2428
+ chatId,
2429
+ input: to
2430
+ });
2431
+ const durationSeconds = normalizedPoll.durationSeconds;
2432
+ if (durationSeconds === void 0 && normalizedPoll.durationHours !== void 0) throw new Error("Telegram poll durationHours is not supported. Use durationSeconds (5-600) instead.");
2433
+ if (durationSeconds !== void 0 && (durationSeconds < 5 || durationSeconds > 600)) throw new Error("Telegram poll durationSeconds must be between 5 and 600");
2434
+ const result = await withTelegramThreadFallback({
2435
+ allows_multiple_answers: normalizedPoll.maxSelections > 1,
2436
+ is_anonymous: opts.isAnonymous ?? true,
2437
+ ...durationSeconds !== void 0 ? { open_period: durationSeconds } : {},
2438
+ ...Object.keys(threadParams).length > 0 ? threadParams : {},
2439
+ ...opts.silent === true ? { disable_notification: true } : {}
2440
+ }, "poll", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendPoll(chatId, normalizedPoll.question, pollOptions, effectiveParams), label));
2441
+ const messageId = resolveTelegramMessageIdOrThrow(result, "poll send");
2442
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2443
+ const pollId = result?.poll?.id;
2444
+ recordSentMessage(chatId, messageId);
2445
+ recordChannelActivity({
2446
+ channel: "telegram",
2447
+ accountId: account.accountId,
2448
+ direction: "outbound"
2449
+ });
2450
+ return {
2451
+ messageId: String(messageId),
2452
+ chatId: resolvedChatId,
2453
+ pollId
2454
+ };
2455
+ }
2456
+ /**
2457
+ * Create a forum topic in a Telegram supergroup.
2458
+ * Requires the bot to have `can_manage_topics` permission.
2459
+ *
2460
+ * @param chatId - Supergroup chat ID
2461
+ * @param name - Topic name (1-128 characters)
2462
+ * @param opts - Optional configuration
2463
+ */
2464
+ async function createForumTopicTelegram(chatId, name, opts = {}) {
2465
+ if (!name?.trim()) throw new Error("Forum topic name is required");
2466
+ const trimmedName = name.trim();
2467
+ if (trimmedName.length > 128) throw new Error("Forum topic name must be 128 characters or fewer");
2468
+ const cfg = loadConfig();
2469
+ const account = resolveTelegramAccount({
2470
+ cfg,
2471
+ accountId: opts.accountId
2472
+ });
2473
+ const token = resolveToken(opts.token, account);
2474
+ const client = resolveTelegramClientOptions(account);
2475
+ const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
2476
+ const normalizedChatId = await resolveAndPersistChatId({
2477
+ cfg,
2478
+ api,
2479
+ lookupTarget: parseTelegramTarget(chatId).chatId,
2480
+ persistTarget: chatId,
2481
+ verbose: opts.verbose
2482
+ });
2483
+ const requestWithDiag = createTelegramNonIdempotentRequestWithDiag({
2484
+ cfg,
2485
+ account,
2486
+ retry: opts.retry,
2487
+ verbose: opts.verbose
2488
+ });
2489
+ const extra = {};
2490
+ if (opts.iconColor != null) extra.icon_color = opts.iconColor;
2491
+ if (opts.iconCustomEmojiId?.trim()) extra.icon_custom_emoji_id = opts.iconCustomEmojiId.trim();
2492
+ const hasExtra = Object.keys(extra).length > 0;
2493
+ const result = await requestWithDiag(() => api.createForumTopic(normalizedChatId, trimmedName, hasExtra ? extra : void 0), "createForumTopic");
2494
+ const topicId = result.message_thread_id;
2495
+ recordChannelActivity({
2496
+ channel: "telegram",
2497
+ accountId: account.accountId,
2498
+ direction: "outbound"
2499
+ });
2500
+ return {
2501
+ topicId,
2502
+ name: result.name ?? trimmedName,
2503
+ chatId: normalizedChatId
2504
+ };
2505
+ }
2506
+ //#endregion
2507
+ export { readChannelAllowFromStoreSync as $, buildTypingThreadParams as A, resolveTelegramMediaPlaceholder as B, buildGroupLabel as C, buildTelegramGroupPeerId as D, buildTelegramGroupFrom as E, hasBotMention as F, normalizeAllowFrom as G, resolveTelegramStreamMode as H, normalizeForwardedContext as I, formatLocationText as J, normalizeDmAllowFromWithStore as K, resolveTelegramDirectPeerId as L, expandTextLinks as M, extractTelegramLocation as N, buildTelegramParentPeer as O, getTelegramTextParts as P, readChannelAllowFromStore as Q, resolveTelegramForumThreadId as R, withTelegramApiErrorLogging as S, buildSenderName as T, resolveTelegramThreadSpec as U, resolveTelegramReplyId as V, isSenderAllowed as W, isVoiceCompatibleAudio as X, toLocationContext as Y, addChannelAllowFromStoreEntry as Z, markdownToTelegramChunks as _, editMessageTelegram as a, listPairingChannels as at, wrapFileReferencesInHtml as b, sendPollTelegram as c, mergeDmAllowFromSources as ct, resolveTelegramVoiceSend as d, removeChannelAllowFromStoreEntry as et, loadCronStore as f, isSafeToRetrySendError as g, isRecoverableTelegramNetworkError as h, editMessageReplyMarkupTelegram as i, getPairingAdapter as it, describeReplyTarget as j, buildTelegramThreadParams as k, sendStickerTelegram as l, resolveGroupAllowFromSources as lt, wasSentByBot as m, createForumTopicTelegram as n, readJsonFileWithFallback as nt, reactMessageTelegram as o, firstDefined as ot, resolveCronStorePath as p, resolveSenderAllowMatch as q, deleteMessageTelegram as r, writeJsonFileAtomically as rt, sendMessageTelegram as s, isSenderIdAllowed as st, buildInlineKeyboard as t, upsertChannelPairingRequest as tt, sendTypingTelegram as u, markdownToTelegramHtml as v, buildSenderLabel as w, splitTelegramCaption as x, renderTelegramHtmlText as y, resolveTelegramGroupAllowFromContext as z };