@capybara-hq/plugin-telegram 0.0.0-stage → 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1263 @@
1
+ import { createRequire as __capybaraCreateRequire } from "node:module";
2
+ import { fileURLToPath as __capybaraFileURLToPath } from "node:url";
3
+ import { dirname as __capybaraPathDirname } from "node:path";
4
+ const require = __capybaraCreateRequire(import.meta.url);
5
+ const __capybara_filename = __capybaraFileURLToPath(import.meta.url);
6
+ const __capybara_dirname = __capybaraPathDirname(__capybara_filename);
7
+
8
+ // src/index.ts
9
+ import { CHANNEL_EXTENSION_POINT, defineExtension } from "@capybara-hq/plugin-sdk";
10
+
11
+ // src/channel.ts
12
+ import {
13
+ ChannelSendError as ChannelSendError2
14
+ } from "@capybara-hq/plugin-sdk";
15
+
16
+ // src/actions.ts
17
+ import { randomBytes } from "node:crypto";
18
+
19
+ // src/config.ts
20
+ import { secret, ui } from "@capybara-hq/plugin-sdk";
21
+ import { z } from "zod";
22
+ var TelegramDeployment = z.object({
23
+ apiUrl: ui(z.string().url().default("https://api.telegram.org"), {
24
+ label: { zh: "API 地址", en: "API URL" }
25
+ })
26
+ });
27
+ var TelegramConnection = z.object({
28
+ botToken: ui(
29
+ secret(
30
+ z.string().regex(/^\d+:[A-Za-z0-9_-]+$/, "must be a BotFather bot token such as 123456:ABC-DEF…")
31
+ ),
32
+ {
33
+ label: { zh: "机器人令牌", en: "Bot token" },
34
+ help: {
35
+ zh: "在 @BotFather 中创建机器人后获得,形如 123456:ABC-DEF…。",
36
+ en: "From @BotFather when you create the bot, such as 123456:ABC-DEF…."
37
+ },
38
+ placeholder: "123456:ABC-DEF…"
39
+ }
40
+ ),
41
+ webhookSecret: ui(
42
+ secret(
43
+ z.string().regex(/^[A-Za-z0-9_-]{1,256}$/, "must be 1-256 characters of A-Z, a-z, 0-9, _ and -").optional()
44
+ ),
45
+ {
46
+ label: { zh: "Webhook 密钥", en: "Webhook secret" },
47
+ help: {
48
+ zh: "仅 webhook 模式需要:调用 setWebhook 时作为 secret_token 传入的同一个值;长轮询留空。",
49
+ en: "Only needed in webhook mode: the same value you pass as secret_token to setWebhook. Leave empty for long polling."
50
+ }
51
+ }
52
+ )
53
+ });
54
+ var botIdOf = (token) => token.slice(0, token.indexOf(":"));
55
+ var SENDER_TENANT = "global";
56
+ var connectionTenant = (botId) => `bot${botId}`;
57
+
58
+ // src/threads.ts
59
+ var RECORD_TTL_SECONDS = 30 * 24 * 60 * 60;
60
+ function threadStore(kv, botId) {
61
+ const threadKey = (chatId, messageId) => `thread/${botId}/${chatId}:${messageId}`;
62
+ const topicKey = (threadId) => `topic/${botId}/${threadId}`;
63
+ return {
64
+ async threadOfMessage(chatId, messageId) {
65
+ return await kv?.get(threadKey(chatId, messageId)) ?? void 0;
66
+ },
67
+ async recordSent(chatId, messageId, threadId) {
68
+ await kv?.set(threadKey(chatId, messageId), threadId, { ttlSeconds: RECORD_TTL_SECONDS });
69
+ },
70
+ async topicOf(threadId) {
71
+ const raw = await kv?.get(topicKey(threadId));
72
+ if (raw === null || raw === void 0) return void 0;
73
+ const n = Number(raw);
74
+ return Number.isSafeInteger(n) ? n : void 0;
75
+ },
76
+ async recordTopic(threadId, messageThreadId) {
77
+ await kv?.set(topicKey(threadId), String(messageThreadId), {
78
+ ttlSeconds: RECORD_TTL_SECONDS
79
+ });
80
+ }
81
+ };
82
+ }
83
+
84
+ // src/actions.ts
85
+ var CALLBACK_PREFIX = "cb:";
86
+ var TOKEN_RE = /^[A-Za-z0-9_-]{16}$/;
87
+ var ACTION_JSON_MAX = 8e3;
88
+ var nonEmpty = (x) => typeof x === "string" && x !== "";
89
+ var SCOPES = /* @__PURE__ */ new Set(["once", "session", "always"]);
90
+ function decodeCardAction(json) {
91
+ if (typeof json !== "string" || json.length === 0 || json.length > ACTION_JSON_MAX) return null;
92
+ let v;
93
+ try {
94
+ v = JSON.parse(json);
95
+ } catch {
96
+ return null;
97
+ }
98
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
99
+ const o = v;
100
+ if (!nonEmpty(o.cardId)) return null;
101
+ const cardId = o.cardId;
102
+ if (o.cardKind === "approval") {
103
+ if (o.answer !== void 0) return null;
104
+ if (o.action === "approve" && typeof o.scope === "string" && SCOPES.has(o.scope)) {
105
+ return {
106
+ cardKind: "approval",
107
+ cardId,
108
+ action: "approve",
109
+ scope: o.scope
110
+ };
111
+ }
112
+ if (o.action === "deny" && o.scope === void 0) {
113
+ return { cardKind: "approval", cardId, action: "deny" };
114
+ }
115
+ return null;
116
+ }
117
+ if (o.cardKind === "question") {
118
+ if (o.action !== "answer" || o.scope !== void 0) return null;
119
+ const a = o.answer;
120
+ if (typeof a !== "object" || a === null || Array.isArray(a)) return null;
121
+ const entries = Object.entries(a);
122
+ if (entries.length === 0) return null;
123
+ for (const [k, val] of entries) {
124
+ if (k === "") return null;
125
+ const ok = typeof val === "string" || typeof val === "boolean" || typeof val === "number" && Number.isFinite(val) || Array.isArray(val) && val.every((x) => typeof x === "string");
126
+ if (!ok) return null;
127
+ }
128
+ return {
129
+ cardKind: "question",
130
+ cardId,
131
+ action: "answer",
132
+ answer: a
133
+ };
134
+ }
135
+ return null;
136
+ }
137
+ function cardTokenStore(kv, botId) {
138
+ const key = (token) => `card/${botId}/${token}`;
139
+ return {
140
+ async put(a) {
141
+ if (!kv) throw new Error("no kv available for card tokens");
142
+ const token = randomBytes(12).toString("base64url");
143
+ await kv.set(key(token), JSON.stringify(a), { ttlSeconds: RECORD_TTL_SECONDS });
144
+ return token;
145
+ },
146
+ async take(token) {
147
+ if (!kv || !TOKEN_RE.test(token)) return null;
148
+ const raw = await kv.get(key(token));
149
+ return raw === null || raw === void 0 ? null : decodeCardAction(raw);
150
+ }
151
+ };
152
+ }
153
+ var obj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
154
+ var int = (v) => typeof v === "number" && Number.isSafeInteger(v) ? v : void 0;
155
+ var str = (v) => typeof v === "string" && v !== "" ? v : void 0;
156
+ async function parseCallback(update, tokens, threads) {
157
+ const q = obj(obj(update)?.callback_query);
158
+ const id = str(q?.id);
159
+ const data = str(q?.data);
160
+ const from = obj(q?.from);
161
+ const message = obj(q?.message);
162
+ const chat = obj(message?.chat);
163
+ const userId = int(from?.id);
164
+ const chatId = int(chat?.id);
165
+ const messageId = int(message?.message_id);
166
+ if (!q || !id || !data || !from || !chat || !messageId || userId === void 0) return null;
167
+ if (chatId === void 0 || from.is_bot === true) return null;
168
+ if (!data.startsWith(CALLBACK_PREFIX) || data.length > 64) return null;
169
+ const chatType = chat.type;
170
+ const dm = chatType === "private";
171
+ if (!dm && chatType !== "group" && chatType !== "supergroup") return null;
172
+ const action = await tokens.take(data.slice(CALLBACK_PREFIX.length));
173
+ if (!action) return null;
174
+ const chatKey = String(chatId);
175
+ const displayName = [str(from.first_name), str(from.last_name)].filter(Boolean).join(" ").trim();
176
+ const threadId = dm ? void 0 : await threads.threadOfMessage(chatKey, messageId);
177
+ const event = {
178
+ type: "card_action",
179
+ messageId: `${chatKey}:${messageId}`,
180
+ eventId: id,
181
+ target: dm ? { chatId: chatKey, chatType: "dm" } : { chatId: chatKey, chatType: "group", ...threadId !== void 0 && { threadId } },
182
+ sender: {
183
+ externalId: String(userId),
184
+ tenantId: SENDER_TENANT,
185
+ ...displayName !== "" && { displayName }
186
+ },
187
+ action
188
+ };
189
+ return { event, callbackQueryId: id };
190
+ }
191
+
192
+ // src/api.ts
193
+ var TelegramApiError = class extends Error {
194
+ name = "TelegramApiError";
195
+ method;
196
+ status;
197
+ errorCode;
198
+ description;
199
+ retryAfterSec;
200
+ migrateToChatId;
201
+ network;
202
+ constructor(method, init) {
203
+ super(`Telegram ${method} failed: ${detail(init)}`);
204
+ this.method = method;
205
+ this.network = init.network === true || init.timeout === true;
206
+ if (init.status !== void 0) this.status = init.status;
207
+ if (init.errorCode !== void 0) this.errorCode = init.errorCode;
208
+ if (init.description !== void 0) this.description = init.description;
209
+ if (init.retryAfterSec !== void 0) this.retryAfterSec = init.retryAfterSec;
210
+ if (init.migrateToChatId !== void 0) this.migrateToChatId = init.migrateToChatId;
211
+ }
212
+ };
213
+ function detail(init) {
214
+ if (init.timeout) return "timeout";
215
+ if (init.network) return "network error";
216
+ const code = init.errorCode !== void 0 ? String(init.errorCode) : init.status !== void 0 ? `HTTP ${init.status}` : "unknown error";
217
+ return init.description ? `${code} ${init.description}` : code;
218
+ }
219
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
220
+ function telegramApi(o) {
221
+ const base = o.apiUrl.replace(/\/+$/, "");
222
+ const timeoutMs = o.timeoutMs ?? 15e3;
223
+ return {
224
+ async call(method, params, signal) {
225
+ const timer = AbortSignal.timeout(timeoutMs);
226
+ let res;
227
+ let text;
228
+ try {
229
+ res = await fetch(`${base}/bot${o.token}/${method}`, {
230
+ method: "POST",
231
+ headers: { "content-type": "application/json" },
232
+ body: JSON.stringify(params ?? {}),
233
+ signal: signal ? AbortSignal.any([signal, timer]) : timer
234
+ });
235
+ text = await res.text();
236
+ } catch {
237
+ signal?.throwIfAborted();
238
+ throw new TelegramApiError(method, { network: true, timeout: timer.aborted });
239
+ }
240
+ let body;
241
+ try {
242
+ body = JSON.parse(text);
243
+ } catch {
244
+ body = void 0;
245
+ }
246
+ if (res.ok && body?.ok === true) return body.result;
247
+ const errorCode = num(body?.error_code);
248
+ const description = typeof body?.description === "string" ? body.description : void 0;
249
+ const retryAfterSec = num(body?.parameters?.retry_after);
250
+ const migrateToChatId = num(body?.parameters?.migrate_to_chat_id);
251
+ throw new TelegramApiError(method, {
252
+ status: res.status,
253
+ ...errorCode !== void 0 && { errorCode },
254
+ ...description !== void 0 && { description },
255
+ ...retryAfterSec !== void 0 && { retryAfterSec },
256
+ ...migrateToChatId !== void 0 && { migrateToChatId }
257
+ });
258
+ }
259
+ };
260
+ }
261
+
262
+ // src/cards.ts
263
+ import {
264
+ questionRenderableInline,
265
+ renderCardAsText
266
+ } from "@capybara-hq/plugin-sdk";
267
+
268
+ // src/html.ts
269
+ var TEXT_LIMIT = 4096;
270
+ function escapeHtml(s) {
271
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
272
+ }
273
+ var escapeAttr = (s) => escapeHtml(s).replace(/"/g, "&quot;");
274
+ var htmlLink = (url) => `<a href="${escapeAttr(url)}">${escapeHtml(url)}</a>`;
275
+ var WORD_JOINER = "⁠";
276
+ function neutraliseHtmlMentions(html) {
277
+ return html.replace(
278
+ /(<pre[\s\S]*?<\/pre>)|(<code>[\s\S]*?<\/code>)|(<a [^>]*>)|(https?:\/\/[^\s<]+)|(?<![A-Za-z0-9_])@(?=(?:<[^>]+>)*[A-Za-z0-9_])/g,
279
+ (m, pre, code, a, url) => pre || code || a ? m : url ? url.replace(/(?<![A-Za-z0-9_/])@(?=[A-Za-z0-9_])/g, `@${WORD_JOINER}`) : `@${WORD_JOINER}`
280
+ );
281
+ }
282
+ var HOLD = "\0";
283
+ function balanced(s) {
284
+ const stack = [];
285
+ for (const m of s.matchAll(/<(\/?)([a-z-]+)[^>]*>/g)) {
286
+ if (m[1] === "") stack.push(m[2] ?? "");
287
+ else if (stack.pop() !== m[2]) return false;
288
+ }
289
+ return stack.length === 0;
290
+ }
291
+ function wrap(s, re, tag) {
292
+ return s.replace(re, (m, inner) => balanced(inner) ? `<${tag}>${inner}</${tag}>` : m);
293
+ }
294
+ function formatText(seg) {
295
+ let s = escapeHtml(seg);
296
+ s = s.replace(
297
+ /\*\*\*(?=\S)(.*?\S)\*\*\*/g,
298
+ (m, inner) => balanced(inner) ? `<b><i>${inner}</i></b>` : m
299
+ );
300
+ s = wrap(s, /~~(?=\S)(.*?\S)~~/g, "s");
301
+ s = wrap(s, /\*\*(?=\S)(.*?\S)\*\*/g, "b");
302
+ s = wrap(s, /(?<![\w])__(?=\S)(.*?\S)__(?!\w)/g, "b");
303
+ s = wrap(s, /(?<![\w*])\*(?=[^\s*])([^*]*?[^\s*])\*(?![\w*])/g, "i");
304
+ s = wrap(s, /(?<![\w_])_(?=[^\s_])([^_]*?[^\s_])_(?![\w_])/g, "i");
305
+ return s;
306
+ }
307
+ function formatSpan(seg) {
308
+ const held = [];
309
+ const hold = (v) => {
310
+ held.push(v);
311
+ return `${HOLD}${held.length - 1}${HOLD}`;
312
+ };
313
+ const link = (label, url) => hold(`<a href="${escapeAttr(url)}">${formatText(label)}</a>`);
314
+ const s = formatText(
315
+ seg.replace(
316
+ /(?<!!)\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)/g,
317
+ (_m, l, u) => link(l, u)
318
+ ).replace(/<(https?:\/\/[^>\s]+)>/g, (_m, u) => link(u, u))
319
+ );
320
+ return s.replace(
321
+ new RegExp(`${HOLD}(\\d+)${HOLD}`, "g"),
322
+ (_m, i) => held[Number(i)] ?? ""
323
+ );
324
+ }
325
+ function inline(line) {
326
+ let out = "";
327
+ let last = 0;
328
+ for (const m of line.matchAll(/`([^`\n]+)`/g)) {
329
+ out += `${formatSpan(line.slice(last, m.index))}<code>${escapeHtml(m[1] ?? "")}</code>`;
330
+ last = m.index + m[0].length;
331
+ }
332
+ return out + formatSpan(line.slice(last));
333
+ }
334
+ function convertLine(line) {
335
+ const h = /^\s{0,3}#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
336
+ if (h) {
337
+ const body = (h[1] ?? "").replace(/\*\*|__/g, "");
338
+ return body === "" ? "" : `<b>${inline(body)}</b>`;
339
+ }
340
+ const li = /^(\s*)[-*+]\s+(.*)$/.exec(line);
341
+ if (li) return `${li[1]}• ${inline(li[2] ?? "")}`;
342
+ return inline(line);
343
+ }
344
+ function codeBlock(lang, lines) {
345
+ const body = escapeHtml(lines.join("\n"));
346
+ const l = lang.replace(/[^A-Za-z0-9_+#.-]/g, "");
347
+ return l === "" ? `<pre>${body}</pre>` : `<pre><code class="language-${l}">${body}</code></pre>`;
348
+ }
349
+ function markdownToTelegramHtml(md) {
350
+ const lines = md.replace(/\r\n?/g, "\n").replaceAll("\0", "").split("\n");
351
+ const out = [];
352
+ let fence;
353
+ let quote;
354
+ const closeQuote = () => {
355
+ if (quote) out.push(`<blockquote>${quote.join("\n")}</blockquote>`);
356
+ quote = void 0;
357
+ };
358
+ for (const line of lines) {
359
+ if (fence) {
360
+ if (/^\s*(```|~~~)\s*$/.test(line)) {
361
+ out.push(codeBlock(fence.lang, fence.lines));
362
+ fence = void 0;
363
+ } else fence.lines.push(line);
364
+ continue;
365
+ }
366
+ const open = /^\s*(?:```|~~~)\s*([^\s`]*)/.exec(line);
367
+ if (open) {
368
+ closeQuote();
369
+ fence = { lang: open[1] ?? "", lines: [] };
370
+ continue;
371
+ }
372
+ const q = /^\s{0,3}>\s?(.*)$/.exec(line);
373
+ if (q) {
374
+ quote ??= [];
375
+ quote.push(convertLine(q[1] ?? ""));
376
+ continue;
377
+ }
378
+ closeQuote();
379
+ out.push(convertLine(line));
380
+ }
381
+ if (fence) out.push(codeBlock(fence.lang, fence.lines));
382
+ closeQuote();
383
+ return neutraliseHtmlMentions(out.join("\n"));
384
+ }
385
+ function tokenise(html) {
386
+ const tokens = [];
387
+ const re = /<(\/?)([a-z-]+)[^>]*>|&(?:amp|lt|gt|quot);|[\s\S]/gu;
388
+ for (const m of html.matchAll(re)) {
389
+ if (m[2] !== void 0)
390
+ tokens.push({ kind: "tag", text: m[0], name: m[2], close: m[1] === "/" });
391
+ else
392
+ tokens.push({
393
+ kind: "text",
394
+ text: m[0],
395
+ len: m[0].startsWith("&") && m[0].length > 1 ? 1 : m[0].length
396
+ });
397
+ }
398
+ return tokens;
399
+ }
400
+ var openers = (stack) => stack.map((t) => t[0]).join("");
401
+ var closers = (stack) => [...stack].reverse().map((t) => `</${t[1]}>`).join("");
402
+ function visibleLength(html) {
403
+ let n = 0;
404
+ for (const t of tokenise(html)) if (t.kind === "text") n += t.len;
405
+ return n;
406
+ }
407
+ function truncateHtml(html, limit = TEXT_LIMIT) {
408
+ if (visibleLength(html) <= limit) return html;
409
+ const head = splitHtml(html, limit - 1)[0] ?? "";
410
+ const closing = /(?:<\/[a-z-]+>)*$/.exec(head)?.[0] ?? "";
411
+ return `${head.slice(0, head.length - closing.length)}…${closing}`;
412
+ }
413
+ function fitHtml(lead, body, limit = TEXT_LIMIT) {
414
+ const sep = "\n\n";
415
+ if (lead === "") return truncateHtml(body, limit);
416
+ if (body === "") return truncateHtml(lead, limit);
417
+ const room = limit - visibleLength(body) - sep.length;
418
+ if (room >= 2) return `${truncateHtml(lead, room)}${sep}${body}`;
419
+ return truncateHtml(body, limit);
420
+ }
421
+ function splitHtml(html, limit = TEXT_LIMIT) {
422
+ const tokens = tokenise(html);
423
+ const parts = [];
424
+ let cur = [];
425
+ let vis = 0;
426
+ let stack = [];
427
+ let prefix = "";
428
+ let cand;
429
+ const emit = (toks, count, at) => {
430
+ if (count > 0) parts.push(prefix + toks.map((t) => t.text).join("") + closers(at));
431
+ };
432
+ for (const t of tokens) {
433
+ if (t.kind === "tag") {
434
+ if (t.close) {
435
+ const at = stack.findLastIndex((s) => s[1] === t.name);
436
+ if (at >= 0) stack = stack.slice(0, at);
437
+ } else stack = [...stack, [t.text, t.name]];
438
+ cur.push(t);
439
+ continue;
440
+ }
441
+ if (vis + t.len > limit) {
442
+ if (cand && cand.vis >= limit / 2) {
443
+ emit(cur.slice(0, cand.i), cand.vis, cand.stack);
444
+ const rest = cur.slice(cand.i + 1);
445
+ prefix = openers(cand.stack);
446
+ vis = vis - cand.vis - 1;
447
+ cur = rest;
448
+ } else {
449
+ emit(cur, vis, stack);
450
+ prefix = openers(stack);
451
+ vis = 0;
452
+ cur = [];
453
+ }
454
+ cand = void 0;
455
+ }
456
+ if (/^\s$/.test(t.text)) cand = { i: cur.length, vis, stack: [...stack] };
457
+ cur.push(t);
458
+ vis += t.len;
459
+ }
460
+ emit(cur, vis, stack);
461
+ return parts.length > 0 ? parts : [""];
462
+ }
463
+
464
+ // src/cards.ts
465
+ var T = {
466
+ zh: {
467
+ once: "批准一次",
468
+ session: "本会话批准",
469
+ always: "总是批准",
470
+ deny: "拒绝",
471
+ web: "在网页上查看",
472
+ connect: "连接",
473
+ bind: "绑定账号"
474
+ },
475
+ en: {
476
+ once: "Approve once",
477
+ session: "Approve for session",
478
+ always: "Always approve",
479
+ deny: "Deny",
480
+ web: "Open on web",
481
+ connect: "Connect",
482
+ bind: "Link account"
483
+ }
484
+ };
485
+ var LABEL_MAX = 60;
486
+ var clip = (s) => {
487
+ const chars = [...s];
488
+ return chars.length > LABEL_MAX ? `${chars.slice(0, LABEL_MAX - 1).join("")}…` : s;
489
+ };
490
+ function isHttp(v) {
491
+ try {
492
+ const p = new URL(v).protocol;
493
+ return p === "https:" || p === "http:";
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
498
+ var urlButton = (text, url) => isHttp(url) ? [{ text, url }] : [];
499
+ function cardText(card, locale) {
500
+ const url = card.kind === "connect" || card.kind === "bind" ? card.url : card.webUrl;
501
+ const lines = renderCardAsText(card, locale).split("\n");
502
+ const last = lines[lines.length - 1];
503
+ if (lines.length > 1 && last?.endsWith(url)) {
504
+ lines.pop();
505
+ const label = markdownToTelegramHtml(last.slice(0, last.length - url.length));
506
+ return { body: markdownToTelegramHtml(lines.join("\n")), linkLine: `${label}${htmlLink(url)}` };
507
+ }
508
+ return { body: markdownToTelegramHtml(lines.join("\n")) };
509
+ }
510
+ async function callbackButton(b, text, action) {
511
+ try {
512
+ return [{ text: clip(text), callback_data: `${CALLBACK_PREFIX}${await b.tokens.put(action)}` }];
513
+ } catch {
514
+ b.log.warn("telegram: could not store a card button's action; the button is left out", {
515
+ cardKind: action.cardKind,
516
+ cardId: action.cardId
517
+ });
518
+ return [];
519
+ }
520
+ }
521
+ async function keyboard(card, locale, tokens) {
522
+ const t = T[locale];
523
+ switch (card.kind) {
524
+ case "approval": {
525
+ if (card.state !== "pending") return [urlButton(t.web, card.webUrl)];
526
+ const rows = [];
527
+ const decide = [];
528
+ for (const scope of card.scopes) {
529
+ decide.push(
530
+ ...await callbackButton(tokens, t[scope], {
531
+ cardKind: "approval",
532
+ cardId: card.id,
533
+ action: "approve",
534
+ scope
535
+ })
536
+ );
537
+ }
538
+ decide.push(
539
+ ...await callbackButton(tokens, t.deny, {
540
+ cardKind: "approval",
541
+ cardId: card.id,
542
+ action: "deny"
543
+ })
544
+ );
545
+ for (const b of decide) rows.push([b]);
546
+ rows.push(urlButton(t.web, card.webUrl));
547
+ return rows;
548
+ }
549
+ case "question": {
550
+ const f = card.form.fields[0];
551
+ const rows = [];
552
+ if (card.state === "pending" && f && !f.multiSelect && questionRenderableInline(card.form) && "enum" in f.schema) {
553
+ for (const option of f.schema.enum) {
554
+ rows.push(
555
+ await callbackButton(tokens, option, {
556
+ cardKind: "question",
557
+ cardId: card.id,
558
+ action: "answer",
559
+ answer: { [f.key]: option }
560
+ })
561
+ );
562
+ }
563
+ }
564
+ rows.push(urlButton(t.web, card.webUrl));
565
+ return rows;
566
+ }
567
+ case "connect":
568
+ return [urlButton(t.connect, card.url)];
569
+ case "bind":
570
+ return [urlButton(t.bind, card.url)];
571
+ }
572
+ }
573
+ var textOf = (t, locale) => typeof t === "string" ? t : t[locale];
574
+ function cardHeadline(card, locale) {
575
+ switch (card.kind) {
576
+ case "approval":
577
+ return card.title;
578
+ case "question":
579
+ return textOf(card.form.title, locale);
580
+ case "bind":
581
+ return card.text;
582
+ case "connect":
583
+ return void 0;
584
+ }
585
+ }
586
+ var markup = (rows) => {
587
+ const kept = rows.filter((r) => r.length > 0);
588
+ return kept.length > 0 ? { replyMarkup: { inline_keyboard: kept } } : {};
589
+ };
590
+ async function renderCards(cards, locale, tokens, log) {
591
+ const posts = [];
592
+ for (const card of cards) {
593
+ const rows = await keyboard(card, locale, { tokens, log });
594
+ const { body, linkLine } = cardText(card, locale);
595
+ const hasUrlButton = rows.some((r) => r.some((b) => "url" in b));
596
+ posts.push({
597
+ html: body,
598
+ ...markup(rows),
599
+ ...hasUrlButton && linkLine !== void 0 && {
600
+ withoutUrlButtons: {
601
+ html: body === "" ? linkLine : `${body}
602
+ ${linkLine}`,
603
+ ...markup(rows.map((r) => r.filter((b) => !("url" in b))))
604
+ }
605
+ }
606
+ });
607
+ }
608
+ return posts;
609
+ }
610
+
611
+ // src/errors.ts
612
+ import { ChannelSendError, isChannelSendError } from "@capybara-hq/plugin-sdk";
613
+ function toChannelSendError(e) {
614
+ if (isChannelSendError(e)) return e;
615
+ if (!(e instanceof TelegramApiError)) {
616
+ return new ChannelSendError("Telegram API call failed", { retryable: true });
617
+ }
618
+ const opts = { cause: e };
619
+ const code = e.errorCode ?? e.status ?? 0;
620
+ if (e.network || code >= 500)
621
+ return new ChannelSendError(e.message, { retryable: true, ...opts });
622
+ if (code === 429) {
623
+ return new ChannelSendError(e.message, {
624
+ retryable: true,
625
+ ...e.retryAfterSec !== void 0 && { retryAfterMs: e.retryAfterSec * 1e3 },
626
+ ...opts
627
+ });
628
+ }
629
+ const reject = (reason) => new ChannelSendError(e.message, { retryable: false, reason, ...opts });
630
+ if (code === 401) return reject("auth");
631
+ if (code === 403) return reject("target_gone");
632
+ const text = (e.description ?? "").toLowerCase();
633
+ if (e.migrateToChatId !== void 0) return reject("target_gone");
634
+ if (code === 400 && text.includes("chat not found")) return reject("target_gone");
635
+ if (code === 400 && /too[ _]long/.test(text)) return reject("too_large");
636
+ return reject("rejected");
637
+ }
638
+
639
+ // src/inbound.ts
640
+ var obj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
641
+ var int2 = (v) => typeof v === "number" && Number.isSafeInteger(v) ? v : void 0;
642
+ var str2 = (v) => typeof v === "string" ? v : void 0;
643
+ async function fetchBotIdentity(api, signal) {
644
+ const me = await api.call(
645
+ "getMe",
646
+ void 0,
647
+ signal
648
+ );
649
+ const id = int2(me?.id);
650
+ if (id === void 0 || typeof me?.username !== "string" || me.username === "") {
651
+ throw new Error("Telegram getMe returned no bot user");
652
+ }
653
+ return { id, username: me.username };
654
+ }
655
+ function chatMigration(update) {
656
+ const message = obj2(obj2(update)?.message);
657
+ const migrateToChatId = int2(message?.migrate_to_chat_id);
658
+ if (migrateToChatId === void 0) return void 0;
659
+ return { chatId: int2(obj2(message?.chat)?.id), migrateToChatId };
660
+ }
661
+ function applyEdits(text, edits) {
662
+ let out = text;
663
+ for (const e of [...edits].sort((a, b) => b.start - a.start)) {
664
+ if (e.replacement !== void 0) {
665
+ out = out.slice(0, e.start) + e.replacement + out.slice(e.end);
666
+ continue;
667
+ }
668
+ let s = e.start;
669
+ let t = e.end;
670
+ while (s > 0 && (out[s - 1] === " " || out[s - 1] === " ")) s--;
671
+ while (t < out.length && (out[t] === " " || out[t] === " ")) t++;
672
+ const lineStart = s === 0 || out[s - 1] === "\n";
673
+ const lineEnd = t === out.length || out[t] === "\n";
674
+ out = out.slice(0, s) + (lineStart || lineEnd ? "" : " ") + out.slice(t);
675
+ }
676
+ return out.trim();
677
+ }
678
+ function readText(text, entities, bot) {
679
+ const handle = `@${bot.username}`.toLowerCase();
680
+ const edits = [];
681
+ let addressed = false;
682
+ for (const raw of Array.isArray(entities) ? entities : []) {
683
+ const e = obj2(raw);
684
+ const start = int2(e?.offset);
685
+ const length = int2(e?.length);
686
+ if (!e || start === void 0 || length === void 0 || length <= 0) continue;
687
+ const end = start + length;
688
+ if (end > text.length) continue;
689
+ const slice = text.slice(start, end);
690
+ if (e.type === "mention") {
691
+ if (slice.toLowerCase() === handle) {
692
+ addressed = true;
693
+ edits.push({ start, end, replacement: void 0 });
694
+ }
695
+ } else if (e.type === "text_mention") {
696
+ if (obj2(e.user)?.id === bot.id) {
697
+ addressed = true;
698
+ edits.push({ start, end, replacement: void 0 });
699
+ } else {
700
+ edits.push({ start, end, replacement: `@${slice}` });
701
+ }
702
+ } else if (e.type === "bot_command") {
703
+ const at = slice.lastIndexOf("@");
704
+ if (at > 0 && slice.slice(at).toLowerCase() === handle) {
705
+ addressed = true;
706
+ edits.push({ start: start + at, end, replacement: "" });
707
+ }
708
+ }
709
+ }
710
+ return { text: applyEdits(text, edits), addressed };
711
+ }
712
+ async function updateToInbound(update, bot, threads, tokens) {
713
+ if (tokens && obj2(obj2(update)?.callback_query)) {
714
+ return (await parseCallback(update, tokens, threads))?.event ?? null;
715
+ }
716
+ const message = obj2(obj2(update)?.message);
717
+ if (!message) return null;
718
+ const chat = obj2(message.chat);
719
+ const from = obj2(message.from);
720
+ const messageId = int2(message.message_id);
721
+ const chatId = int2(chat?.id);
722
+ const userId = int2(from?.id);
723
+ if (!chat || !from || !messageId || chatId === void 0 || userId === void 0) return null;
724
+ if (from.is_bot === true) return null;
725
+ const isText = typeof message.text === "string";
726
+ const rawText = str2(message.text) ?? str2(message.caption);
727
+ if (rawText === void 0) return null;
728
+ const { text, addressed } = readText(
729
+ rawText,
730
+ isText ? message.entities : message.caption_entities,
731
+ bot
732
+ );
733
+ if (text === "") return null;
734
+ const chatType = chat.type;
735
+ const dm = chatType === "private";
736
+ if (!dm && chatType !== "group" && chatType !== "supergroup") return null;
737
+ const chatKey = String(chatId);
738
+ const displayName = [str2(from.first_name), str2(from.last_name)].filter(Boolean).join(" ").trim();
739
+ const sender = {
740
+ externalId: String(userId),
741
+ tenantId: SENDER_TENANT,
742
+ ...displayName !== "" && { displayName }
743
+ };
744
+ const base = { type: "message", messageId: `${chatKey}:${messageId}`, sender, text };
745
+ if (dm) {
746
+ return {
747
+ ...base,
748
+ target: { chatId: chatKey, chatType: "dm" },
749
+ mentionsBot: true
750
+ };
751
+ }
752
+ const parent = obj2(message.reply_to_message);
753
+ const parentId = parent && !parent.forum_topic_created ? int2(parent.message_id) : void 0;
754
+ const replyToBot = parentId !== void 0 && obj2(parent?.from)?.id === bot.id;
755
+ if (!addressed && !replyToBot) return null;
756
+ const threadId = parentId !== void 0 ? await threads.threadOfMessage(chatKey, parentId) : void 0;
757
+ await threads.recordSent(chatKey, messageId, threadId ?? base.messageId);
758
+ const messageThreadId = int2(message.message_thread_id);
759
+ if (message.is_topic_message === true && messageThreadId !== void 0) {
760
+ await threads.recordTopic(threadId ?? base.messageId, messageThreadId);
761
+ }
762
+ const target = {
763
+ chatId: chatKey,
764
+ chatType: "group",
765
+ ...threadId !== void 0 && { threadId }
766
+ };
767
+ return { ...base, target, mentionsBot: true };
768
+ }
769
+
770
+ // src/wait.ts
771
+ var pause = (ms, signal) => new Promise((resolve) => {
772
+ if (signal.aborted) return resolve();
773
+ const timer = AbortSignal.timeout(ms);
774
+ const done = () => {
775
+ signal.removeEventListener("abort", done);
776
+ timer.removeEventListener("abort", done);
777
+ resolve();
778
+ };
779
+ signal.addEventListener("abort", done, { once: true });
780
+ timer.addEventListener("abort", done, { once: true });
781
+ });
782
+
783
+ // src/poll.ts
784
+ var OFFSET_TTL_SECONDS = 604800;
785
+ var BACKOFF_CAP_MS = 3e4;
786
+ var MAX_CONSECUTIVE_FAILURES = 5;
787
+ var WEBHOOK_SET_ERROR = "A webhook is set for this Telegram bot; switch the connection to webhook mode or call deleteWebhook";
788
+ var CONFLICT_ERROR = "Telegram getUpdates conflict: another client is polling this bot";
789
+ var TOKEN_REJECTED_ERROR = "Telegram bot token rejected";
790
+ var obj3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
791
+ var int3 = (v) => typeof v === "number" && Number.isSafeInteger(v) ? v : void 0;
792
+ function backoffMs(base, attempt) {
793
+ const step = Math.min(BACKOFF_CAP_MS, base * 2 ** Math.min(attempt - 1, 20));
794
+ return Math.round(step * (0.5 + Math.random() * 0.5));
795
+ }
796
+ function refusal(e) {
797
+ if (!(e instanceof TelegramApiError)) return void 0;
798
+ const code = e.errorCode ?? e.status;
799
+ if (code === 401) return new Error(TOKEN_REJECTED_ERROR);
800
+ if (code === 409) {
801
+ return new Error(/webhook/i.test(e.description ?? "") ? WEBHOOK_SET_ERROR : CONFLICT_ERROR);
802
+ }
803
+ return void 0;
804
+ }
805
+ function transient(e) {
806
+ const code = e.errorCode ?? e.status;
807
+ return e.network || code === 429 || e.status !== void 0 && e.status >= 500;
808
+ }
809
+ var messageOf = (e) => e instanceof TelegramApiError ? e.message : "unexpected error while polling Telegram";
810
+ async function runLongPolling(a) {
811
+ const { api, signal, log } = a;
812
+ const pollApi = a.pollApi ?? api;
813
+ const bot = await startup(a);
814
+ if (!bot) return;
815
+ const offsetKey = `offset/${bot.id}`;
816
+ let offset = await readOffset(a.kv, offsetKey, log);
817
+ let failures = 0;
818
+ let rejections = 0;
819
+ while (!signal.aborted) {
820
+ let updates;
821
+ try {
822
+ const result = await pollApi.call(
823
+ "getUpdates",
824
+ {
825
+ ...offset !== void 0 && { offset },
826
+ timeout: a.pollTimeoutSec,
827
+ allowed_updates: ["message", "callback_query"]
828
+ },
829
+ signal
830
+ );
831
+ updates = Array.isArray(result) ? result : [];
832
+ failures = 0;
833
+ } catch (e) {
834
+ if (signal.aborted) return;
835
+ const refused = refusal(e);
836
+ if (refused) throw refused;
837
+ if (!(e instanceof TelegramApiError) || !transient(e)) throw new Error(messageOf(e));
838
+ failures++;
839
+ if (failures >= MAX_CONSECUTIVE_FAILURES) throw new Error(e.message);
840
+ const wait = Math.max(backoffMs(a.backoffBaseMs, failures), (e.retryAfterSec ?? 0) * 1e3);
841
+ log.warn("telegram: getUpdates failed; retrying", {
842
+ connectionId: a.connectionId,
843
+ error: e.message,
844
+ attempt: failures,
845
+ waitMs: wait
846
+ });
847
+ await pause(wait, signal);
848
+ continue;
849
+ }
850
+ let usable = 0;
851
+ for (const update of updates) {
852
+ const updateId = int3(obj3(update)?.update_id);
853
+ if (updateId === void 0) continue;
854
+ usable++;
855
+ let delivered;
856
+ try {
857
+ delivered = await deliver(a, update, bot);
858
+ } catch (e) {
859
+ if (signal.aborted) return;
860
+ rejections++;
861
+ const wait = backoffMs(a.backoffBaseMs, rejections);
862
+ log.warn("telegram: an update was not accepted; fetching it again", {
863
+ connectionId: a.connectionId,
864
+ updateId,
865
+ error: e instanceof Error ? e.message : String(e),
866
+ waitMs: wait
867
+ });
868
+ await pause(wait, signal);
869
+ break;
870
+ }
871
+ if (delivered) rejections = 0;
872
+ offset = updateId + 1;
873
+ await writeOffset(a.kv, offsetKey, offset, log);
874
+ const callbackId = obj3(obj3(update)?.callback_query)?.id;
875
+ if (typeof callbackId === "string" && callbackId !== "") {
876
+ await answerCallback(a, callbackId);
877
+ }
878
+ if (signal.aborted) return;
879
+ }
880
+ if (updates.length > 0 && usable === 0) {
881
+ await pause(backoffMs(a.backoffBaseMs, 1), signal);
882
+ }
883
+ }
884
+ }
885
+ async function startup(a) {
886
+ try {
887
+ const bot = await fetchBotIdentity(a.api, a.signal);
888
+ const hook = await a.api.call(
889
+ "getWebhookInfo",
890
+ void 0,
891
+ a.signal
892
+ );
893
+ if (typeof hook?.url === "string" && hook.url !== "") throw new Error(WEBHOOK_SET_ERROR);
894
+ return bot;
895
+ } catch (e) {
896
+ if (a.signal.aborted) return void 0;
897
+ if (e instanceof TelegramApiError) throw refusal(e) ?? new Error(e.message);
898
+ throw e;
899
+ }
900
+ }
901
+ async function deliver(a, update, bot) {
902
+ const migration = chatMigration(update);
903
+ if (migration) {
904
+ a.log.info("telegram: a group became a supergroup; its old conversations stop", {
905
+ connectionId: a.connectionId,
906
+ ...migration
907
+ });
908
+ return false;
909
+ }
910
+ const event = await updateToInbound(update, bot, a.threads, a.tokens);
911
+ if (!event) return false;
912
+ await a.onEvent(event);
913
+ return true;
914
+ }
915
+ async function answerCallback(a, callbackQueryId) {
916
+ try {
917
+ await a.api.call("answerCallbackQuery", { callback_query_id: callbackQueryId }, a.signal);
918
+ } catch (e) {
919
+ if (a.signal.aborted) return;
920
+ a.log.warn("telegram: answerCallbackQuery failed", {
921
+ connectionId: a.connectionId,
922
+ error: messageOf(e)
923
+ });
924
+ }
925
+ }
926
+ async function readOffset(kv, key, log) {
927
+ if (!kv) return void 0;
928
+ try {
929
+ const raw = await kv.get(key);
930
+ const n = raw === null || raw === void 0 ? void 0 : Number(raw);
931
+ return n !== void 0 && Number.isSafeInteger(n) && n > 0 ? n : void 0;
932
+ } catch {
933
+ log.warn("telegram: could not read the stored getUpdates offset; starting without it");
934
+ return void 0;
935
+ }
936
+ }
937
+ async function writeOffset(kv, key, offset, log) {
938
+ if (!kv) return;
939
+ try {
940
+ await kv.set(key, String(offset), { ttlSeconds: OFFSET_TTL_SECONDS });
941
+ } catch {
942
+ log.warn("telegram: could not store the getUpdates offset", { offset });
943
+ }
944
+ }
945
+
946
+ // src/webhook.ts
947
+ import { timingSafeEqual } from "node:crypto";
948
+ var SECRET_HEADER = "x-telegram-bot-api-secret-token";
949
+ var BOT_IDENTITY_TTL_SECONDS = 3600;
950
+ var TELEGRAM_WEBHOOK_ACK = { status: 200 };
951
+ var obj4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
952
+ function secretMatches(header, secret2) {
953
+ if (secret2 === void 0 || secret2 === "" || header === void 0) return false;
954
+ const got = Buffer.from(header, "utf8");
955
+ const want = Buffer.from(secret2, "utf8");
956
+ return got.length === want.length && timingSafeEqual(got, want);
957
+ }
958
+ async function handleTelegramWebhook(a) {
959
+ if (!secretMatches(a.request.headers[SECRET_HEADER], a.config.webhookSecret)) {
960
+ return { kind: "reject", status: 401 };
961
+ }
962
+ let update;
963
+ try {
964
+ update = JSON.parse(new TextDecoder().decode(a.request.body));
965
+ } catch {
966
+ return { kind: "reject", status: 400 };
967
+ }
968
+ const id = obj4(update)?.update_id;
969
+ if (typeof id !== "number" || !Number.isSafeInteger(id)) return { kind: "reject", status: 400 };
970
+ const event = await a.toEvent(update);
971
+ return { kind: "events", events: event ? [event] : [], ack: TELEGRAM_WEBHOOK_ACK };
972
+ }
973
+ async function webhookBotIdentity(api, kv, botId, signal) {
974
+ const key = `bot/${botId}`;
975
+ if (kv) {
976
+ try {
977
+ const raw = await kv.get(key);
978
+ const cached = obj4(raw ? JSON.parse(raw) : void 0);
979
+ if (typeof cached?.id === "number" && Number.isSafeInteger(cached.id) && typeof cached.username === "string" && cached.username !== "") {
980
+ return { id: cached.id, username: cached.username };
981
+ }
982
+ } catch {
983
+ }
984
+ }
985
+ const bot = await fetchBotIdentity(api, signal);
986
+ if (kv) {
987
+ try {
988
+ await kv.set(key, JSON.stringify(bot), { ttlSeconds: BOT_IDENTITY_TTL_SECONDS });
989
+ } catch {
990
+ }
991
+ }
992
+ return bot;
993
+ }
994
+ async function answerWebhookCallback(api, update, log, connectionId, signal) {
995
+ const id = obj4(obj4(update)?.callback_query)?.id;
996
+ if (typeof id !== "string" || id === "") return;
997
+ try {
998
+ await api.call("answerCallbackQuery", { callback_query_id: id }, signal);
999
+ } catch (e) {
1000
+ if (signal.aborted) return;
1001
+ log.warn("telegram: answerCallbackQuery failed", {
1002
+ connectionId,
1003
+ error: e instanceof TelegramApiError ? e.message : "unexpected error"
1004
+ });
1005
+ }
1006
+ }
1007
+
1008
+ // src/channel.ts
1009
+ var MIN_EDIT_INTERVAL_MS = 3e3;
1010
+ function parseMessageId(id) {
1011
+ const at = id.lastIndexOf(":");
1012
+ const messageId = Number(id.slice(at + 1));
1013
+ if (at <= 0 || !Number.isSafeInteger(messageId)) {
1014
+ throw new ChannelSendError2("malformed Telegram message id", {
1015
+ retryable: false,
1016
+ reason: "rejected"
1017
+ });
1018
+ }
1019
+ return { chatId: id.slice(0, at), messageId };
1020
+ }
1021
+ var tooLarge = () => new ChannelSendError2("message too long for a single Telegram message", {
1022
+ retryable: false,
1023
+ reason: "too_large"
1024
+ });
1025
+ var urlButtonRejected = (e) => e instanceof TelegramApiError && (e.errorCode ?? e.status) === 400 && /button URL|Wrong HTTP URL|BUTTON_URL_INVALID/i.test(e.description ?? "");
1026
+ function createTelegramChannel(o) {
1027
+ const apiFor = (config, signal) => {
1028
+ const c = TelegramConnection.parse(config);
1029
+ const botId = botIdOf(c.botToken);
1030
+ const api = telegramApi({
1031
+ apiUrl: o.apiUrl,
1032
+ token: c.botToken,
1033
+ ...o.requestTimeoutMs !== void 0 && { timeoutMs: o.requestTimeoutMs }
1034
+ });
1035
+ return {
1036
+ api,
1037
+ botId,
1038
+ call: (m, p) => api.call(m, p, signal)
1039
+ };
1040
+ };
1041
+ async function render(message, locale, botId) {
1042
+ const text = message.text.trim();
1043
+ const cards = message.cards ?? [];
1044
+ if (cards.length === 0) {
1045
+ return text === "" ? [] : splitHtml(markdownToTelegramHtml(message.text)).map((html) => ({ html }));
1046
+ }
1047
+ const dup = cards.some((c) => cardHeadline(c, locale)?.trim() === text);
1048
+ const lead = text === "" || dup ? "" : markdownToTelegramHtml(message.text);
1049
+ const fit = (p, leading) => ({
1050
+ ...p,
1051
+ html: fitHtml(leading, p.html),
1052
+ ...p.withoutUrlButtons && { withoutUrlButtons: fit(p.withoutUrlButtons, leading) }
1053
+ });
1054
+ const posts = await renderCards(cards, locale, cardTokenStore(o.kv, botId), o.log);
1055
+ return posts.map((p, i) => fit(p, i === 0 ? lead : ""));
1056
+ }
1057
+ async function withUrlButtonFallback(post, attempt) {
1058
+ try {
1059
+ return await attempt(post);
1060
+ } catch (e) {
1061
+ if (!post.withoutUrlButtons || !urlButtonRejected(e)) throw e;
1062
+ o.log.warn("telegram: Telegram rejected a card's URL button; the link is sent as text");
1063
+ return attempt(post.withoutUrlButtons);
1064
+ }
1065
+ }
1066
+ async function sendOne(api, target, post, extra) {
1067
+ let r;
1068
+ try {
1069
+ r = await withUrlButtonFallback(
1070
+ post,
1071
+ (p) => api.call("sendMessage", {
1072
+ chat_id: target.chatId,
1073
+ text: p.html,
1074
+ parse_mode: "HTML",
1075
+ link_preview_options: { is_disabled: true },
1076
+ ...p.replyMarkup && { reply_markup: p.replyMarkup },
1077
+ ...extra
1078
+ })
1079
+ );
1080
+ } catch (e) {
1081
+ throw toChannelSendError(e);
1082
+ }
1083
+ if (typeof r?.message_id !== "number") {
1084
+ throw new ChannelSendError2("Telegram returned no message id", {
1085
+ retryable: false,
1086
+ reason: "rejected"
1087
+ });
1088
+ }
1089
+ return r.message_id;
1090
+ }
1091
+ return {
1092
+ connectionSchema: TelegramConnection,
1093
+ features: { cards: true, edit: true, threads: true, minEditIntervalMs: MIN_EDIT_INTERVAL_MS },
1094
+ async verify({ config, signal }) {
1095
+ const { call } = apiFor(config, signal);
1096
+ const me = await call("getMe");
1097
+ if (typeof me?.id !== "number" || !me.username) {
1098
+ throw new Error("Telegram getMe returned no bot user");
1099
+ }
1100
+ if (me.can_read_all_group_messages) {
1101
+ o.log.warn(
1102
+ "telegram: the bot can read all group messages (privacy mode is off); only mentions and replies to the bot are forwarded",
1103
+ { botId: me.id }
1104
+ );
1105
+ }
1106
+ if (me.has_topics_enabled) {
1107
+ o.log.warn(
1108
+ "telegram: topic mode in private chats is not supported; turn it off in BotFather",
1109
+ { botId: me.id }
1110
+ );
1111
+ }
1112
+ return {
1113
+ botUserId: String(me.id),
1114
+ botName: `@${me.username}`,
1115
+ tenantId: connectionTenant(me.id)
1116
+ };
1117
+ },
1118
+ async connect({ connectionId, config, onEvent, signal }) {
1119
+ const c = TelegramConnection.parse(config);
1120
+ const botId = botIdOf(c.botToken);
1121
+ const pollTimeoutSec = o.pollTimeoutSec ?? 25;
1122
+ const api = telegramApi({
1123
+ apiUrl: o.apiUrl,
1124
+ token: c.botToken,
1125
+ ...o.requestTimeoutMs !== void 0 && { timeoutMs: o.requestTimeoutMs }
1126
+ });
1127
+ const pollApi = telegramApi({
1128
+ apiUrl: o.apiUrl,
1129
+ token: c.botToken,
1130
+ timeoutMs: pollTimeoutSec * 1e3 + 1e4
1131
+ });
1132
+ await runLongPolling({
1133
+ api,
1134
+ pollApi,
1135
+ connectionId,
1136
+ onEvent,
1137
+ signal,
1138
+ log: o.log,
1139
+ kv: o.kv,
1140
+ threads: threadStore(o.kv, botId),
1141
+ tokens: cardTokenStore(o.kv, botId),
1142
+ pollTimeoutSec,
1143
+ backoffBaseMs: o.backoffBaseMs ?? 1e3
1144
+ });
1145
+ },
1146
+ webhook: {
1147
+ // The kernel validated `config`; parsing again only narrows its type. Nothing reaches
1148
+ // Telegram (getMe, answerCallbackQuery) before the secret check passed.
1149
+ async handle({ connectionId, config, request, signal }) {
1150
+ const c = TelegramConnection.parse(config);
1151
+ const botId = botIdOf(c.botToken);
1152
+ const api = telegramApi({
1153
+ apiUrl: o.apiUrl,
1154
+ token: c.botToken,
1155
+ ...o.requestTimeoutMs !== void 0 && { timeoutMs: o.requestTimeoutMs }
1156
+ });
1157
+ return handleTelegramWebhook({
1158
+ config: c,
1159
+ request,
1160
+ toEvent: async (update) => {
1161
+ const migration = chatMigration(update);
1162
+ if (migration) {
1163
+ o.log.info("telegram: a group became a supergroup; its old conversations stop", {
1164
+ connectionId,
1165
+ ...migration
1166
+ });
1167
+ return null;
1168
+ }
1169
+ const bot = await webhookBotIdentity(api, o.kv, botId, signal);
1170
+ const event = await updateToInbound(
1171
+ update,
1172
+ bot,
1173
+ threadStore(o.kv, botId),
1174
+ cardTokenStore(o.kv, botId)
1175
+ );
1176
+ await answerWebhookCallback(api, update, o.log, connectionId, signal);
1177
+ return event;
1178
+ }
1179
+ });
1180
+ }
1181
+ },
1182
+ async send({ config, target, message, locale, signal }) {
1183
+ const { call, botId } = apiFor(config, signal);
1184
+ const posts = await render(message, locale, botId);
1185
+ if (posts.length === 0) throw new ChannelSendError2("nothing to send", { retryable: false });
1186
+ const store = threadStore(o.kv, botId);
1187
+ const inThread = target.chatType === "group" && target.threadId !== void 0;
1188
+ const extra = {};
1189
+ if (inThread && target.threadId !== void 0) {
1190
+ extra.reply_parameters = {
1191
+ message_id: parseMessageId(target.threadId).messageId,
1192
+ allow_sending_without_reply: true
1193
+ };
1194
+ const topic = await store.topicOf(target.threadId);
1195
+ if (topic !== void 0) extra.message_thread_id = topic;
1196
+ }
1197
+ let last = 0;
1198
+ for (const post of posts) {
1199
+ last = await sendOne({ call }, target, post, extra);
1200
+ if (inThread && target.threadId !== void 0) {
1201
+ try {
1202
+ await store.recordSent(target.chatId, last, target.threadId);
1203
+ } catch {
1204
+ o.log.warn("telegram: could not record a sent message's thread", {
1205
+ chatId: target.chatId,
1206
+ messageId: last
1207
+ });
1208
+ }
1209
+ }
1210
+ }
1211
+ return { messageId: `${target.chatId}:${last}` };
1212
+ },
1213
+ async update({ config, messageId, message, locale, signal }) {
1214
+ const { call, botId } = apiFor(config, signal);
1215
+ const { chatId, messageId: id } = parseMessageId(messageId);
1216
+ const posts = await render(message, locale, botId);
1217
+ if (posts.length > 1) throw tooLarge();
1218
+ const post = posts[0];
1219
+ if (!post) throw new ChannelSendError2("nothing to send", { retryable: false });
1220
+ try {
1221
+ await withUrlButtonFallback(
1222
+ post,
1223
+ (p) => call("editMessageText", {
1224
+ chat_id: chatId,
1225
+ message_id: id,
1226
+ text: p.html,
1227
+ parse_mode: "HTML",
1228
+ link_preview_options: { is_disabled: true },
1229
+ ...p.replyMarkup && { reply_markup: p.replyMarkup }
1230
+ })
1231
+ );
1232
+ } catch (e) {
1233
+ if (e instanceof TelegramApiError && /message is not modified/i.test(e.description ?? "")) {
1234
+ return;
1235
+ }
1236
+ throw toChannelSendError(e);
1237
+ }
1238
+ }
1239
+ };
1240
+ }
1241
+
1242
+ // src/index.ts
1243
+ var index_default = defineExtension({
1244
+ name: "telegram",
1245
+ version: "0.1.0-rc.1",
1246
+ sdk: "^0.1.0-rc.4",
1247
+ provides: [CHANNEL_EXTENSION_POINT],
1248
+ permissions: { network: ["api.telegram.org"] },
1249
+ config: { deployment: TelegramDeployment },
1250
+ setup(ctx) {
1251
+ const { apiUrl } = TelegramDeployment.parse(ctx.config ?? {});
1252
+ ctx.provide(
1253
+ CHANNEL_EXTENSION_POINT,
1254
+ createTelegramChannel({ apiUrl, log: ctx.log, kv: ctx.kv })
1255
+ );
1256
+ }
1257
+ });
1258
+ export {
1259
+ TelegramConnection,
1260
+ TelegramDeployment,
1261
+ createTelegramChannel,
1262
+ index_default as default
1263
+ };