@llblab/pi-telegram 0.45.11 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,1159 @@
1
+ /**
2
+ * Telegram API transport helpers
3
+ * Zones: telegram transport, filesystem, runtime diagnostics
4
+ *
5
+ * Wraps bot API calls, file uploads/downloads (including voice messages),
6
+ * multipart sending, runtime transport binding, and Telegram temp-file lifecycle.
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import { createWriteStream, openAsBlob } from "node:fs";
10
+ import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { request as requestHttps } from "node:https";
12
+ import { join } from "node:path";
13
+ import { resolveTelegramTempDir } from "./paths.js";
14
+ import { Readable, Transform } from "node:stream";
15
+ import { pipeline } from "node:stream/promises";
16
+ export const TELEGRAM_API_BASE = "https://api.telegram.org";
17
+ export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
18
+ export function getTelegramInboundFileByteLimitFromEnv(env, names, defaultValue = TELEGRAM_FILE_MAX_BYTES) {
19
+ for (const name of names) {
20
+ const rawValue = env[name]?.trim();
21
+ if (!rawValue)
22
+ continue;
23
+ const parsed = Number(rawValue);
24
+ if (Number.isSafeInteger(parsed) && parsed > 0)
25
+ return parsed;
26
+ }
27
+ return defaultValue;
28
+ }
29
+ function getTelegramApiTempDir() {
30
+ return resolveTelegramTempDir();
31
+ }
32
+ const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
33
+ const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
34
+ const TELEGRAM_TEMP_SCRATCH_FILE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/u;
35
+ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
36
+ const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
37
+ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
38
+ "auto",
39
+ "ipv4",
40
+ "ipv6",
41
+ "ipv4-fallback",
42
+ ]);
43
+ function parseTelegramApiTargetInteger(value) {
44
+ const parsed = typeof value === "number" ? value :
45
+ typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
46
+ return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
47
+ }
48
+ export function createTelegramApiTargetActivityRuntime() {
49
+ const pending = new Map();
50
+ const pendingChats = new Map();
51
+ const messageScopedMethods = new Set([
52
+ "deleteMessage",
53
+ "editMessageCaption",
54
+ "editMessageReplyMarkup",
55
+ "editMessageText",
56
+ ]);
57
+ return {
58
+ begin(method, body) {
59
+ const chatId = parseTelegramApiTargetInteger(body.chat_id);
60
+ const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
61
+ const messageId = parseTelegramApiTargetInteger(body.message_id);
62
+ const chatScoped = chatId !== undefined && threadId === undefined &&
63
+ messageId !== undefined && messageScopedMethods.has(method);
64
+ if (chatId === undefined || (threadId === undefined && !chatScoped)) {
65
+ return () => undefined;
66
+ }
67
+ const key = threadId === undefined ? undefined : `${chatId}:${threadId}`;
68
+ if (key) {
69
+ const existing = pending.get(key);
70
+ if (existing)
71
+ existing.count += 1;
72
+ else
73
+ pending.set(key, { target: { chatId, threadId: threadId }, count: 1 });
74
+ }
75
+ else {
76
+ pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
77
+ }
78
+ let completed = false;
79
+ return () => {
80
+ if (completed)
81
+ return;
82
+ completed = true;
83
+ if (!key) {
84
+ const count = pendingChats.get(chatId);
85
+ if (!count || count <= 1)
86
+ pendingChats.delete(chatId);
87
+ else
88
+ pendingChats.set(chatId, count - 1);
89
+ return;
90
+ }
91
+ const current = pending.get(key);
92
+ if (!current || current.count <= 1)
93
+ pending.delete(key);
94
+ else
95
+ current.count -= 1;
96
+ };
97
+ },
98
+ hasPendingTarget(target) {
99
+ return pendingChats.has(target.chatId) ||
100
+ (target.threadId !== undefined && pending.has(`${target.chatId}:${target.threadId}`));
101
+ },
102
+ listPendingTargets() {
103
+ return Array.from(pending.values(), ({ target }) => ({ ...target }));
104
+ },
105
+ listPendingChats() {
106
+ return Array.from(pendingChats.keys());
107
+ },
108
+ };
109
+ }
110
+ export function createTelegramApiTargetTrackingClient(client, activity) {
111
+ const track = async (method, body, operation) => {
112
+ const end = activity.begin(method, body);
113
+ try {
114
+ return await operation();
115
+ }
116
+ finally {
117
+ end();
118
+ }
119
+ };
120
+ return {
121
+ call: (method, body, options) => track(method, body, () => client.call(method, body, options)),
122
+ callMultipart: (method, fields, fileField, filePath, fileName, options) => track(method, fields, () => client.callMultipart(method, fields, fileField, filePath, fileName, options)),
123
+ downloadFile: client.downloadFile,
124
+ answerCallbackQuery: client.answerCallbackQuery,
125
+ ...(client.answerGuestQuery
126
+ ? { answerGuestQuery: client.answerGuestQuery }
127
+ : {}),
128
+ };
129
+ }
130
+ export class TelegramApiWorkspaceAdmissionError extends Error {
131
+ code;
132
+ constructor(code, message) {
133
+ super(message);
134
+ this.name = "TelegramApiWorkspaceAdmissionError";
135
+ this.code = code;
136
+ }
137
+ }
138
+ export function getTelegramApiWorkspaceAdmissionScope(body) {
139
+ if (!("chat_id" in body))
140
+ return undefined;
141
+ const chatId = parseTelegramApiTargetInteger(body.chat_id);
142
+ if (chatId === undefined || chatId === 0)
143
+ return { kind: "profile" };
144
+ if (!("message_thread_id" in body) || body.message_thread_id === undefined) {
145
+ return { kind: "chat", chatId };
146
+ }
147
+ const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
148
+ if (threadId === undefined || threadId <= 0)
149
+ return { kind: "profile" };
150
+ return { kind: "target", target: { chatId, threadId } };
151
+ }
152
+ export function createTelegramApiWorkspaceAdmissionClient(client, admission, options = {}) {
153
+ const admit = async (method, body, operation) => {
154
+ const scope = getTelegramApiWorkspaceAdmissionScope(body);
155
+ if (!scope)
156
+ return operation();
157
+ const currentAdmission = typeof admission === "function" ? admission() : admission;
158
+ if (!currentAdmission) {
159
+ throw new TelegramApiWorkspaceAdmissionError("unavailable", "Telegram API Workspace admission authority is unavailable.");
160
+ }
161
+ const operationId = `api:${(options.createOperationId ?? randomUUID)()}`;
162
+ if (activeTelegramApiWorkspaceAdmissionOperationIds.has(operationId)) {
163
+ throw new TelegramApiWorkspaceAdmissionError("duplicate-operation", "Telegram API Workspace admission operation is already active.");
164
+ }
165
+ activeTelegramApiWorkspaceAdmissionOperationIds.add(operationId);
166
+ try {
167
+ const acquired = currentAdmission.acquireAdmission({
168
+ operationId,
169
+ operationKind: `api.${method}`,
170
+ scope,
171
+ });
172
+ if (acquired.kind === "blocked") {
173
+ throw new TelegramApiWorkspaceAdmissionError("blocked", "Telegram API target is temporarily unavailable during Workspace retirement.");
174
+ }
175
+ try {
176
+ return await operation();
177
+ }
178
+ finally {
179
+ try {
180
+ if (!currentAdmission.releaseAdmission(acquired.lease)) {
181
+ throw new TelegramApiWorkspaceAdmissionError("release-lost", "Telegram API Workspace admission lease disappeared before release.");
182
+ }
183
+ }
184
+ catch (error) {
185
+ try {
186
+ options.onReleaseError?.(error, method);
187
+ }
188
+ catch {
189
+ // Diagnostics cannot convert an already-settled API request into replay.
190
+ }
191
+ }
192
+ }
193
+ }
194
+ finally {
195
+ activeTelegramApiWorkspaceAdmissionOperationIds.delete(operationId);
196
+ }
197
+ };
198
+ return {
199
+ call: (method, body, callOptions) => admit(method, body, () => client.call(method, body, callOptions)),
200
+ callMultipart: (method, fields, fileField, filePath, fileName, callOptions) => admit(method, fields, () => client.callMultipart(method, fields, fileField, filePath, fileName, callOptions)),
201
+ downloadFile: client.downloadFile,
202
+ answerCallbackQuery: client.answerCallbackQuery,
203
+ ...(client.answerGuestQuery
204
+ ? { answerGuestQuery: client.answerGuestQuery }
205
+ : {}),
206
+ };
207
+ }
208
+ function sanitizeFileName(name) {
209
+ return name.replace(/[^a-zA-Z0-9._-]+/g, "_");
210
+ }
211
+ class TelegramApiMalformedSuccessError extends Error {
212
+ constructor(method, detail) {
213
+ super(`Telegram API ${method} ${detail}`);
214
+ this.name = "TelegramApiMalformedSuccessError";
215
+ }
216
+ }
217
+ export class TelegramApiCommitUnknownError extends Error {
218
+ kind = "commit-unknown";
219
+ method;
220
+ cause;
221
+ constructor(method, cause) {
222
+ super(`Telegram API ${method} may have committed before transport failed.`);
223
+ this.name = "TelegramApiCommitUnknownError";
224
+ this.method = method;
225
+ this.cause = cause;
226
+ }
227
+ }
228
+ export function isTelegramApiCommitUnknownError(error) {
229
+ return error instanceof TelegramApiCommitUnknownError;
230
+ }
231
+ class TelegramApiHttpError extends Error {
232
+ status;
233
+ retryAfterSeconds;
234
+ requestTarget;
235
+ constructor(message, status, retryAfterSeconds) {
236
+ super(message);
237
+ this.status = status;
238
+ this.retryAfterSeconds = retryAfterSeconds;
239
+ }
240
+ }
241
+ function attachTelegramApiRequestTarget(error, body) {
242
+ if (!(error instanceof TelegramApiHttpError))
243
+ return;
244
+ const chatId = Number(body.chat_id);
245
+ const threadId = Number(body.message_thread_id);
246
+ if (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))
247
+ return;
248
+ error.requestTarget = { chatId, threadId };
249
+ }
250
+ export class TelegramApiStaleTargetError extends Error {
251
+ requestTarget;
252
+ constructor(message, requestTarget) {
253
+ super(message);
254
+ this.name = "TelegramApiStaleTargetError";
255
+ this.requestTarget = { ...requestTarget };
256
+ }
257
+ }
258
+ export function getTelegramApiErrorRequestTarget(error) {
259
+ const target = error instanceof TelegramApiHttpError ||
260
+ error instanceof TelegramApiStaleTargetError
261
+ ? error.requestTarget
262
+ : undefined;
263
+ return target ? { ...target } : undefined;
264
+ }
265
+ export function isTelegramStaleTargetHttpError(error) {
266
+ if (!(error instanceof TelegramApiHttpError) || error.status !== 400)
267
+ return false;
268
+ return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
269
+ }
270
+ export function isTelegramMessageNotModifiedError(error) {
271
+ return (error instanceof Error && error.message.includes("message is not modified"));
272
+ }
273
+ const TELEGRAM_RETRY_SAFE_METHODS = new Set([
274
+ "answerCallbackQuery",
275
+ "closeForumTopic",
276
+ "deleteForumTopic",
277
+ "deleteMessage",
278
+ "deleteWebhook",
279
+ "editForumTopic",
280
+ "editMessageCaption",
281
+ "editMessageReplyMarkup",
282
+ "editMessageText",
283
+ "getChat",
284
+ "getFile",
285
+ "getMe",
286
+ "getUpdates",
287
+ "sendChatAction",
288
+ "sendMessageDraft",
289
+ "sendRichMessageDraft",
290
+ "setMyCommands",
291
+ ]);
292
+ export function isTelegramApiMethodRetrySafe(method) {
293
+ return TELEGRAM_RETRY_SAFE_METHODS.has(method);
294
+ }
295
+ export function isRetryableTelegramApiError(error) {
296
+ return (error instanceof TelegramApiHttpError &&
297
+ (error.status === 429 ||
298
+ (error.status !== undefined && error.status >= 500)));
299
+ }
300
+ export function getTelegramApiRetryAfterMs(error) {
301
+ return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
302
+ ? Math.max(0, error.retryAfterSeconds * 1000)
303
+ : undefined;
304
+ }
305
+ export function isTelegramMessageUnavailableError(error) {
306
+ return error instanceof TelegramApiHttpError && error.status === 400 &&
307
+ /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message);
308
+ }
309
+ function getTelegramRetryDelayMs(error, attempt, baseDelayMs) {
310
+ if (error instanceof TelegramApiHttpError &&
311
+ error.retryAfterSeconds !== undefined) {
312
+ return Math.max(0, error.retryAfterSeconds * 1000);
313
+ }
314
+ return Math.max(0, baseDelayMs * 2 ** attempt);
315
+ }
316
+ function getTelegramApiAbortReason(signal) {
317
+ return signal.reason ?? new DOMException("Aborted", "AbortError");
318
+ }
319
+ function throwIfTelegramApiCallAborted(signal) {
320
+ if (signal?.aborted)
321
+ throw getTelegramApiAbortReason(signal);
322
+ }
323
+ function sleepTelegramRetry(ms, signal) {
324
+ if (signal?.aborted)
325
+ return Promise.reject(getTelegramApiAbortReason(signal));
326
+ return new Promise((resolve, reject) => {
327
+ const timeout = setTimeout(() => {
328
+ signal?.removeEventListener("abort", onAbort);
329
+ resolve();
330
+ }, ms);
331
+ const onAbort = () => {
332
+ clearTimeout(timeout);
333
+ reject(getTelegramApiAbortReason(signal));
334
+ };
335
+ signal?.addEventListener("abort", onAbort, { once: true });
336
+ if (signal?.aborted)
337
+ onAbort();
338
+ });
339
+ }
340
+ function assertTelegramFileSizeWithinLimit(size, maxFileSizeBytes) {
341
+ if (size === undefined || maxFileSizeBytes === undefined)
342
+ return;
343
+ if (size <= maxFileSizeBytes)
344
+ return;
345
+ throw new Error(`Telegram file exceeds size limit (${size} bytes > ${maxFileSizeBytes} bytes)`);
346
+ }
347
+ function createTelegramDownloadLimitTransform(maxFileSizeBytes) {
348
+ let downloadedBytes = 0;
349
+ return new Transform({
350
+ transform(chunk, _encoding, callback) {
351
+ downloadedBytes += chunk.byteLength;
352
+ try {
353
+ assertTelegramFileSizeWithinLimit(downloadedBytes, maxFileSizeBytes);
354
+ callback(undefined, chunk);
355
+ }
356
+ catch (error) {
357
+ callback(error instanceof Error ? error : new Error(String(error)));
358
+ }
359
+ },
360
+ });
361
+ }
362
+ async function writeTelegramDownloadResponse(response, targetPath, maxFileSizeBytes) {
363
+ if (!response.body) {
364
+ const buffer = Buffer.from(await response.arrayBuffer());
365
+ assertTelegramFileSizeWithinLimit(buffer.byteLength, maxFileSizeBytes);
366
+ await writeFile(targetPath, buffer, { mode: 0o600 });
367
+ return;
368
+ }
369
+ await pipeline(Readable.from(response.body, { objectMode: false }), createTelegramDownloadLimitTransform(maxFileSizeBytes), createWriteStream(targetPath, { mode: 0o600 }));
370
+ }
371
+ async function removeTelegramPartialDownload(path) {
372
+ try {
373
+ await unlink(path);
374
+ }
375
+ catch {
376
+ // ignore
377
+ }
378
+ }
379
+ async function parseTelegramApiResponse(response, method) {
380
+ let data;
381
+ try {
382
+ if (typeof response.text === "function") {
383
+ const text = await response.text();
384
+ data = text
385
+ ? JSON.parse(text)
386
+ : undefined;
387
+ }
388
+ else {
389
+ data = (await response.json());
390
+ }
391
+ }
392
+ catch {
393
+ data = undefined;
394
+ }
395
+ if (response.ok === false) {
396
+ const status = `HTTP ${response.status}`;
397
+ const description = data?.description ? `: ${data.description}` : "";
398
+ const retryAfterHeader = response.headers?.get("retry-after");
399
+ const retryAfterSeconds = data?.parameters?.retry_after ??
400
+ (retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
401
+ throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined);
402
+ }
403
+ if (!data) {
404
+ throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
405
+ }
406
+ return data;
407
+ }
408
+ function unwrapTelegramApiResult(method, data) {
409
+ if (data.ok && data.result === undefined) {
410
+ throw new TelegramApiMalformedSuccessError(method, "returned no result");
411
+ }
412
+ if (!data.ok) {
413
+ throw new Error(data.description || `Telegram API ${method} failed`);
414
+ }
415
+ return data.result;
416
+ }
417
+ function getTelegramNetworkFamilyPolicy(env = process.env) {
418
+ const value = env[TELEGRAM_NETWORK_FAMILY_ENV]?.trim().toLowerCase();
419
+ if (TELEGRAM_NETWORK_FAMILY_VALUES.has(value)) {
420
+ return value;
421
+ }
422
+ return "ipv4-fallback";
423
+ }
424
+ function getTelegramNetworkFamily(policy) {
425
+ if (policy === "ipv4")
426
+ return 4;
427
+ if (policy === "ipv6")
428
+ return 6;
429
+ return undefined;
430
+ }
431
+ function isTelegramTransportFailure(error) {
432
+ if (!(error instanceof Error))
433
+ return false;
434
+ if (error.name === "AbortError")
435
+ return false;
436
+ if (error instanceof TypeError && /fetch failed/i.test(error.message)) {
437
+ return true;
438
+ }
439
+ if (error instanceof AggregateError)
440
+ return true;
441
+ const code = getErrorCode(error);
442
+ if (code === "ECONNREFUSED" ||
443
+ code === "ETIMEDOUT" ||
444
+ code === "ENETUNREACH" ||
445
+ code === "EHOSTUNREACH" ||
446
+ code === "ECONNRESET" ||
447
+ code === "EAI_AGAIN") {
448
+ return true;
449
+ }
450
+ return isTelegramTransportFailure(error.cause);
451
+ }
452
+ function getTelegramRequestBodyBuffer(body) {
453
+ if (body === undefined || body === null)
454
+ return undefined;
455
+ if (typeof body === "string")
456
+ return Buffer.from(body);
457
+ if (body instanceof Uint8Array)
458
+ return Buffer.from(body);
459
+ throw new Error("Unsupported Telegram HTTPS request body");
460
+ }
461
+ async function buildTelegramMultipartBody(fields, fileField, fileBlob, fileName) {
462
+ const boundary = `pi-telegram-${randomUUID()}`;
463
+ const chunks = [];
464
+ for (const [key, value] of Object.entries(fields)) {
465
+ chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`));
466
+ }
467
+ chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${fileField}"; filename="${fileName}"\r\nContent-Type: ${fileBlob.type || "application/octet-stream"}\r\n\r\n`), Buffer.from(await fileBlob.arrayBuffer()), Buffer.from(`\r\n--${boundary}--\r\n`));
468
+ return {
469
+ body: Buffer.concat(chunks),
470
+ contentType: `multipart/form-data; boundary=${boundary}`,
471
+ };
472
+ }
473
+ async function telegramHttpsFetch(input, init, family) {
474
+ const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
475
+ const body = getTelegramRequestBodyBuffer(init.body);
476
+ const headers = new Headers(init.headers);
477
+ if (body && !headers.has("content-length")) {
478
+ headers.set("content-length", String(body.byteLength));
479
+ }
480
+ return new Promise((resolve, reject) => {
481
+ const req = requestHttps(url, {
482
+ method: init.method ?? "GET",
483
+ family,
484
+ headers: Object.fromEntries(headers.entries()),
485
+ }, (res) => {
486
+ const responseHeaders = new Headers();
487
+ for (const [key, value] of Object.entries(res.headers)) {
488
+ if (Array.isArray(value))
489
+ responseHeaders.set(key, value.join(", "));
490
+ else if (value !== undefined)
491
+ responseHeaders.set(key, String(value));
492
+ }
493
+ resolve(new Response(Readable.toWeb(res), {
494
+ status: res.statusCode ?? 200,
495
+ statusText: res.statusMessage,
496
+ headers: responseHeaders,
497
+ }));
498
+ });
499
+ req.on("error", reject);
500
+ if (init.signal) {
501
+ if (init.signal.aborted)
502
+ req.destroy(new DOMException("Aborted", "AbortError"));
503
+ else {
504
+ init.signal.addEventListener("abort", () => req.destroy(new DOMException("Aborted", "AbortError")), { once: true });
505
+ }
506
+ }
507
+ req.end(body);
508
+ });
509
+ }
510
+ let telegramHttpsFetchForTesting;
511
+ export function setTelegramApiHttpsFetchForTesting(fetchImpl) {
512
+ const previous = telegramHttpsFetchForTesting;
513
+ telegramHttpsFetchForTesting = fetchImpl;
514
+ return () => {
515
+ telegramHttpsFetchForTesting = previous;
516
+ };
517
+ }
518
+ async function telegramFetch(input, init = {}, family) {
519
+ if (!family)
520
+ return fetch(input, init);
521
+ return (telegramHttpsFetchForTesting ?? telegramHttpsFetch)(input, init, family);
522
+ }
523
+ async function callTelegramTransportRequest(request, allowFallback = true) {
524
+ const policy = getTelegramNetworkFamilyPolicy();
525
+ if (policy === "auto")
526
+ return request();
527
+ const family = getTelegramNetworkFamily(policy);
528
+ if (family)
529
+ return request(family);
530
+ if (!allowFallback)
531
+ return request();
532
+ try {
533
+ return await request();
534
+ }
535
+ catch (error) {
536
+ if (!isTelegramTransportFailure(error))
537
+ throw error;
538
+ return request(4);
539
+ }
540
+ }
541
+ function getErrorCode(error) {
542
+ const maybeCode = error.code;
543
+ return typeof maybeCode === "string" ? maybeCode : undefined;
544
+ }
545
+ function getErrorAddress(error) {
546
+ const maybeAddress = error.address;
547
+ return typeof maybeAddress === "string" ? maybeAddress : undefined;
548
+ }
549
+ function getErrorPort(error) {
550
+ const maybePort = error.port;
551
+ return typeof maybePort === "number" ? maybePort : undefined;
552
+ }
553
+ function getErrorFamily(error) {
554
+ const maybeFamily = error.family;
555
+ if (typeof maybeFamily === "number" || typeof maybeFamily === "string") {
556
+ return maybeFamily;
557
+ }
558
+ return undefined;
559
+ }
560
+ function describeTelegramErrorSummary(error) {
561
+ return {
562
+ name: error.name,
563
+ message: error.message,
564
+ ...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
565
+ };
566
+ }
567
+ function describeTelegramTransportAttempt(error) {
568
+ return {
569
+ name: error.name,
570
+ ...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
571
+ ...(getErrorAddress(error) ? { address: getErrorAddress(error) } : {}),
572
+ ...(getErrorPort(error) ? { port: getErrorPort(error) } : {}),
573
+ ...(getErrorFamily(error) ? { family: getErrorFamily(error) } : {}),
574
+ };
575
+ }
576
+ function describeTelegramTransportError(error) {
577
+ if (!isTelegramTransportFailure(error) || !(error instanceof Error)) {
578
+ return undefined;
579
+ }
580
+ const cause = error.cause instanceof Error ? error.cause : undefined;
581
+ const aggregate = error instanceof AggregateError
582
+ ? error
583
+ : cause instanceof AggregateError
584
+ ? cause
585
+ : undefined;
586
+ const attempts = aggregate?.errors
587
+ .filter((attempt) => attempt instanceof Error)
588
+ .map(describeTelegramTransportAttempt);
589
+ return {
590
+ error: describeTelegramErrorSummary(error),
591
+ ...(cause ? { cause: describeTelegramErrorSummary(cause) } : {}),
592
+ ...(attempts && attempts.length > 0 ? { attempts } : {}),
593
+ };
594
+ }
595
+ function withTelegramTransportDiagnostics(error, details) {
596
+ const transport = describeTelegramTransportError(error);
597
+ return transport ? { ...details, transport } : details;
598
+ }
599
+ async function callTelegramWithRetry(method, request, options) {
600
+ const retrySafe = options?.retrySafety === "safe" ||
601
+ (options?.retrySafety !== "non-idempotent" &&
602
+ isTelegramApiMethodRetrySafe(method));
603
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
604
+ const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
605
+ const waitBeforeRetry = async (error, attempt) => {
606
+ const ms = getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs);
607
+ if (!options?.signal?.aborted &&
608
+ error instanceof TelegramApiHttpError &&
609
+ error.status === 429) {
610
+ options?.onRetryWait?.({
611
+ method,
612
+ delayMs: ms,
613
+ attempt,
614
+ ...(error.retryAfterSeconds !== undefined
615
+ ? { retryAfterSeconds: error.retryAfterSeconds }
616
+ : {}),
617
+ });
618
+ }
619
+ if (options?.sleep)
620
+ await options.sleep(ms);
621
+ else
622
+ await sleepTelegramRetry(ms, options?.signal);
623
+ throwIfTelegramApiCallAborted(options?.signal);
624
+ };
625
+ for (let attempt = 0;; attempt += 1) {
626
+ throwIfTelegramApiCallAborted(options?.signal);
627
+ try {
628
+ return unwrapTelegramApiResult(method, await parseTelegramApiResponse(await callTelegramTransportRequest(request, retrySafe), method));
629
+ }
630
+ catch (error) {
631
+ const retryable = isRetryableTelegramApiError(error) &&
632
+ !(options?.retryRateLimit === false &&
633
+ error instanceof TelegramApiHttpError &&
634
+ error.status === 429);
635
+ if (!retrySafe) {
636
+ if (error instanceof TelegramApiHttpError && error.status === 429) {
637
+ if (attempt >= maxAttempts - 1)
638
+ throw error;
639
+ await waitBeforeRetry(error, attempt);
640
+ continue;
641
+ }
642
+ if (error instanceof TelegramApiMalformedSuccessError ||
643
+ isTelegramTransportFailure(error) ||
644
+ (error instanceof TelegramApiHttpError &&
645
+ error.status !== undefined &&
646
+ error.status >= 500)) {
647
+ throw new TelegramApiCommitUnknownError(method, error);
648
+ }
649
+ throw error;
650
+ }
651
+ if (attempt >= maxAttempts - 1 || !retryable)
652
+ throw error;
653
+ await waitBeforeRetry(error, attempt);
654
+ }
655
+ }
656
+ }
657
+ export async function cleanupTelegramTempFiles(tempDir, maxAgeMs, now = Date.now()) {
658
+ let removedCount = 0;
659
+ let entries;
660
+ try {
661
+ entries = await readdir(tempDir, { withFileTypes: true });
662
+ }
663
+ catch {
664
+ return 0;
665
+ }
666
+ for (const entry of entries) {
667
+ if (!entry.isFile() || !TELEGRAM_TEMP_SCRATCH_FILE_PATTERN.test(entry.name)) {
668
+ continue;
669
+ }
670
+ const path = join(tempDir, entry.name);
671
+ try {
672
+ const stats = await stat(path);
673
+ if (now - stats.mtimeMs <= maxAgeMs)
674
+ continue;
675
+ await unlink(path);
676
+ removedCount += 1;
677
+ }
678
+ catch {
679
+ // ignore
680
+ }
681
+ }
682
+ return removedCount;
683
+ }
684
+ export async function prepareTelegramTempDir(tempDir, maxAgeMs) {
685
+ await mkdir(tempDir, { recursive: true, mode: 0o700 });
686
+ return cleanupTelegramTempFiles(tempDir, maxAgeMs);
687
+ }
688
+ function assertTelegramBotTokenConfigured(botToken) {
689
+ if (!botToken)
690
+ throw new Error("Telegram bot token is not configured");
691
+ return botToken;
692
+ }
693
+ export async function callTelegram(botToken, method, body, options) {
694
+ const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
695
+ try {
696
+ return await callTelegramWithRetry(method, async (family) => telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
697
+ method: "POST",
698
+ headers: { "content-type": "application/json" },
699
+ body: JSON.stringify(body),
700
+ signal: options?.signal,
701
+ }, family), options);
702
+ }
703
+ catch (error) {
704
+ attachTelegramApiRequestTarget(error, body);
705
+ throw error;
706
+ }
707
+ }
708
+ export async function fetchTelegramBotIdentity(botToken, fetchImpl = fetch) {
709
+ const url = `${TELEGRAM_API_BASE}/bot${botToken}/getMe`;
710
+ const response = await callTelegramTransportRequest((family) => fetchImpl === fetch ? telegramFetch(url, {}, family) : fetchImpl(url));
711
+ return response.json();
712
+ }
713
+ /**
714
+ * Low-level helper to send a multipart/form-data request to the Telegram Bot API.
715
+ * This is the core implementation used for uploading voice messages, photos,
716
+ * documents, animations, etc. It handles FormData construction, retry logic
717
+ * (via callTelegramWithRetry), and error recording under the "multipart" category.
718
+ */
719
+ export async function callTelegramMultipart(botToken, method, fields, fileField, filePath, fileName, options) {
720
+ const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
721
+ const fileBlob = await openAsBlob(filePath);
722
+ try {
723
+ return await callTelegramWithRetry(method, async (family) => {
724
+ if (family) {
725
+ const multipart = await buildTelegramMultipartBody(fields, fileField, fileBlob, fileName);
726
+ return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
727
+ method: "POST",
728
+ headers: { "content-type": multipart.contentType },
729
+ body: multipart.body,
730
+ signal: options?.signal,
731
+ }, family);
732
+ }
733
+ const form = new FormData();
734
+ for (const [key, value] of Object.entries(fields)) {
735
+ form.set(key, value);
736
+ }
737
+ form.set(fileField, fileBlob, fileName);
738
+ return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
739
+ method: "POST",
740
+ body: form,
741
+ signal: options?.signal,
742
+ });
743
+ }, options);
744
+ }
745
+ catch (error) {
746
+ attachTelegramApiRequestTarget(error, fields);
747
+ throw error;
748
+ }
749
+ }
750
+ export async function downloadTelegramFile(botToken, fileId, suggestedName, tempDir, options) {
751
+ const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
752
+ const file = await callTelegram(configuredBotToken, "getFile", { file_id: fileId }, { signal: options?.signal });
753
+ assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
754
+ await mkdir(tempDir, { recursive: true, mode: 0o700 });
755
+ const targetPath = join(tempDir, `${randomUUID()}-${sanitizeFileName(suggestedName)}`);
756
+ const response = await callTelegramTransportRequest((family) => telegramFetch(`${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`, { signal: options?.signal }, family));
757
+ if (!response.ok) {
758
+ throw new Error(`Failed to download Telegram file: ${response.status}`);
759
+ }
760
+ const contentLength = response.headers?.get("content-length");
761
+ assertTelegramFileSizeWithinLimit(contentLength ? Number.parseInt(contentLength, 10) : undefined, options?.maxFileSizeBytes);
762
+ try {
763
+ await writeTelegramDownloadResponse(response, targetPath, options?.maxFileSizeBytes);
764
+ }
765
+ catch (error) {
766
+ await removeTelegramPartialDownload(targetPath);
767
+ throw error;
768
+ }
769
+ return targetPath;
770
+ }
771
+ export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
772
+ try {
773
+ await callTelegram(botToken, "answerCallbackQuery", text
774
+ ? { callback_query_id: callbackQueryId, text }
775
+ : { callback_query_id: callbackQueryId });
776
+ }
777
+ catch (error) {
778
+ options.recordRuntimeEvent?.("api", error, withTelegramTransportDiagnostics(error, {
779
+ method: "answerCallbackQuery",
780
+ }));
781
+ }
782
+ }
783
+ export async function deleteTelegramMessage(botToken, chatId, messageId) {
784
+ try {
785
+ await callTelegram(botToken, "deleteMessage", {
786
+ chat_id: chatId,
787
+ message_id: messageId,
788
+ });
789
+ }
790
+ catch {
791
+ // ignore
792
+ }
793
+ }
794
+ export function createTelegramChatActionSender(sendChatAction, action) {
795
+ return (chatId, options) => sendChatAction(chatId, action, options);
796
+ }
797
+ export function createTelegramNativeMarkdownDraftSender(deps) {
798
+ return (chatId, draftId, text, options) => {
799
+ if (text === undefined) {
800
+ return deps.sendMessageDraft(chatId, draftId, text, options);
801
+ }
802
+ return deps.sendRichMessageDraft({
803
+ chat_id: chatId,
804
+ draft_id: draftId,
805
+ rich_message: { markdown: text },
806
+ ...(options?.message_thread_id !== undefined
807
+ ? { message_thread_id: options.message_thread_id }
808
+ : {}),
809
+ });
810
+ };
811
+ }
812
+ export function createTelegramAssistantDraftSender(deps) {
813
+ const sendNativeDraft = createTelegramNativeMarkdownDraftSender(deps);
814
+ return (chatId, draftId, text, options) => {
815
+ if (text === undefined || deps.getAssistantRenderingMode() === "rich") {
816
+ return sendNativeDraft(chatId, draftId, text, options);
817
+ }
818
+ return deps.sendMessageDraft(chatId, draftId, deps.renderMarkdownToHtmlDraft(text), {
819
+ ...options,
820
+ parse_mode: "HTML",
821
+ });
822
+ };
823
+ }
824
+ export function buildTelegramAnswerGuestQueryBody(guestQueryId, text, options) {
825
+ const body = { guest_query_id: guestQueryId };
826
+ if (options?.result) {
827
+ body.result = options.result;
828
+ }
829
+ else if (text !== undefined || options?.richMessage) {
830
+ const inputContent = options?.richMessage
831
+ ? { rich_message: options.richMessage }
832
+ : { message_text: text };
833
+ if (!options?.richMessage && options?.parseMode) {
834
+ inputContent.parse_mode = options.parseMode;
835
+ }
836
+ body.result = {
837
+ type: "article",
838
+ id: "1",
839
+ title: "Response",
840
+ input_message_content: inputContent,
841
+ };
842
+ }
843
+ return body;
844
+ }
845
+ export function createDefaultTelegramBridgeApiRuntime(deps) {
846
+ const client = createTelegramApiClient(deps.getBotToken, {
847
+ recordRuntimeEvent: deps.recordRuntimeEvent,
848
+ });
849
+ const admittedClient = deps.workspaceAdmission
850
+ ? createTelegramApiWorkspaceAdmissionClient(client, deps.workspaceAdmission, {
851
+ onReleaseError(error, method) {
852
+ deps.recordRuntimeEvent("api", error, {
853
+ phase: "workspace-admission-release",
854
+ method,
855
+ });
856
+ },
857
+ })
858
+ : client;
859
+ return createTelegramBridgeApiRuntime({
860
+ client: deps.targetActivity
861
+ ? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
862
+ : admittedClient,
863
+ tempDir: getTelegramApiTempDir(),
864
+ maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
865
+ tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
866
+ recordRuntimeEvent: deps.recordRuntimeEvent,
867
+ captureRequestErrorHandler: deps.captureRequestErrorHandler,
868
+ });
869
+ }
870
+ export function createTelegramBridgeApiRuntime(deps) {
871
+ const recoverRequestError = async (handler, error) => {
872
+ try {
873
+ await handler?.(error);
874
+ }
875
+ catch (recoveryError) {
876
+ deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
877
+ }
878
+ };
879
+ const now = deps.now ?? Date.now;
880
+ const chatActionMinIntervalMs = Math.max(0, deps.chatActionMinIntervalMs ?? 2_000);
881
+ const chatActionMaxGates = Math.max(1, deps.chatActionMaxGates ?? 256);
882
+ const chatActionGates = new Map();
883
+ const getChatActionKey = (method, body) => {
884
+ if (method !== "sendChatAction")
885
+ return undefined;
886
+ const chatId = body.chat_id;
887
+ const action = body.action;
888
+ if ((typeof chatId !== "number" && typeof chatId !== "string") ||
889
+ typeof action !== "string") {
890
+ return undefined;
891
+ }
892
+ const threadId = body.message_thread_id;
893
+ return `${String(chatId)}:${typeof threadId === "number" || typeof threadId === "string"
894
+ ? String(threadId)
895
+ : "all"}:${action}`;
896
+ };
897
+ const callRecorded = async (method, body, options) => {
898
+ const recoverError = deps.captureRequestErrorHandler?.(body);
899
+ const chatActionKey = getChatActionKey(method, body);
900
+ if (chatActionKey) {
901
+ const nowMs = now();
902
+ for (const [key, candidate] of chatActionGates) {
903
+ if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
904
+ chatActionGates.delete(key);
905
+ }
906
+ }
907
+ let gate = chatActionGates.get(chatActionKey);
908
+ if (!gate) {
909
+ if (chatActionGates.size >= chatActionMaxGates)
910
+ return true;
911
+ gate = { notBeforeMs: 0 };
912
+ chatActionGates.set(chatActionKey, gate);
913
+ }
914
+ if (gate.inFlight)
915
+ return (await gate.inFlight);
916
+ if (now() < gate.notBeforeMs)
917
+ return true;
918
+ let request;
919
+ request = Promise.resolve()
920
+ .then(() => deps.client.call(method, body, {
921
+ ...options,
922
+ retryRateLimit: false,
923
+ }))
924
+ .then((result) => {
925
+ gate.notBeforeMs = now() + chatActionMinIntervalMs;
926
+ return result;
927
+ })
928
+ .catch(async (error) => {
929
+ await recoverRequestError(recoverError, error);
930
+ if (error instanceof TelegramApiHttpError && error.status === 429) {
931
+ const retryAfterMs = Math.max(chatActionMinIntervalMs, (error.retryAfterSeconds ?? 0) * 1_000);
932
+ gate.notBeforeMs = now() + retryAfterMs;
933
+ deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
934
+ method,
935
+ rateLimited: true,
936
+ retryAfterMs,
937
+ }));
938
+ return true;
939
+ }
940
+ deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
941
+ throw error;
942
+ })
943
+ .finally(() => {
944
+ if (gate.inFlight === request)
945
+ gate.inFlight = undefined;
946
+ });
947
+ gate.inFlight = request;
948
+ return request;
949
+ }
950
+ try {
951
+ return await deps.client.call(method, body, options);
952
+ }
953
+ catch (error) {
954
+ await recoverRequestError(recoverError, error);
955
+ if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
956
+ error.status === 400 && error.message ===
957
+ "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
958
+ return true;
959
+ }
960
+ deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
961
+ throw error;
962
+ }
963
+ };
964
+ return {
965
+ call: callRecorded,
966
+ /**
967
+ * Sends a multipart/form-data request (used for sending voice messages,
968
+ * photos, documents, animations, etc.).
969
+ * Errors are recorded under the "multipart" category for diagnostics.
970
+ */
971
+ callMultipart: async (method, fields, fileField, filePath, fileName, options) => {
972
+ const recoverError = deps.captureRequestErrorHandler?.(fields);
973
+ try {
974
+ return await deps.client.callMultipart(method, fields, fileField, filePath, fileName, options);
975
+ }
976
+ catch (error) {
977
+ await recoverRequestError(recoverError, error);
978
+ deps.recordRuntimeEvent("multipart", error, withTelegramTransportDiagnostics(error, { method, fileName }));
979
+ throw error;
980
+ }
981
+ },
982
+ /**
983
+ * Downloads a file from the Telegram servers into the local temp directory.
984
+ * Used for inbound voice messages, photos, documents, etc.
985
+ */
986
+ downloadFile: async (fileId, suggestedName) => {
987
+ try {
988
+ return await deps.client.downloadFile(fileId, suggestedName, deps.tempDir, {
989
+ maxFileSizeBytes: deps.maxFileSizeBytes,
990
+ });
991
+ }
992
+ catch (error) {
993
+ deps.recordRuntimeEvent("download", error, withTelegramTransportDiagnostics(error, { suggestedName }));
994
+ throw error;
995
+ }
996
+ },
997
+ deleteWebhook: (signal) => callRecorded("deleteWebhook", { drop_pending_updates: false }, { signal }),
998
+ getUpdates: (body, signal) => callRecorded("getUpdates", body, { signal }),
999
+ setMyCommands: (commands) => callRecorded("setMyCommands", { commands }),
1000
+ sendChatAction: (chatId, action, options) => callRecorded("sendChatAction", {
1001
+ chat_id: chatId,
1002
+ action,
1003
+ ...(options?.message_thread_id !== undefined
1004
+ ? { message_thread_id: options.message_thread_id }
1005
+ : {}),
1006
+ }),
1007
+ sendTypingAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
1008
+ chat_id: chatId,
1009
+ action,
1010
+ ...(options?.message_thread_id !== undefined
1011
+ ? { message_thread_id: options.message_thread_id }
1012
+ : {}),
1013
+ }), "typing"),
1014
+ sendRecordVoiceAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
1015
+ chat_id: chatId,
1016
+ action,
1017
+ ...(options?.message_thread_id !== undefined
1018
+ ? { message_thread_id: options.message_thread_id }
1019
+ : {}),
1020
+ }), "record_voice"),
1021
+ sendMessageDraft: (chatId, draftId, text, options) => {
1022
+ const body = {
1023
+ chat_id: chatId,
1024
+ draft_id: draftId,
1025
+ };
1026
+ if (text !== undefined)
1027
+ body.text = text;
1028
+ if (options?.parse_mode !== undefined)
1029
+ body.parse_mode = options.parse_mode;
1030
+ if (options?.entities !== undefined)
1031
+ body.entities = options.entities;
1032
+ if (options?.message_thread_id !== undefined)
1033
+ body.message_thread_id = options.message_thread_id;
1034
+ return callRecorded("sendMessageDraft", body);
1035
+ },
1036
+ sendMessage: (body) => callRecorded("sendMessage", body),
1037
+ sendRichMessage: (body) => callRecorded("sendRichMessage", body),
1038
+ sendRichMessageDraft: (body) => callRecorded("sendRichMessageDraft", body),
1039
+ editMessageText: async (body) => {
1040
+ const recoverError = deps.captureRequestErrorHandler?.(body);
1041
+ try {
1042
+ await deps.client.call("editMessageText", body);
1043
+ return "edited";
1044
+ }
1045
+ catch (error) {
1046
+ if (isTelegramMessageNotModifiedError(error))
1047
+ return "unchanged";
1048
+ await recoverRequestError(recoverError, error);
1049
+ deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
1050
+ method: "editMessageText",
1051
+ }));
1052
+ throw error;
1053
+ }
1054
+ },
1055
+ editMessageReplyMarkup: async (chatId, messageId, replyMarkup) => {
1056
+ await callRecorded("editMessageReplyMarkup", {
1057
+ chat_id: chatId,
1058
+ message_id: messageId,
1059
+ reply_markup: replyMarkup,
1060
+ });
1061
+ },
1062
+ answerCallbackQuery: async (callbackQueryId, text) => {
1063
+ try {
1064
+ await deps.client.answerCallbackQuery(callbackQueryId, text);
1065
+ }
1066
+ catch (error) {
1067
+ deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
1068
+ method: "answerCallbackQuery",
1069
+ }));
1070
+ }
1071
+ },
1072
+ answerGuestQuery: (guestQueryId, text, options) => callRecorded("answerGuestQuery", buildTelegramAnswerGuestQueryBody(guestQueryId, text, options)),
1073
+ answerGuestQueryForInlineMessage: async (guestQueryId, text, options) => {
1074
+ const sent = await callRecorded("answerGuestQuery", buildTelegramAnswerGuestQueryBody(guestQueryId, text, options));
1075
+ return sent?.inline_message_id;
1076
+ },
1077
+ editGuestInlineMessage: async (inlineMessageId, content) => {
1078
+ await callRecorded("editMessageText", {
1079
+ inline_message_id: inlineMessageId,
1080
+ ...(content.richMessage
1081
+ ? { rich_message: content.richMessage }
1082
+ : { text: content.text }),
1083
+ ...(content.parseMode ? { parse_mode: content.parseMode } : {}),
1084
+ });
1085
+ },
1086
+ prepareTempDir: () => prepareTelegramTempDir(deps.tempDir, deps.tempFileMaxAgeMs),
1087
+ deleteMessage: (chatId, messageId) => callRecorded("deleteMessage", {
1088
+ chat_id: chatId,
1089
+ message_id: messageId,
1090
+ }).then(() => { }),
1091
+ };
1092
+ }
1093
+ /**
1094
+ * Creates a low-level Telegram Bot API client.
1095
+ * This is the main entry point for all direct Bot API communication
1096
+ * (both JSON calls and multipart uploads for files/voice).
1097
+ */
1098
+ export function createTelegramApiClient(getBotToken, options = {}) {
1099
+ const now = options.now ?? Date.now;
1100
+ const recordRuntimeEvent = options.recordRuntimeEvent;
1101
+ const draftRetryNotBeforeByTarget = new Map();
1102
+ return {
1103
+ call: async (method, body, options) => {
1104
+ const token = getBotToken();
1105
+ // Cooldown keys retain only the public bot-id prefix, not the credential.
1106
+ const botId = token?.match(/^(\d+):/)?.[1];
1107
+ const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
1108
+ const draftKey = isDraft && botId
1109
+ ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
1110
+ : undefined;
1111
+ if (draftKey) {
1112
+ const nowMs = now();
1113
+ for (const [key, deadline] of draftRetryNotBeforeByTarget) {
1114
+ if (nowMs >= deadline)
1115
+ draftRetryNotBeforeByTarget.delete(key);
1116
+ }
1117
+ if (draftRetryNotBeforeByTarget.has(draftKey))
1118
+ return false;
1119
+ }
1120
+ try {
1121
+ // A draft is a replaceable snapshot, not a body to replay after backoff.
1122
+ const retryWaitOptions = recordRuntimeEvent
1123
+ ? {
1124
+ onRetryWait: (wait) => {
1125
+ recordRuntimeEvent("api", new Error(`Telegram API rate limit: waiting ${wait.delayMs} ms before retrying ${wait.method}`), {
1126
+ phase: "retry-wait",
1127
+ method: wait.method,
1128
+ waitMs: wait.delayMs,
1129
+ attempt: wait.attempt,
1130
+ ...(wait.retryAfterSeconds !== undefined
1131
+ ? { retryAfterSeconds: wait.retryAfterSeconds }
1132
+ : {}),
1133
+ });
1134
+ },
1135
+ }
1136
+ : {};
1137
+ return await callTelegram(token, method, body, {
1138
+ ...(isDraft ? { ...options, maxAttempts: 1 } : options),
1139
+ ...retryWaitOptions,
1140
+ });
1141
+ }
1142
+ catch (error) {
1143
+ if (draftKey && isRetryableTelegramApiError(error)) {
1144
+ draftRetryNotBeforeByTarget.set(draftKey, Math.max(draftRetryNotBeforeByTarget.get(draftKey) ?? 0, now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500)));
1145
+ }
1146
+ throw error;
1147
+ }
1148
+ },
1149
+ callMultipart: async (method, fields, fileField, filePath, fileName, options) => {
1150
+ return callTelegramMultipart(getBotToken(), method, fields, fileField, filePath, fileName, options);
1151
+ },
1152
+ downloadFile: async (fileId, suggestedName, tempDir, options) => {
1153
+ return downloadTelegramFile(getBotToken(), fileId, suggestedName, tempDir, options);
1154
+ },
1155
+ answerCallbackQuery: async (callbackQueryId, text) => {
1156
+ await answerTelegramCallbackQuery(getBotToken(), callbackQueryId, text, options);
1157
+ },
1158
+ };
1159
+ }