@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/lib/telegram-api.ts
CHANGED
|
@@ -8,22 +8,31 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
mkdir,
|
|
13
|
+
readdir,
|
|
14
|
+
rename,
|
|
15
|
+
stat,
|
|
16
|
+
unlink,
|
|
17
|
+
writeFile,
|
|
18
|
+
} from "node:fs/promises";
|
|
12
19
|
import { request as requestHttps } from "node:https";
|
|
13
20
|
import { join } from "node:path";
|
|
14
|
-
import type { TelegramAttachmentSource } from "./media.ts";
|
|
15
|
-
import { resolveTelegramAttachmentsDir } from "./paths.ts";
|
|
16
21
|
import { Readable, Transform } from "node:stream";
|
|
17
22
|
import { pipeline } from "node:stream/promises";
|
|
18
23
|
|
|
24
|
+
import type { TelegramAttachmentSource } from "./media.ts";
|
|
25
|
+
import { resolveTelegramAttachmentsDir } from "./paths.ts";
|
|
26
|
+
|
|
19
27
|
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
20
28
|
|
|
21
29
|
export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
/** First positive safe-integer byte limit among ordered env names, else the caller's default. */
|
|
32
|
+
export function getTelegramByteLimitFromEnv(
|
|
24
33
|
env: NodeJS.ProcessEnv,
|
|
25
|
-
names: string[],
|
|
26
|
-
defaultValue
|
|
34
|
+
names: readonly string[],
|
|
35
|
+
defaultValue: number,
|
|
27
36
|
): number {
|
|
28
37
|
for (const name of names) {
|
|
29
38
|
const rawValue = env[name]?.trim();
|
|
@@ -38,8 +47,9 @@ function getTelegramApiTempDir(): string {
|
|
|
38
47
|
return resolveTelegramAttachmentsDir();
|
|
39
48
|
}
|
|
40
49
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
50
|
+
const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
|
|
41
51
|
const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
|
|
42
|
-
const TELEGRAM_INBOUND_FILE_MAX_BYTES =
|
|
52
|
+
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(
|
|
43
53
|
process.env,
|
|
44
54
|
["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
|
|
45
55
|
TELEGRAM_FILE_MAX_BYTES,
|
|
@@ -301,8 +311,7 @@ export type TelegramInputRichBlock =
|
|
|
301
311
|
};
|
|
302
312
|
|
|
303
313
|
export type TelegramInputRichDraftBlock =
|
|
304
|
-
|
|
|
305
|
-
| { type: "thinking"; text: TelegramRichText };
|
|
314
|
+
TelegramInputRichBlock | { type: "thinking"; text: TelegramRichText };
|
|
306
315
|
|
|
307
316
|
export type TelegramInputRichMessage = TelegramInputRichMessageCommon &
|
|
308
317
|
(
|
|
@@ -382,6 +391,8 @@ export interface TelegramApiRetryWait {
|
|
|
382
391
|
}
|
|
383
392
|
|
|
384
393
|
export interface TelegramApiCallOptions {
|
|
394
|
+
/** Local per-call effect authority; independent of source completion and transport ownership. */
|
|
395
|
+
assertAuthority?: () => void;
|
|
385
396
|
signal?: AbortSignal;
|
|
386
397
|
maxAttempts?: number;
|
|
387
398
|
retryRateLimit?: boolean;
|
|
@@ -392,6 +403,27 @@ export interface TelegramApiCallOptions {
|
|
|
392
403
|
onRetryWait?: (wait: TelegramApiRetryWait) => void;
|
|
393
404
|
}
|
|
394
405
|
|
|
406
|
+
export class TelegramApiAuthorityError extends Error {
|
|
407
|
+
readonly requestIssued: boolean;
|
|
408
|
+
constructor(requestIssued: boolean) {
|
|
409
|
+
super("Telegram API call authority is unavailable.");
|
|
410
|
+
this.name = "TelegramApiAuthorityError";
|
|
411
|
+
this.requestIssued = requestIssued;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
|
|
416
|
+
export function assertTelegramApiCallAuthority(
|
|
417
|
+
assertAuthority: (() => void) | undefined,
|
|
418
|
+
requestIssued: boolean,
|
|
419
|
+
): void {
|
|
420
|
+
try {
|
|
421
|
+
assertAuthority?.();
|
|
422
|
+
} catch {
|
|
423
|
+
throw new TelegramApiAuthorityError(requestIssued);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
395
427
|
interface TelegramGetFileResult {
|
|
396
428
|
file_path: string;
|
|
397
429
|
file_size?: number;
|
|
@@ -446,7 +478,10 @@ export interface TelegramEditGuestInlineMessageContent {
|
|
|
446
478
|
parseMode?: "HTML";
|
|
447
479
|
}
|
|
448
480
|
|
|
449
|
-
export interface TelegramAnswerCallbackQueryOptions
|
|
481
|
+
export interface TelegramAnswerCallbackQueryOptions extends Pick<
|
|
482
|
+
TelegramApiCallOptions,
|
|
483
|
+
"assertAuthority"
|
|
484
|
+
> {
|
|
450
485
|
recordRuntimeEvent?: (
|
|
451
486
|
kind: "api",
|
|
452
487
|
error: unknown,
|
|
@@ -477,6 +512,7 @@ export interface TelegramApiClient {
|
|
|
477
512
|
answerCallbackQuery: (
|
|
478
513
|
callbackQueryId: string,
|
|
479
514
|
text?: string,
|
|
515
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
480
516
|
) => Promise<void>;
|
|
481
517
|
answerGuestQuery?: (
|
|
482
518
|
guestQueryId: string,
|
|
@@ -486,26 +522,32 @@ export interface TelegramApiClient {
|
|
|
486
522
|
}
|
|
487
523
|
|
|
488
524
|
export interface TelegramApiTargetActivityRuntime {
|
|
489
|
-
begin: (
|
|
490
|
-
method: string,
|
|
491
|
-
body: Record<string, unknown>,
|
|
492
|
-
) => () => void;
|
|
525
|
+
begin: (method: string, body: Record<string, unknown>) => () => void;
|
|
493
526
|
hasPendingTarget: (target: { chatId: number; threadId?: number }) => boolean;
|
|
494
527
|
listPendingTargets: () => { chatId: number; threadId: number }[];
|
|
495
528
|
listPendingChats: () => number[];
|
|
496
529
|
}
|
|
497
530
|
|
|
498
531
|
function parseTelegramApiTargetInteger(value: unknown): number | undefined {
|
|
499
|
-
const parsed =
|
|
500
|
-
typeof value === "
|
|
501
|
-
|
|
532
|
+
const parsed =
|
|
533
|
+
typeof value === "number"
|
|
534
|
+
? value
|
|
535
|
+
: typeof value === "string" && /^-?\d+$/u.test(value)
|
|
536
|
+
? Number(value)
|
|
537
|
+
: undefined;
|
|
538
|
+
return parsed !== undefined && Number.isSafeInteger(parsed)
|
|
539
|
+
? parsed
|
|
540
|
+
: undefined;
|
|
502
541
|
}
|
|
503
542
|
|
|
504
543
|
export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActivityRuntime {
|
|
505
|
-
const pending = new Map<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
544
|
+
const pending = new Map<
|
|
545
|
+
string,
|
|
546
|
+
{
|
|
547
|
+
target: { chatId: number; threadId: number };
|
|
548
|
+
count: number;
|
|
549
|
+
}
|
|
550
|
+
>();
|
|
509
551
|
const pendingChats = new Map<number, number>();
|
|
510
552
|
const messageScopedMethods = new Set([
|
|
511
553
|
"deleteMessage",
|
|
@@ -518,8 +560,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
518
560
|
const chatId = parseTelegramApiTargetInteger(body.chat_id);
|
|
519
561
|
const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
|
|
520
562
|
const messageId = parseTelegramApiTargetInteger(body.message_id);
|
|
521
|
-
const chatScoped =
|
|
522
|
-
|
|
563
|
+
const chatScoped =
|
|
564
|
+
chatId !== undefined &&
|
|
565
|
+
threadId === undefined &&
|
|
566
|
+
messageId !== undefined &&
|
|
567
|
+
messageScopedMethods.has(method);
|
|
523
568
|
if (chatId === undefined || (threadId === undefined && !chatScoped)) {
|
|
524
569
|
return () => undefined;
|
|
525
570
|
}
|
|
@@ -527,7 +572,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
527
572
|
if (key) {
|
|
528
573
|
const existing = pending.get(key);
|
|
529
574
|
if (existing) existing.count += 1;
|
|
530
|
-
else
|
|
575
|
+
else
|
|
576
|
+
pending.set(key, {
|
|
577
|
+
target: { chatId, threadId: threadId! },
|
|
578
|
+
count: 1,
|
|
579
|
+
});
|
|
531
580
|
} else {
|
|
532
581
|
pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
|
|
533
582
|
}
|
|
@@ -547,8 +596,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
547
596
|
};
|
|
548
597
|
},
|
|
549
598
|
hasPendingTarget(target) {
|
|
550
|
-
return
|
|
551
|
-
|
|
599
|
+
return (
|
|
600
|
+
pendingChats.has(target.chatId) ||
|
|
601
|
+
(target.threadId !== undefined &&
|
|
602
|
+
pending.has(`${target.chatId}:${target.threadId}`))
|
|
603
|
+
);
|
|
552
604
|
},
|
|
553
605
|
listPendingTargets() {
|
|
554
606
|
return Array.from(pending.values(), ({ target }) => ({ ...target }));
|
|
@@ -576,15 +628,19 @@ export function createTelegramApiTargetTrackingClient(
|
|
|
576
628
|
}
|
|
577
629
|
};
|
|
578
630
|
return {
|
|
579
|
-
call: (method, body, options) =>
|
|
580
|
-
method,
|
|
581
|
-
body,
|
|
582
|
-
() => client.call(method, body, options),
|
|
583
|
-
),
|
|
631
|
+
call: (method, body, options) =>
|
|
632
|
+
track(method, body, () => client.call(method, body, options)),
|
|
584
633
|
callMultipart: (method, fields, fileField, filePath, fileName, options) =>
|
|
585
|
-
track(method, fields, () =>
|
|
586
|
-
|
|
587
|
-
|
|
634
|
+
track(method, fields, () =>
|
|
635
|
+
client.callMultipart(
|
|
636
|
+
method,
|
|
637
|
+
fields,
|
|
638
|
+
fileField,
|
|
639
|
+
filePath,
|
|
640
|
+
fileName,
|
|
641
|
+
options,
|
|
642
|
+
),
|
|
643
|
+
),
|
|
588
644
|
downloadFile: client.downloadFile,
|
|
589
645
|
answerCallbackQuery: client.answerCallbackQuery,
|
|
590
646
|
...(client.answerGuestQuery
|
|
@@ -619,13 +675,19 @@ export interface TelegramApiWorkspaceAdmissionPort {
|
|
|
619
675
|
resumed: boolean;
|
|
620
676
|
}
|
|
621
677
|
| { kind: "blocked"; reason: "retirement-fenced" };
|
|
622
|
-
releaseAdmission: (
|
|
678
|
+
releaseAdmission: (
|
|
679
|
+
expected: TelegramApiWorkspaceAdmissionLeaseLike,
|
|
680
|
+
) => boolean;
|
|
623
681
|
}
|
|
624
682
|
|
|
625
683
|
export class TelegramApiWorkspaceAdmissionError extends Error {
|
|
626
|
-
readonly code:
|
|
684
|
+
readonly code:
|
|
685
|
+
"blocked" | "unavailable" | "release-lost" | "duplicate-operation";
|
|
627
686
|
|
|
628
|
-
constructor(
|
|
687
|
+
constructor(
|
|
688
|
+
code: TelegramApiWorkspaceAdmissionError["code"],
|
|
689
|
+
message: string,
|
|
690
|
+
) {
|
|
629
691
|
super(message);
|
|
630
692
|
this.name = "TelegramApiWorkspaceAdmissionError";
|
|
631
693
|
this.code = code;
|
|
@@ -743,8 +805,9 @@ export function createTelegramApiWorkspaceAdmissionClient(
|
|
|
743
805
|
}
|
|
744
806
|
|
|
745
807
|
export interface TelegramBridgeApiRuntimeDeps {
|
|
746
|
-
captureRequestErrorHandler?: (
|
|
747
|
-
|
|
808
|
+
captureRequestErrorHandler?: (
|
|
809
|
+
body: Record<string, unknown>,
|
|
810
|
+
) => ((error: unknown) => Promise<void>) | undefined;
|
|
748
811
|
client: TelegramApiClient;
|
|
749
812
|
tempDir: string;
|
|
750
813
|
maxFileSizeBytes: number;
|
|
@@ -761,20 +824,10 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
761
824
|
chatActionMaxGates?: number;
|
|
762
825
|
}
|
|
763
826
|
|
|
764
|
-
export interface TelegramBridgeApiRuntime
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
options?: TelegramApiCallOptions,
|
|
769
|
-
) => Promise<TResponse>;
|
|
770
|
-
callMultipart: <TResponse>(
|
|
771
|
-
method: string,
|
|
772
|
-
fields: Record<string, string>,
|
|
773
|
-
fileField: string,
|
|
774
|
-
filePath: string,
|
|
775
|
-
fileName: string,
|
|
776
|
-
options?: TelegramApiCallOptions,
|
|
777
|
-
) => Promise<TResponse>;
|
|
827
|
+
export interface TelegramBridgeApiRuntime extends Pick<
|
|
828
|
+
TelegramApiClient,
|
|
829
|
+
"call" | "callMultipart"
|
|
830
|
+
> {
|
|
778
831
|
downloadFile: (
|
|
779
832
|
fileId: string,
|
|
780
833
|
suggestedName: string,
|
|
@@ -787,6 +840,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
787
840
|
) => Promise<TelegramUpdate[]>;
|
|
788
841
|
setMyCommands: (
|
|
789
842
|
commands: readonly { command: string; description: string }[],
|
|
843
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
790
844
|
) => Promise<boolean>;
|
|
791
845
|
sendChatAction: (
|
|
792
846
|
chatId: number,
|
|
@@ -811,15 +865,20 @@ export interface TelegramBridgeApiRuntime {
|
|
|
811
865
|
message_thread_id?: number;
|
|
812
866
|
},
|
|
813
867
|
) => Promise<boolean>;
|
|
814
|
-
sendMessage: (
|
|
868
|
+
sendMessage: (
|
|
869
|
+
body: TelegramSendMessageBody,
|
|
870
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
871
|
+
) => Promise<TelegramSentMessage>;
|
|
815
872
|
sendRichMessage: (
|
|
816
873
|
body: TelegramSendRichMessageBody,
|
|
874
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
817
875
|
) => Promise<TelegramSentMessage>;
|
|
818
876
|
sendRichMessageDraft: (
|
|
819
877
|
body: TelegramSendRichMessageDraftBody,
|
|
820
878
|
) => Promise<boolean>;
|
|
821
879
|
editMessageText: (
|
|
822
880
|
body: TelegramEditMessageTextBody,
|
|
881
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
823
882
|
) => Promise<"edited" | "unchanged">;
|
|
824
883
|
editMessageReplyMarkup: (
|
|
825
884
|
chatId: number,
|
|
@@ -829,6 +888,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
829
888
|
answerCallbackQuery: (
|
|
830
889
|
callbackQueryId: string,
|
|
831
890
|
text?: string,
|
|
891
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
832
892
|
) => Promise<void>;
|
|
833
893
|
answerGuestQuery: (
|
|
834
894
|
guestQueryId: string,
|
|
@@ -869,15 +929,31 @@ export function createTelegramAttachmentFileName(
|
|
|
869
929
|
botScope?: string,
|
|
870
930
|
): string {
|
|
871
931
|
const chat = source.chat;
|
|
872
|
-
const clean = (value: string): string =>
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
932
|
+
const clean = (value: string): string =>
|
|
933
|
+
sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
|
|
934
|
+
const scope = source.scope
|
|
935
|
+
? clean(source.scope)
|
|
936
|
+
: chat?.type === "private" || !chat
|
|
937
|
+
? botScope
|
|
938
|
+
? clean(botScope)
|
|
939
|
+
: chat
|
|
940
|
+
? String(Math.abs(chat.id))
|
|
941
|
+
: ""
|
|
942
|
+
: chat.username
|
|
943
|
+
? clean(chat.username)
|
|
944
|
+
: String(Math.abs(chat.id));
|
|
876
945
|
const dot = generatedName.lastIndexOf(".");
|
|
877
946
|
const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
|
|
878
|
-
const tail = source.userFileName
|
|
879
|
-
|
|
880
|
-
|
|
947
|
+
const tail = source.userFileName
|
|
948
|
+
? `-${clean(source.userFileName)}`
|
|
949
|
+
: extension;
|
|
950
|
+
return (
|
|
951
|
+
[sanitizeFileName(source.kind), scope, String(source.messageId)]
|
|
952
|
+
.filter(Boolean)
|
|
953
|
+
.join("-") +
|
|
954
|
+
(source.index !== undefined ? `-${source.index}` : "") +
|
|
955
|
+
tail
|
|
956
|
+
);
|
|
881
957
|
}
|
|
882
958
|
|
|
883
959
|
class TelegramApiMalformedSuccessError extends Error {
|
|
@@ -960,14 +1036,23 @@ export function getTelegramApiErrorRequestTarget(
|
|
|
960
1036
|
}
|
|
961
1037
|
|
|
962
1038
|
export function isTelegramStaleTargetHttpError(error: unknown): boolean {
|
|
963
|
-
if (!(error instanceof TelegramApiHttpError) || error.status !== 400)
|
|
964
|
-
|
|
1039
|
+
if (!(error instanceof TelegramApiHttpError) || error.status !== 400)
|
|
1040
|
+
return false;
|
|
1041
|
+
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(
|
|
1042
|
+
error.message,
|
|
1043
|
+
);
|
|
965
1044
|
}
|
|
966
1045
|
|
|
967
1046
|
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
968
|
-
export function isTelegramApiRequestRejected(
|
|
969
|
-
|
|
970
|
-
|
|
1047
|
+
export function isTelegramApiRequestRejected(
|
|
1048
|
+
error: unknown,
|
|
1049
|
+
method: string,
|
|
1050
|
+
): boolean {
|
|
1051
|
+
return (
|
|
1052
|
+
error instanceof TelegramApiHttpError &&
|
|
1053
|
+
error.rejectedRequestMethod !== undefined &&
|
|
1054
|
+
error.rejectedRequestMethod === method
|
|
1055
|
+
);
|
|
971
1056
|
}
|
|
972
1057
|
|
|
973
1058
|
export function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
@@ -1009,14 +1094,20 @@ export function isRetryableTelegramApiError(error: unknown): boolean {
|
|
|
1009
1094
|
}
|
|
1010
1095
|
|
|
1011
1096
|
export function getTelegramApiRetryAfterMs(error: unknown): number | undefined {
|
|
1012
|
-
return error instanceof TelegramApiHttpError &&
|
|
1097
|
+
return error instanceof TelegramApiHttpError &&
|
|
1098
|
+
error.retryAfterSeconds !== undefined
|
|
1013
1099
|
? Math.max(0, error.retryAfterSeconds * 1000)
|
|
1014
1100
|
: undefined;
|
|
1015
1101
|
}
|
|
1016
1102
|
|
|
1017
1103
|
export function isTelegramMessageUnavailableError(error: unknown): boolean {
|
|
1018
|
-
return
|
|
1019
|
-
|
|
1104
|
+
return (
|
|
1105
|
+
error instanceof TelegramApiHttpError &&
|
|
1106
|
+
error.status === 400 &&
|
|
1107
|
+
/Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(
|
|
1108
|
+
error.message,
|
|
1109
|
+
)
|
|
1110
|
+
);
|
|
1020
1111
|
}
|
|
1021
1112
|
|
|
1022
1113
|
function getTelegramRetryDelayMs(
|
|
@@ -1041,10 +1132,7 @@ function throwIfTelegramApiCallAborted(signal: AbortSignal | undefined): void {
|
|
|
1041
1132
|
if (signal?.aborted) throw getTelegramApiAbortReason(signal);
|
|
1042
1133
|
}
|
|
1043
1134
|
|
|
1044
|
-
function sleepTelegramRetry(
|
|
1045
|
-
ms: number,
|
|
1046
|
-
signal?: AbortSignal,
|
|
1047
|
-
): Promise<void> {
|
|
1135
|
+
function sleepTelegramRetry(ms: number, signal?: AbortSignal): Promise<void> {
|
|
1048
1136
|
if (signal?.aborted) return Promise.reject(getTelegramApiAbortReason(signal));
|
|
1049
1137
|
return new Promise((resolve, reject) => {
|
|
1050
1138
|
const timeout = setTimeout(() => {
|
|
@@ -1106,6 +1194,30 @@ async function writeTelegramDownloadResponse(
|
|
|
1106
1194
|
);
|
|
1107
1195
|
}
|
|
1108
1196
|
|
|
1197
|
+
async function publishTelegramDownload(
|
|
1198
|
+
partPath: string,
|
|
1199
|
+
targetPath: string,
|
|
1200
|
+
signal?: AbortSignal,
|
|
1201
|
+
): Promise<void> {
|
|
1202
|
+
for (let attempt = 0; ; attempt++) {
|
|
1203
|
+
throwIfTelegramApiCallAborted(signal);
|
|
1204
|
+
try {
|
|
1205
|
+
await rename(partPath, targetPath);
|
|
1206
|
+
return;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
1209
|
+
// Windows may briefly deny replacement while another publisher/scanner holds the file.
|
|
1210
|
+
if (
|
|
1211
|
+
process.platform !== "win32" ||
|
|
1212
|
+
(code !== "EPERM" && code !== "EACCES") ||
|
|
1213
|
+
attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS
|
|
1214
|
+
)
|
|
1215
|
+
throw error;
|
|
1216
|
+
await sleepTelegramRetry(50 * 2 ** attempt, signal);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1109
1221
|
async function removeTelegramPartialDownload(path: string): Promise<void> {
|
|
1110
1222
|
try {
|
|
1111
1223
|
await unlink(path);
|
|
@@ -1142,8 +1254,11 @@ async function parseTelegramApiResponse<TResponse>(
|
|
|
1142
1254
|
`Telegram API ${method} failed: ${status}${description}`,
|
|
1143
1255
|
response.status,
|
|
1144
1256
|
Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
|
|
1145
|
-
data?.ok === false &&
|
|
1146
|
-
|
|
1257
|
+
data?.ok === false &&
|
|
1258
|
+
data.error_code === response.status &&
|
|
1259
|
+
[400, 401, 403, 404, 429].includes(response.status)
|
|
1260
|
+
? method
|
|
1261
|
+
: undefined,
|
|
1147
1262
|
);
|
|
1148
1263
|
}
|
|
1149
1264
|
if (!data) {
|
|
@@ -1431,9 +1546,18 @@ function withTelegramTransportDiagnostics(
|
|
|
1431
1546
|
|
|
1432
1547
|
async function callTelegramWithRetry<TResponse>(
|
|
1433
1548
|
method: string,
|
|
1434
|
-
request: (
|
|
1549
|
+
request: (
|
|
1550
|
+
family: TelegramNetworkFamily | undefined,
|
|
1551
|
+
beforeRequest: () => void,
|
|
1552
|
+
) => Promise<Response>,
|
|
1435
1553
|
options: TelegramApiCallOptions | undefined,
|
|
1436
1554
|
): Promise<TResponse> {
|
|
1555
|
+
const assertAuthority = options?.assertAuthority;
|
|
1556
|
+
let requestIssued = false;
|
|
1557
|
+
const beforeRequest = () => {
|
|
1558
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1559
|
+
requestIssued = true;
|
|
1560
|
+
};
|
|
1437
1561
|
const retrySafe =
|
|
1438
1562
|
options?.retrySafety === "safe" ||
|
|
1439
1563
|
(options?.retrySafety !== "non-idempotent" &&
|
|
@@ -1465,15 +1589,24 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1465
1589
|
};
|
|
1466
1590
|
for (let attempt = 0; ; attempt += 1) {
|
|
1467
1591
|
throwIfTelegramApiCallAborted(options?.signal);
|
|
1592
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1468
1593
|
try {
|
|
1469
|
-
|
|
1594
|
+
const result = unwrapTelegramApiResult(
|
|
1470
1595
|
method,
|
|
1471
1596
|
await parseTelegramApiResponse<TResponse>(
|
|
1472
|
-
await callTelegramTransportRequest(
|
|
1597
|
+
await callTelegramTransportRequest(
|
|
1598
|
+
(family) => request(family, beforeRequest),
|
|
1599
|
+
retrySafe,
|
|
1600
|
+
),
|
|
1473
1601
|
method,
|
|
1474
1602
|
),
|
|
1475
1603
|
);
|
|
1604
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1605
|
+
return result;
|
|
1476
1606
|
} catch (error) {
|
|
1607
|
+
// Authority refusal is never a transport failure, retry grant or acknowledged success.
|
|
1608
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
1609
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1477
1610
|
const retryable =
|
|
1478
1611
|
isRetryableTelegramApiError(error) &&
|
|
1479
1612
|
!(
|
|
@@ -1557,17 +1690,20 @@ export async function callTelegram<TResponse>(
|
|
|
1557
1690
|
try {
|
|
1558
1691
|
return await callTelegramWithRetry(
|
|
1559
1692
|
method,
|
|
1560
|
-
async (family) =>
|
|
1561
|
-
|
|
1693
|
+
async (family, beforeRequest) => {
|
|
1694
|
+
const encodedBody = JSON.stringify(body);
|
|
1695
|
+
beforeRequest();
|
|
1696
|
+
return telegramFetch(
|
|
1562
1697
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1563
1698
|
{
|
|
1564
1699
|
method: "POST",
|
|
1565
1700
|
headers: { "content-type": "application/json" },
|
|
1566
|
-
body:
|
|
1701
|
+
body: encodedBody,
|
|
1567
1702
|
signal: options?.signal,
|
|
1568
1703
|
},
|
|
1569
1704
|
family,
|
|
1570
|
-
)
|
|
1705
|
+
);
|
|
1706
|
+
},
|
|
1571
1707
|
options,
|
|
1572
1708
|
);
|
|
1573
1709
|
} catch (error) {
|
|
@@ -1608,11 +1744,13 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1608
1744
|
options?: TelegramApiCallOptions,
|
|
1609
1745
|
): Promise<TResponse> {
|
|
1610
1746
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
1747
|
+
options = options && { ...options };
|
|
1748
|
+
assertTelegramApiCallAuthority(options?.assertAuthority, false);
|
|
1611
1749
|
const fileBlob = await openAsBlob(filePath);
|
|
1612
1750
|
try {
|
|
1613
1751
|
return await callTelegramWithRetry(
|
|
1614
1752
|
method,
|
|
1615
|
-
async (family) => {
|
|
1753
|
+
async (family, beforeRequest) => {
|
|
1616
1754
|
if (family) {
|
|
1617
1755
|
const multipart = await buildTelegramMultipartBody(
|
|
1618
1756
|
fields,
|
|
@@ -1620,6 +1758,7 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1620
1758
|
fileBlob,
|
|
1621
1759
|
fileName,
|
|
1622
1760
|
);
|
|
1761
|
+
beforeRequest();
|
|
1623
1762
|
return telegramFetch(
|
|
1624
1763
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1625
1764
|
{
|
|
@@ -1636,6 +1775,7 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1636
1775
|
form.set(key, value);
|
|
1637
1776
|
}
|
|
1638
1777
|
form.set(fileField, fileBlob, fileName);
|
|
1778
|
+
beforeRequest();
|
|
1639
1779
|
return telegramFetch(
|
|
1640
1780
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1641
1781
|
{
|
|
@@ -1693,7 +1833,7 @@ export async function downloadTelegramFile(
|
|
|
1693
1833
|
partPath,
|
|
1694
1834
|
options?.maxFileSizeBytes,
|
|
1695
1835
|
);
|
|
1696
|
-
await
|
|
1836
|
+
await publishTelegramDownload(partPath, targetPath, options?.signal);
|
|
1697
1837
|
} catch (error) {
|
|
1698
1838
|
await removeTelegramPartialDownload(partPath);
|
|
1699
1839
|
throw error;
|
|
@@ -1701,11 +1841,6 @@ export async function downloadTelegramFile(
|
|
|
1701
1841
|
return targetPath;
|
|
1702
1842
|
}
|
|
1703
1843
|
|
|
1704
|
-
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
1705
|
-
export function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined {
|
|
1706
|
-
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
1844
|
export async function answerTelegramCallbackQuery(
|
|
1710
1845
|
botToken: string | undefined,
|
|
1711
1846
|
callbackQueryId: string,
|
|
@@ -1719,6 +1854,9 @@ export async function answerTelegramCallbackQuery(
|
|
|
1719
1854
|
text
|
|
1720
1855
|
? { callback_query_id: callbackQueryId, text }
|
|
1721
1856
|
: { callback_query_id: callbackQueryId },
|
|
1857
|
+
options.assertAuthority
|
|
1858
|
+
? { assertAuthority: options.assertAuthority }
|
|
1859
|
+
: undefined,
|
|
1722
1860
|
);
|
|
1723
1861
|
} catch (error) {
|
|
1724
1862
|
options.recordRuntimeEvent?.(
|
|
@@ -1728,6 +1866,7 @@ export async function answerTelegramCallbackQuery(
|
|
|
1728
1866
|
method: "answerCallbackQuery",
|
|
1729
1867
|
}),
|
|
1730
1868
|
);
|
|
1869
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
1731
1870
|
}
|
|
1732
1871
|
}
|
|
1733
1872
|
|
|
@@ -1832,18 +1971,25 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1832
1971
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1833
1972
|
});
|
|
1834
1973
|
const admittedClient = deps.workspaceAdmission
|
|
1835
|
-
? createTelegramApiWorkspaceAdmissionClient(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1974
|
+
? createTelegramApiWorkspaceAdmissionClient(
|
|
1975
|
+
client,
|
|
1976
|
+
deps.workspaceAdmission,
|
|
1977
|
+
{
|
|
1978
|
+
onReleaseError(error, method) {
|
|
1979
|
+
deps.recordRuntimeEvent("api", error, {
|
|
1980
|
+
phase: "workspace-admission-release",
|
|
1981
|
+
method,
|
|
1982
|
+
});
|
|
1983
|
+
},
|
|
1841
1984
|
},
|
|
1842
|
-
|
|
1985
|
+
)
|
|
1843
1986
|
: client;
|
|
1844
1987
|
const runtime = createTelegramBridgeApiRuntime({
|
|
1845
1988
|
client: deps.targetActivity
|
|
1846
|
-
? createTelegramApiTargetTrackingClient(
|
|
1989
|
+
? createTelegramApiTargetTrackingClient(
|
|
1990
|
+
admittedClient,
|
|
1991
|
+
deps.targetActivity,
|
|
1992
|
+
)
|
|
1847
1993
|
: admittedClient,
|
|
1848
1994
|
tempDir: getTelegramApiTempDir(),
|
|
1849
1995
|
maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
|
|
@@ -1857,10 +2003,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1857
2003
|
async deleteWorkspaceThread(authorize) {
|
|
1858
2004
|
// The exclusive deletion permit replaces ordinary target admission.
|
|
1859
2005
|
const target = authorize();
|
|
1860
|
-
const deleted = await client.call<boolean>(
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2006
|
+
const deleted = await client.call<boolean>(
|
|
2007
|
+
"deleteForumTopic",
|
|
2008
|
+
{
|
|
2009
|
+
chat_id: target.chatId,
|
|
2010
|
+
message_thread_id: target.threadId,
|
|
2011
|
+
},
|
|
2012
|
+
{ maxAttempts: 1, retrySafety: "non-idempotent" },
|
|
2013
|
+
);
|
|
2014
|
+
if (deleted !== true)
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
"Telegram Workspace Thread deletion was not confirmed.",
|
|
2017
|
+
);
|
|
1864
2018
|
},
|
|
1865
2019
|
};
|
|
1866
2020
|
}
|
|
@@ -1875,7 +2029,9 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1875
2029
|
try {
|
|
1876
2030
|
await handler?.(error);
|
|
1877
2031
|
} catch (recoveryError) {
|
|
1878
|
-
deps.recordRuntimeEvent("api", recoveryError, {
|
|
2032
|
+
deps.recordRuntimeEvent("api", recoveryError, {
|
|
2033
|
+
phase: "stale-target-recovery",
|
|
2034
|
+
});
|
|
1879
2035
|
}
|
|
1880
2036
|
};
|
|
1881
2037
|
const now = deps.now ?? Date.now;
|
|
@@ -1935,7 +2091,8 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1935
2091
|
}
|
|
1936
2092
|
let gate = chatActionGates.get(chatActionKeys.action);
|
|
1937
2093
|
if (!gate) {
|
|
1938
|
-
if (chatActionGates.size >= chatActionMaxGates)
|
|
2094
|
+
if (chatActionGates.size >= chatActionMaxGates)
|
|
2095
|
+
return true as TResponse;
|
|
1939
2096
|
gate = { notBeforeMs: 0 };
|
|
1940
2097
|
chatActionGates.set(chatActionKeys.action, gate);
|
|
1941
2098
|
}
|
|
@@ -2004,9 +2161,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2004
2161
|
return await deps.client.call<TResponse>(method, body, options);
|
|
2005
2162
|
} catch (error) {
|
|
2006
2163
|
await recoverRequestError(recoverError, error);
|
|
2007
|
-
if (
|
|
2008
|
-
|
|
2009
|
-
|
|
2164
|
+
if (
|
|
2165
|
+
method === "deleteMessage" &&
|
|
2166
|
+
error instanceof TelegramApiHttpError &&
|
|
2167
|
+
error.status === 400 &&
|
|
2168
|
+
error.message ===
|
|
2169
|
+
"Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found"
|
|
2170
|
+
) {
|
|
2010
2171
|
return true as TResponse;
|
|
2011
2172
|
}
|
|
2012
2173
|
deps.recordRuntimeEvent(
|
|
@@ -2063,7 +2224,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2063
2224
|
return await deps.client.downloadFile(
|
|
2064
2225
|
fileId,
|
|
2065
2226
|
source
|
|
2066
|
-
? createTelegramAttachmentFileName(
|
|
2227
|
+
? createTelegramAttachmentFileName(
|
|
2228
|
+
source,
|
|
2229
|
+
suggestedName,
|
|
2230
|
+
deps.getBotScope?.(),
|
|
2231
|
+
)
|
|
2067
2232
|
: suggestedName,
|
|
2068
2233
|
deps.tempDir,
|
|
2069
2234
|
{
|
|
@@ -2087,8 +2252,8 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2087
2252
|
),
|
|
2088
2253
|
getUpdates: (body, signal) =>
|
|
2089
2254
|
callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
|
|
2090
|
-
setMyCommands: (commands) =>
|
|
2091
|
-
callRecorded<boolean>("setMyCommands", { commands }),
|
|
2255
|
+
setMyCommands: (commands, options) =>
|
|
2256
|
+
callRecorded<boolean>("setMyCommands", { commands }, options),
|
|
2092
2257
|
sendChatAction: (chatId, action, options) =>
|
|
2093
2258
|
callRecorded<boolean>("sendChatAction", {
|
|
2094
2259
|
chat_id: chatId,
|
|
@@ -2132,16 +2297,16 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2132
2297
|
body.message_thread_id = options.message_thread_id;
|
|
2133
2298
|
return callRecorded<boolean>("sendMessageDraft", body);
|
|
2134
2299
|
},
|
|
2135
|
-
sendMessage: (body) =>
|
|
2136
|
-
callRecorded<TelegramSentMessage>("sendMessage", body),
|
|
2137
|
-
sendRichMessage: (body) =>
|
|
2138
|
-
callRecorded<TelegramSentMessage>("sendRichMessage", body),
|
|
2300
|
+
sendMessage: (body, options) =>
|
|
2301
|
+
callRecorded<TelegramSentMessage>("sendMessage", body, options),
|
|
2302
|
+
sendRichMessage: (body, options) =>
|
|
2303
|
+
callRecorded<TelegramSentMessage>("sendRichMessage", body, options),
|
|
2139
2304
|
sendRichMessageDraft: (body) =>
|
|
2140
2305
|
callRecorded<boolean>("sendRichMessageDraft", body),
|
|
2141
|
-
editMessageText: async (body) => {
|
|
2306
|
+
editMessageText: async (body, options) => {
|
|
2142
2307
|
const recoverError = deps.captureRequestErrorHandler?.(body);
|
|
2143
2308
|
try {
|
|
2144
|
-
await deps.client.call("editMessageText", body);
|
|
2309
|
+
await deps.client.call("editMessageText", body, options);
|
|
2145
2310
|
return "edited";
|
|
2146
2311
|
} catch (error) {
|
|
2147
2312
|
if (isTelegramMessageNotModifiedError(error)) return "unchanged";
|
|
@@ -2163,7 +2328,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2163
2328
|
reply_markup: replyMarkup,
|
|
2164
2329
|
});
|
|
2165
2330
|
},
|
|
2166
|
-
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
2331
|
+
answerCallbackQuery: async (callbackQueryId, text, options) => {
|
|
2332
|
+
if (options?.assertAuthority) {
|
|
2333
|
+
try {
|
|
2334
|
+
await callRecorded<boolean>(
|
|
2335
|
+
"answerCallbackQuery",
|
|
2336
|
+
text
|
|
2337
|
+
? { callback_query_id: callbackQueryId, text }
|
|
2338
|
+
: { callback_query_id: callbackQueryId },
|
|
2339
|
+
{ assertAuthority: options.assertAuthority },
|
|
2340
|
+
);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
2343
|
+
}
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2167
2346
|
try {
|
|
2168
2347
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
2169
2348
|
} catch (error) {
|
|
@@ -2174,6 +2353,7 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2174
2353
|
method: "answerCallbackQuery",
|
|
2175
2354
|
}),
|
|
2176
2355
|
);
|
|
2356
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
2177
2357
|
}
|
|
2178
2358
|
},
|
|
2179
2359
|
answerGuestQuery: (
|
|
@@ -2239,16 +2419,19 @@ export function createTelegramApiClient(
|
|
|
2239
2419
|
const token = getBotToken();
|
|
2240
2420
|
// Cooldown keys retain only the public bot-id prefix, not the credential.
|
|
2241
2421
|
const botId = token?.match(/^(\d+):/)?.[1];
|
|
2242
|
-
const isDraft =
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2422
|
+
const isDraft =
|
|
2423
|
+
method === "sendMessageDraft" || method === "sendRichMessageDraft";
|
|
2424
|
+
const draftKey =
|
|
2425
|
+
isDraft && botId
|
|
2426
|
+
? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
|
|
2427
|
+
: undefined;
|
|
2246
2428
|
if (draftKey) {
|
|
2247
2429
|
const nowMs = now();
|
|
2248
2430
|
for (const [key, deadline] of draftRetryNotBeforeByTarget) {
|
|
2249
2431
|
if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
|
|
2250
2432
|
}
|
|
2251
|
-
if (draftRetryNotBeforeByTarget.has(draftKey))
|
|
2433
|
+
if (draftRetryNotBeforeByTarget.has(draftKey))
|
|
2434
|
+
return false as TResponse;
|
|
2252
2435
|
}
|
|
2253
2436
|
try {
|
|
2254
2437
|
// A draft is a replaceable snapshot, not a body to replay after backoff.
|
|
@@ -2279,10 +2462,18 @@ export function createTelegramApiClient(
|
|
|
2279
2462
|
});
|
|
2280
2463
|
} catch (error) {
|
|
2281
2464
|
if (draftKey && isRetryableTelegramApiError(error)) {
|
|
2282
|
-
draftRetryNotBeforeByTarget.set(
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2465
|
+
draftRetryNotBeforeByTarget.set(
|
|
2466
|
+
draftKey,
|
|
2467
|
+
Math.max(
|
|
2468
|
+
draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
|
|
2469
|
+
now() +
|
|
2470
|
+
getTelegramRetryDelayMs(
|
|
2471
|
+
error,
|
|
2472
|
+
0,
|
|
2473
|
+
options?.retryBaseDelayMs ?? 500,
|
|
2474
|
+
),
|
|
2475
|
+
),
|
|
2476
|
+
);
|
|
2286
2477
|
}
|
|
2287
2478
|
throw error;
|
|
2288
2479
|
}
|
|
@@ -2314,12 +2505,16 @@ export function createTelegramApiClient(
|
|
|
2314
2505
|
options,
|
|
2315
2506
|
);
|
|
2316
2507
|
},
|
|
2317
|
-
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
2508
|
+
answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
|
|
2509
|
+
const assertAuthority = authorityOptions?.assertAuthority;
|
|
2510
|
+
const callbackOptions = assertAuthority
|
|
2511
|
+
? { ...options, assertAuthority }
|
|
2512
|
+
: options;
|
|
2318
2513
|
await answerTelegramCallbackQuery(
|
|
2319
2514
|
getBotToken(),
|
|
2320
2515
|
callbackQueryId,
|
|
2321
2516
|
text,
|
|
2322
|
-
|
|
2517
|
+
callbackOptions,
|
|
2323
2518
|
);
|
|
2324
2519
|
},
|
|
2325
2520
|
};
|