@llblab/pi-telegram 0.52.1 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -8
- package/BACKLOG.md +1 -16
- package/CHANGELOG.md +17 -0
- package/README.md +8 -10
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +640 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +34 -3
- package/dist/lib/locks.js +318 -119
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +23 -37
- package/dist/lib/polling.js +71 -52
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +6 -3
- package/dist/lib/sync.js +112 -45
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +165 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +36 -30
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1207 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +703 -212
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +165 -149
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +488 -359
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +322 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/dist/lib/telegram-api.js
CHANGED
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
10
|
-
import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, readdir, rename, stat, unlink, writeFile, } from "node:fs/promises";
|
|
11
11
|
import { request as requestHttps } from "node:https";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { resolveTelegramAttachmentsDir } from "./paths.js";
|
|
14
13
|
import { Readable, Transform } from "node:stream";
|
|
15
14
|
import { pipeline } from "node:stream/promises";
|
|
15
|
+
import { resolveTelegramAttachmentsDir } from "./paths.js";
|
|
16
16
|
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
17
17
|
export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
18
|
-
|
|
18
|
+
/** First positive safe-integer byte limit among ordered env names, else the caller's default. */
|
|
19
|
+
export function getTelegramByteLimitFromEnv(env, names, defaultValue) {
|
|
19
20
|
for (const name of names) {
|
|
20
21
|
const rawValue = env[name]?.trim();
|
|
21
22
|
if (!rawValue)
|
|
@@ -30,8 +31,9 @@ function getTelegramApiTempDir() {
|
|
|
30
31
|
return resolveTelegramAttachmentsDir();
|
|
31
32
|
}
|
|
32
33
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
34
|
+
const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
|
|
33
35
|
const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
|
|
34
|
-
const TELEGRAM_INBOUND_FILE_MAX_BYTES =
|
|
36
|
+
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
|
|
35
37
|
const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
|
|
36
38
|
const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
|
|
37
39
|
"auto",
|
|
@@ -39,10 +41,32 @@ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
|
|
|
39
41
|
"ipv6",
|
|
40
42
|
"ipv4-fallback",
|
|
41
43
|
]);
|
|
44
|
+
export class TelegramApiAuthorityError extends Error {
|
|
45
|
+
requestIssued;
|
|
46
|
+
constructor(requestIssued) {
|
|
47
|
+
super("Telegram API call authority is unavailable.");
|
|
48
|
+
this.name = "TelegramApiAuthorityError";
|
|
49
|
+
this.requestIssued = requestIssued;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
|
|
53
|
+
export function assertTelegramApiCallAuthority(assertAuthority, requestIssued) {
|
|
54
|
+
try {
|
|
55
|
+
assertAuthority?.();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new TelegramApiAuthorityError(requestIssued);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
42
61
|
function parseTelegramApiTargetInteger(value) {
|
|
43
|
-
const parsed = typeof value === "number"
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
const parsed = typeof value === "number"
|
|
63
|
+
? value
|
|
64
|
+
: typeof value === "string" && /^-?\d+$/u.test(value)
|
|
65
|
+
? Number(value)
|
|
66
|
+
: undefined;
|
|
67
|
+
return parsed !== undefined && Number.isSafeInteger(parsed)
|
|
68
|
+
? parsed
|
|
69
|
+
: undefined;
|
|
46
70
|
}
|
|
47
71
|
export function createTelegramApiTargetActivityRuntime() {
|
|
48
72
|
const pending = new Map();
|
|
@@ -58,8 +82,10 @@ export function createTelegramApiTargetActivityRuntime() {
|
|
|
58
82
|
const chatId = parseTelegramApiTargetInteger(body.chat_id);
|
|
59
83
|
const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
|
|
60
84
|
const messageId = parseTelegramApiTargetInteger(body.message_id);
|
|
61
|
-
const chatScoped = chatId !== undefined &&
|
|
62
|
-
|
|
85
|
+
const chatScoped = chatId !== undefined &&
|
|
86
|
+
threadId === undefined &&
|
|
87
|
+
messageId !== undefined &&
|
|
88
|
+
messageScopedMethods.has(method);
|
|
63
89
|
if (chatId === undefined || (threadId === undefined && !chatScoped)) {
|
|
64
90
|
return () => undefined;
|
|
65
91
|
}
|
|
@@ -69,7 +95,10 @@ export function createTelegramApiTargetActivityRuntime() {
|
|
|
69
95
|
if (existing)
|
|
70
96
|
existing.count += 1;
|
|
71
97
|
else
|
|
72
|
-
pending.set(key, {
|
|
98
|
+
pending.set(key, {
|
|
99
|
+
target: { chatId, threadId: threadId },
|
|
100
|
+
count: 1,
|
|
101
|
+
});
|
|
73
102
|
}
|
|
74
103
|
else {
|
|
75
104
|
pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
|
|
@@ -95,8 +124,9 @@ export function createTelegramApiTargetActivityRuntime() {
|
|
|
95
124
|
};
|
|
96
125
|
},
|
|
97
126
|
hasPendingTarget(target) {
|
|
98
|
-
return pendingChats.has(target.chatId) ||
|
|
99
|
-
(target.threadId !== undefined &&
|
|
127
|
+
return (pendingChats.has(target.chatId) ||
|
|
128
|
+
(target.threadId !== undefined &&
|
|
129
|
+
pending.has(`${target.chatId}:${target.threadId}`)));
|
|
100
130
|
},
|
|
101
131
|
listPendingTargets() {
|
|
102
132
|
return Array.from(pending.values(), ({ target }) => ({ ...target }));
|
|
@@ -215,14 +245,27 @@ function sanitizeFileName(name) {
|
|
|
215
245
|
export function createTelegramAttachmentFileName(source, generatedName, botScope) {
|
|
216
246
|
const chat = source.chat;
|
|
217
247
|
const clean = (value) => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
|
|
218
|
-
const scope = source.scope
|
|
219
|
-
?
|
|
220
|
-
: chat
|
|
248
|
+
const scope = source.scope
|
|
249
|
+
? clean(source.scope)
|
|
250
|
+
: chat?.type === "private" || !chat
|
|
251
|
+
? botScope
|
|
252
|
+
? clean(botScope)
|
|
253
|
+
: chat
|
|
254
|
+
? String(Math.abs(chat.id))
|
|
255
|
+
: ""
|
|
256
|
+
: chat.username
|
|
257
|
+
? clean(chat.username)
|
|
258
|
+
: String(Math.abs(chat.id));
|
|
221
259
|
const dot = generatedName.lastIndexOf(".");
|
|
222
260
|
const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
|
|
223
|
-
const tail = source.userFileName
|
|
224
|
-
|
|
225
|
-
|
|
261
|
+
const tail = source.userFileName
|
|
262
|
+
? `-${clean(source.userFileName)}`
|
|
263
|
+
: extension;
|
|
264
|
+
return ([sanitizeFileName(source.kind), scope, String(source.messageId)]
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.join("-") +
|
|
267
|
+
(source.index !== undefined ? `-${source.index}` : "") +
|
|
268
|
+
tail);
|
|
226
269
|
}
|
|
227
270
|
class TelegramApiMalformedSuccessError extends Error {
|
|
228
271
|
constructor(method, detail) {
|
|
@@ -287,8 +330,9 @@ export function isTelegramStaleTargetHttpError(error) {
|
|
|
287
330
|
}
|
|
288
331
|
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
289
332
|
export function isTelegramApiRequestRejected(error, method) {
|
|
290
|
-
return error instanceof TelegramApiHttpError &&
|
|
291
|
-
error.rejectedRequestMethod
|
|
333
|
+
return (error instanceof TelegramApiHttpError &&
|
|
334
|
+
error.rejectedRequestMethod !== undefined &&
|
|
335
|
+
error.rejectedRequestMethod === method);
|
|
292
336
|
}
|
|
293
337
|
export function isTelegramMessageNotModifiedError(error) {
|
|
294
338
|
return (error instanceof Error && error.message.includes("message is not modified"));
|
|
@@ -321,13 +365,15 @@ export function isRetryableTelegramApiError(error) {
|
|
|
321
365
|
(error.status !== undefined && error.status >= 500)));
|
|
322
366
|
}
|
|
323
367
|
export function getTelegramApiRetryAfterMs(error) {
|
|
324
|
-
return error instanceof TelegramApiHttpError &&
|
|
368
|
+
return error instanceof TelegramApiHttpError &&
|
|
369
|
+
error.retryAfterSeconds !== undefined
|
|
325
370
|
? Math.max(0, error.retryAfterSeconds * 1000)
|
|
326
371
|
: undefined;
|
|
327
372
|
}
|
|
328
373
|
export function isTelegramMessageUnavailableError(error) {
|
|
329
|
-
return error instanceof TelegramApiHttpError &&
|
|
330
|
-
|
|
374
|
+
return (error instanceof TelegramApiHttpError &&
|
|
375
|
+
error.status === 400 &&
|
|
376
|
+
/Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message));
|
|
331
377
|
}
|
|
332
378
|
function getTelegramRetryDelayMs(error, attempt, baseDelayMs) {
|
|
333
379
|
if (error instanceof TelegramApiHttpError &&
|
|
@@ -391,6 +437,24 @@ async function writeTelegramDownloadResponse(response, targetPath, maxFileSizeBy
|
|
|
391
437
|
}
|
|
392
438
|
await pipeline(Readable.from(response.body, { objectMode: false }), createTelegramDownloadLimitTransform(maxFileSizeBytes), createWriteStream(targetPath, { mode: 0o600 }));
|
|
393
439
|
}
|
|
440
|
+
async function publishTelegramDownload(partPath, targetPath, signal) {
|
|
441
|
+
for (let attempt = 0;; attempt++) {
|
|
442
|
+
throwIfTelegramApiCallAborted(signal);
|
|
443
|
+
try {
|
|
444
|
+
await rename(partPath, targetPath);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
const code = error?.code;
|
|
449
|
+
// Windows may briefly deny replacement while another publisher/scanner holds the file.
|
|
450
|
+
if (process.platform !== "win32" ||
|
|
451
|
+
(code !== "EPERM" && code !== "EACCES") ||
|
|
452
|
+
attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS)
|
|
453
|
+
throw error;
|
|
454
|
+
await sleepTelegramRetry(50 * 2 ** attempt, signal);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
394
458
|
async function removeTelegramPartialDownload(path) {
|
|
395
459
|
try {
|
|
396
460
|
await unlink(path);
|
|
@@ -421,8 +485,11 @@ async function parseTelegramApiResponse(response, method) {
|
|
|
421
485
|
const retryAfterHeader = response.headers?.get("retry-after");
|
|
422
486
|
const retryAfterSeconds = data?.parameters?.retry_after ??
|
|
423
487
|
(retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
|
|
424
|
-
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false &&
|
|
425
|
-
|
|
488
|
+
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false &&
|
|
489
|
+
data.error_code === response.status &&
|
|
490
|
+
[400, 401, 403, 404, 429].includes(response.status)
|
|
491
|
+
? method
|
|
492
|
+
: undefined);
|
|
426
493
|
}
|
|
427
494
|
if (!data) {
|
|
428
495
|
throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
|
|
@@ -621,6 +688,12 @@ function withTelegramTransportDiagnostics(error, details) {
|
|
|
621
688
|
return transport ? { ...details, transport } : details;
|
|
622
689
|
}
|
|
623
690
|
async function callTelegramWithRetry(method, request, options) {
|
|
691
|
+
const assertAuthority = options?.assertAuthority;
|
|
692
|
+
let requestIssued = false;
|
|
693
|
+
const beforeRequest = () => {
|
|
694
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
695
|
+
requestIssued = true;
|
|
696
|
+
};
|
|
624
697
|
const retrySafe = options?.retrySafety === "safe" ||
|
|
625
698
|
(options?.retrySafety !== "non-idempotent" &&
|
|
626
699
|
isTelegramApiMethodRetrySafe(method));
|
|
@@ -648,10 +721,17 @@ async function callTelegramWithRetry(method, request, options) {
|
|
|
648
721
|
};
|
|
649
722
|
for (let attempt = 0;; attempt += 1) {
|
|
650
723
|
throwIfTelegramApiCallAborted(options?.signal);
|
|
724
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
651
725
|
try {
|
|
652
|
-
|
|
726
|
+
const result = unwrapTelegramApiResult(method, await parseTelegramApiResponse(await callTelegramTransportRequest((family) => request(family, beforeRequest), retrySafe), method));
|
|
727
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
728
|
+
return result;
|
|
653
729
|
}
|
|
654
730
|
catch (error) {
|
|
731
|
+
// Authority refusal is never a transport failure, retry grant or acknowledged success.
|
|
732
|
+
if (error instanceof TelegramApiAuthorityError)
|
|
733
|
+
throw error;
|
|
734
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
655
735
|
const retryable = isRetryableTelegramApiError(error) &&
|
|
656
736
|
!(options?.retryRateLimit === false &&
|
|
657
737
|
error instanceof TelegramApiHttpError &&
|
|
@@ -717,12 +797,16 @@ function assertTelegramBotTokenConfigured(botToken) {
|
|
|
717
797
|
export async function callTelegram(botToken, method, body, options) {
|
|
718
798
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
719
799
|
try {
|
|
720
|
-
return await callTelegramWithRetry(method, async (family) =>
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
800
|
+
return await callTelegramWithRetry(method, async (family, beforeRequest) => {
|
|
801
|
+
const encodedBody = JSON.stringify(body);
|
|
802
|
+
beforeRequest();
|
|
803
|
+
return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: { "content-type": "application/json" },
|
|
806
|
+
body: encodedBody,
|
|
807
|
+
signal: options?.signal,
|
|
808
|
+
}, family);
|
|
809
|
+
}, options);
|
|
726
810
|
}
|
|
727
811
|
catch (error) {
|
|
728
812
|
attachTelegramApiRequestTarget(error, body);
|
|
@@ -742,11 +826,14 @@ export async function fetchTelegramBotIdentity(botToken, fetchImpl = fetch) {
|
|
|
742
826
|
*/
|
|
743
827
|
export async function callTelegramMultipart(botToken, method, fields, fileField, filePath, fileName, options) {
|
|
744
828
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
829
|
+
options = options && { ...options };
|
|
830
|
+
assertTelegramApiCallAuthority(options?.assertAuthority, false);
|
|
745
831
|
const fileBlob = await openAsBlob(filePath);
|
|
746
832
|
try {
|
|
747
|
-
return await callTelegramWithRetry(method, async (family) => {
|
|
833
|
+
return await callTelegramWithRetry(method, async (family, beforeRequest) => {
|
|
748
834
|
if (family) {
|
|
749
835
|
const multipart = await buildTelegramMultipartBody(fields, fileField, fileBlob, fileName);
|
|
836
|
+
beforeRequest();
|
|
750
837
|
return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
|
|
751
838
|
method: "POST",
|
|
752
839
|
headers: { "content-type": multipart.contentType },
|
|
@@ -759,6 +846,7 @@ export async function callTelegramMultipart(botToken, method, fields, fileField,
|
|
|
759
846
|
form.set(key, value);
|
|
760
847
|
}
|
|
761
848
|
form.set(fileField, fileBlob, fileName);
|
|
849
|
+
beforeRequest();
|
|
762
850
|
return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
|
|
763
851
|
method: "POST",
|
|
764
852
|
body: form,
|
|
@@ -787,7 +875,7 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
|
|
|
787
875
|
assertTelegramFileSizeWithinLimit(contentLength ? Number.parseInt(contentLength, 10) : undefined, options?.maxFileSizeBytes);
|
|
788
876
|
try {
|
|
789
877
|
await writeTelegramDownloadResponse(response, partPath, options?.maxFileSizeBytes);
|
|
790
|
-
await
|
|
878
|
+
await publishTelegramDownload(partPath, targetPath, options?.signal);
|
|
791
879
|
}
|
|
792
880
|
catch (error) {
|
|
793
881
|
await removeTelegramPartialDownload(partPath);
|
|
@@ -795,20 +883,20 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
|
|
|
795
883
|
}
|
|
796
884
|
return targetPath;
|
|
797
885
|
}
|
|
798
|
-
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
799
|
-
export function formatTelegramCallbackAnswerText(text) {
|
|
800
|
-
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
801
|
-
}
|
|
802
886
|
export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
|
|
803
887
|
try {
|
|
804
888
|
await callTelegram(botToken, "answerCallbackQuery", text
|
|
805
889
|
? { callback_query_id: callbackQueryId, text }
|
|
806
|
-
: { callback_query_id: callbackQueryId }
|
|
890
|
+
: { callback_query_id: callbackQueryId }, options.assertAuthority
|
|
891
|
+
? { assertAuthority: options.assertAuthority }
|
|
892
|
+
: undefined);
|
|
807
893
|
}
|
|
808
894
|
catch (error) {
|
|
809
895
|
options.recordRuntimeEvent?.("api", error, withTelegramTransportDiagnostics(error, {
|
|
810
896
|
method: "answerCallbackQuery",
|
|
811
897
|
}));
|
|
898
|
+
if (error instanceof TelegramApiAuthorityError)
|
|
899
|
+
throw error;
|
|
812
900
|
}
|
|
813
901
|
}
|
|
814
902
|
export function createTelegramChatActionSender(sendChatAction, action) {
|
|
@@ -893,7 +981,8 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
|
|
|
893
981
|
// The exclusive deletion permit replaces ordinary target admission.
|
|
894
982
|
const target = authorize();
|
|
895
983
|
const deleted = await client.call("deleteForumTopic", {
|
|
896
|
-
chat_id: target.chatId,
|
|
984
|
+
chat_id: target.chatId,
|
|
985
|
+
message_thread_id: target.threadId,
|
|
897
986
|
}, { maxAttempts: 1, retrySafety: "non-idempotent" });
|
|
898
987
|
if (deleted !== true)
|
|
899
988
|
throw new Error("Telegram Workspace Thread deletion was not confirmed.");
|
|
@@ -906,7 +995,9 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
906
995
|
await handler?.(error);
|
|
907
996
|
}
|
|
908
997
|
catch (recoveryError) {
|
|
909
|
-
deps.recordRuntimeEvent("api", recoveryError, {
|
|
998
|
+
deps.recordRuntimeEvent("api", recoveryError, {
|
|
999
|
+
phase: "stale-target-recovery",
|
|
1000
|
+
});
|
|
910
1001
|
}
|
|
911
1002
|
};
|
|
912
1003
|
const now = deps.now ?? Date.now;
|
|
@@ -1011,9 +1102,11 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1011
1102
|
}
|
|
1012
1103
|
catch (error) {
|
|
1013
1104
|
await recoverRequestError(recoverError, error);
|
|
1014
|
-
if (method === "deleteMessage" &&
|
|
1015
|
-
error
|
|
1016
|
-
|
|
1105
|
+
if (method === "deleteMessage" &&
|
|
1106
|
+
error instanceof TelegramApiHttpError &&
|
|
1107
|
+
error.status === 400 &&
|
|
1108
|
+
error.message ===
|
|
1109
|
+
"Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
|
|
1017
1110
|
return true;
|
|
1018
1111
|
}
|
|
1019
1112
|
deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
|
|
@@ -1057,7 +1150,7 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1057
1150
|
},
|
|
1058
1151
|
deleteWebhook: (signal) => callRecorded("deleteWebhook", { drop_pending_updates: false }, { signal }),
|
|
1059
1152
|
getUpdates: (body, signal) => callRecorded("getUpdates", body, { signal }),
|
|
1060
|
-
setMyCommands: (commands) => callRecorded("setMyCommands", { commands }),
|
|
1153
|
+
setMyCommands: (commands, options) => callRecorded("setMyCommands", { commands }, options),
|
|
1061
1154
|
sendChatAction: (chatId, action, options) => callRecorded("sendChatAction", {
|
|
1062
1155
|
chat_id: chatId,
|
|
1063
1156
|
action,
|
|
@@ -1094,13 +1187,13 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1094
1187
|
body.message_thread_id = options.message_thread_id;
|
|
1095
1188
|
return callRecorded("sendMessageDraft", body);
|
|
1096
1189
|
},
|
|
1097
|
-
sendMessage: (body) => callRecorded("sendMessage", body),
|
|
1098
|
-
sendRichMessage: (body) => callRecorded("sendRichMessage", body),
|
|
1190
|
+
sendMessage: (body, options) => callRecorded("sendMessage", body, options),
|
|
1191
|
+
sendRichMessage: (body, options) => callRecorded("sendRichMessage", body, options),
|
|
1099
1192
|
sendRichMessageDraft: (body) => callRecorded("sendRichMessageDraft", body),
|
|
1100
|
-
editMessageText: async (body) => {
|
|
1193
|
+
editMessageText: async (body, options) => {
|
|
1101
1194
|
const recoverError = deps.captureRequestErrorHandler?.(body);
|
|
1102
1195
|
try {
|
|
1103
|
-
await deps.client.call("editMessageText", body);
|
|
1196
|
+
await deps.client.call("editMessageText", body, options);
|
|
1104
1197
|
return "edited";
|
|
1105
1198
|
}
|
|
1106
1199
|
catch (error) {
|
|
@@ -1120,7 +1213,19 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1120
1213
|
reply_markup: replyMarkup,
|
|
1121
1214
|
});
|
|
1122
1215
|
},
|
|
1123
|
-
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
1216
|
+
answerCallbackQuery: async (callbackQueryId, text, options) => {
|
|
1217
|
+
if (options?.assertAuthority) {
|
|
1218
|
+
try {
|
|
1219
|
+
await callRecorded("answerCallbackQuery", text
|
|
1220
|
+
? { callback_query_id: callbackQueryId, text }
|
|
1221
|
+
: { callback_query_id: callbackQueryId }, { assertAuthority: options.assertAuthority });
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
if (error instanceof TelegramApiAuthorityError)
|
|
1225
|
+
throw error;
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1124
1229
|
try {
|
|
1125
1230
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
1126
1231
|
}
|
|
@@ -1128,6 +1233,8 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
1128
1233
|
deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
|
|
1129
1234
|
method: "answerCallbackQuery",
|
|
1130
1235
|
}));
|
|
1236
|
+
if (error instanceof TelegramApiAuthorityError)
|
|
1237
|
+
throw error;
|
|
1131
1238
|
}
|
|
1132
1239
|
},
|
|
1133
1240
|
answerGuestQuery: (guestQueryId, text, options) => callRecorded("answerGuestQuery", buildTelegramAnswerGuestQueryBody(guestQueryId, text, options)),
|
|
@@ -1202,7 +1309,8 @@ export function createTelegramApiClient(getBotToken, options = {}) {
|
|
|
1202
1309
|
}
|
|
1203
1310
|
catch (error) {
|
|
1204
1311
|
if (draftKey && isRetryableTelegramApiError(error)) {
|
|
1205
|
-
draftRetryNotBeforeByTarget.set(draftKey, Math.max(draftRetryNotBeforeByTarget.get(draftKey) ?? 0, now() +
|
|
1312
|
+
draftRetryNotBeforeByTarget.set(draftKey, Math.max(draftRetryNotBeforeByTarget.get(draftKey) ?? 0, now() +
|
|
1313
|
+
getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500)));
|
|
1206
1314
|
}
|
|
1207
1315
|
throw error;
|
|
1208
1316
|
}
|
|
@@ -1213,8 +1321,12 @@ export function createTelegramApiClient(getBotToken, options = {}) {
|
|
|
1213
1321
|
downloadFile: async (fileId, suggestedName, tempDir, options) => {
|
|
1214
1322
|
return downloadTelegramFile(getBotToken(), fileId, suggestedName, tempDir, options);
|
|
1215
1323
|
},
|
|
1216
|
-
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
1217
|
-
|
|
1324
|
+
answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
|
|
1325
|
+
const assertAuthority = authorityOptions?.assertAuthority;
|
|
1326
|
+
const callbackOptions = assertAuthority
|
|
1327
|
+
? { ...options, assertAuthority }
|
|
1328
|
+
: options;
|
|
1329
|
+
await answerTelegramCallbackQuery(getBotToken(), callbackQueryId, text, callbackOptions);
|
|
1218
1330
|
},
|
|
1219
1331
|
};
|
|
1220
1332
|
}
|
package/dist/lib/text-groups.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
|
|
5
5
|
*/
|
|
6
6
|
import { setTimeout as waitForTimeout } from "node:timers/promises";
|
|
7
|
-
import { extractTelegramMessageText, } from "./media.js";
|
|
7
|
+
import { createTelegramPendingGroupLifecycle, extractTelegramMessageText, } from "./media.js";
|
|
8
8
|
const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
|
|
9
9
|
const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
|
|
10
10
|
const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
|
|
@@ -155,6 +155,7 @@ export function createTelegramTextGroupController(options = {}) {
|
|
|
155
155
|
: (timer) => {
|
|
156
156
|
timer.abort();
|
|
157
157
|
});
|
|
158
|
+
const lifecycle = createTelegramPendingGroupLifecycle(groups, clearTimer);
|
|
158
159
|
return {
|
|
159
160
|
prepareUpdateBatch(updates) {
|
|
160
161
|
for (let index = 0; index + 1 < updates.length; index += 1) {
|
|
@@ -249,45 +250,9 @@ export function createTelegramTextGroupController(options = {}) {
|
|
|
249
250
|
: undefined,
|
|
250
251
|
});
|
|
251
252
|
},
|
|
252
|
-
|
|
253
|
-
for (const state of groups.values()) {
|
|
254
|
-
if (!state.messages.some((message) => message.message_id === messageId)) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (state.flushTimer)
|
|
258
|
-
clearTimer(state.flushTimer);
|
|
259
|
-
state.flushTimer = undefined;
|
|
260
|
-
await state.dispatchNow?.();
|
|
261
|
-
if (state.messages.some((message) => message.message_id === messageId) &&
|
|
262
|
-
!state.dispatching) {
|
|
263
|
-
await state.dispatchNow?.();
|
|
264
|
-
}
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
return false;
|
|
268
|
-
},
|
|
269
|
-
suspend: () => {
|
|
270
|
-
for (const state of groups.values()) {
|
|
271
|
-
state.suspended = true;
|
|
272
|
-
if (state.flushTimer)
|
|
273
|
-
clearTimer(state.flushTimer);
|
|
274
|
-
state.flushTimer = undefined;
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
resume: (context) => {
|
|
278
|
-
for (const state of groups.values()) {
|
|
279
|
-
state.context = context;
|
|
280
|
-
state.suspended = false;
|
|
281
|
-
if (!state.dispatching && !state.flushTimer)
|
|
282
|
-
state.reschedule?.();
|
|
283
|
-
}
|
|
284
|
-
},
|
|
253
|
+
...lifecycle,
|
|
285
254
|
clear: () => {
|
|
286
|
-
|
|
287
|
-
if (state.flushTimer)
|
|
288
|
-
clearTimer(state.flushTimer);
|
|
289
|
-
}
|
|
290
|
-
groups.clear();
|
|
255
|
+
lifecycle.clear();
|
|
291
256
|
plannedForwardCommentStarts.clear();
|
|
292
257
|
plannedForwardCommentEnds.clear();
|
|
293
258
|
},
|
|
@@ -64,8 +64,8 @@ export declare function planTelegramInactiveThreadCleanup(input: {
|
|
|
64
64
|
threadId: number;
|
|
65
65
|
}[];
|
|
66
66
|
}): TelegramThreadCleanupCandidate[];
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
/** Read ports for the bindings and target claims that cleanup evidence is captured from. */
|
|
68
|
+
export interface TelegramInactiveThreadCleanupEvidencePorts<TBinding extends TelegramThreadCleanupBindingSnapshot> {
|
|
69
69
|
listBindings(): readonly TBinding[];
|
|
70
70
|
getProtection(binding: TBinding): {
|
|
71
71
|
liveOwner: TelegramThreadCleanupProtectionState;
|
|
@@ -90,33 +90,12 @@ export declare function captureTelegramInactiveThreadCleanupEvidence<TBinding ex
|
|
|
90
90
|
threadId: number;
|
|
91
91
|
};
|
|
92
92
|
}[];
|
|
93
|
+
}
|
|
94
|
+
export declare function captureTelegramInactiveThreadCleanupEvidence<TBinding extends TelegramThreadCleanupBindingSnapshot>(input: TelegramInactiveThreadCleanupEvidencePorts<TBinding> & {
|
|
95
|
+
profileName: string;
|
|
93
96
|
}): Parameters<typeof planTelegramInactiveThreadCleanup>[0];
|
|
94
|
-
export declare function createTelegramInactiveThreadCleanupReviewRuntime<TBinding extends TelegramThreadCleanupBindingSnapshot>(deps: {
|
|
97
|
+
export declare function createTelegramInactiveThreadCleanupReviewRuntime<TBinding extends TelegramThreadCleanupBindingSnapshot>(deps: TelegramInactiveThreadCleanupEvidencePorts<TBinding> & {
|
|
95
98
|
getProfileName(): string;
|
|
96
|
-
listBindings(): readonly TBinding[];
|
|
97
|
-
getProtection(binding: TBinding): {
|
|
98
|
-
liveOwner: TelegramThreadCleanupProtectionState;
|
|
99
|
-
acceptedWork: TelegramThreadCleanupProtectionState;
|
|
100
|
-
deliveryAuthority: TelegramThreadCleanupProtectionState;
|
|
101
|
-
};
|
|
102
|
-
listReservations(): readonly {
|
|
103
|
-
target: {
|
|
104
|
-
chatId: number;
|
|
105
|
-
threadId: number;
|
|
106
|
-
};
|
|
107
|
-
}[];
|
|
108
|
-
listPendingProvisions(): readonly {
|
|
109
|
-
target?: {
|
|
110
|
-
chatId: number;
|
|
111
|
-
threadId: number;
|
|
112
|
-
};
|
|
113
|
-
}[];
|
|
114
|
-
listPendingCleanups(): readonly {
|
|
115
|
-
target: {
|
|
116
|
-
chatId: number;
|
|
117
|
-
threadId: number;
|
|
118
|
-
};
|
|
119
|
-
}[];
|
|
120
99
|
getWorkStore(): TelegramThreadCleanupWorkStore;
|
|
121
100
|
runWorkspaceOperation<T>(input: {
|
|
122
101
|
operationId: string;
|