@llblab/pi-telegram 0.52.2 → 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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- 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 +639 -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 +29 -3
- package/dist/lib/locks.js +289 -108
- 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 +21 -37
- package/dist/lib/polling.js +59 -50
- 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 +1 -3
- package/dist/lib/sync.js +67 -43
- 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 +148 -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 +31 -29
- 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 +1206 -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 +677 -203
- 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 +144 -147
- 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 +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -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();
|
|
@@ -40,7 +49,7 @@ function getTelegramApiTempDir(): string {
|
|
|
40
49
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
41
50
|
const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
|
|
42
51
|
const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
|
|
43
|
-
const TELEGRAM_INBOUND_FILE_MAX_BYTES =
|
|
52
|
+
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(
|
|
44
53
|
process.env,
|
|
45
54
|
["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
|
|
46
55
|
TELEGRAM_FILE_MAX_BYTES,
|
|
@@ -302,8 +311,7 @@ export type TelegramInputRichBlock =
|
|
|
302
311
|
};
|
|
303
312
|
|
|
304
313
|
export type TelegramInputRichDraftBlock =
|
|
305
|
-
|
|
|
306
|
-
| { type: "thinking"; text: TelegramRichText };
|
|
314
|
+
TelegramInputRichBlock | { type: "thinking"; text: TelegramRichText };
|
|
307
315
|
|
|
308
316
|
export type TelegramInputRichMessage = TelegramInputRichMessageCommon &
|
|
309
317
|
(
|
|
@@ -383,6 +391,8 @@ export interface TelegramApiRetryWait {
|
|
|
383
391
|
}
|
|
384
392
|
|
|
385
393
|
export interface TelegramApiCallOptions {
|
|
394
|
+
/** Local per-call effect authority; independent of source completion and transport ownership. */
|
|
395
|
+
assertAuthority?: () => void;
|
|
386
396
|
signal?: AbortSignal;
|
|
387
397
|
maxAttempts?: number;
|
|
388
398
|
retryRateLimit?: boolean;
|
|
@@ -393,6 +403,27 @@ export interface TelegramApiCallOptions {
|
|
|
393
403
|
onRetryWait?: (wait: TelegramApiRetryWait) => void;
|
|
394
404
|
}
|
|
395
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
|
+
|
|
396
427
|
interface TelegramGetFileResult {
|
|
397
428
|
file_path: string;
|
|
398
429
|
file_size?: number;
|
|
@@ -447,7 +478,10 @@ export interface TelegramEditGuestInlineMessageContent {
|
|
|
447
478
|
parseMode?: "HTML";
|
|
448
479
|
}
|
|
449
480
|
|
|
450
|
-
export interface TelegramAnswerCallbackQueryOptions
|
|
481
|
+
export interface TelegramAnswerCallbackQueryOptions extends Pick<
|
|
482
|
+
TelegramApiCallOptions,
|
|
483
|
+
"assertAuthority"
|
|
484
|
+
> {
|
|
451
485
|
recordRuntimeEvent?: (
|
|
452
486
|
kind: "api",
|
|
453
487
|
error: unknown,
|
|
@@ -478,6 +512,7 @@ export interface TelegramApiClient {
|
|
|
478
512
|
answerCallbackQuery: (
|
|
479
513
|
callbackQueryId: string,
|
|
480
514
|
text?: string,
|
|
515
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
481
516
|
) => Promise<void>;
|
|
482
517
|
answerGuestQuery?: (
|
|
483
518
|
guestQueryId: string,
|
|
@@ -487,26 +522,32 @@ export interface TelegramApiClient {
|
|
|
487
522
|
}
|
|
488
523
|
|
|
489
524
|
export interface TelegramApiTargetActivityRuntime {
|
|
490
|
-
begin: (
|
|
491
|
-
method: string,
|
|
492
|
-
body: Record<string, unknown>,
|
|
493
|
-
) => () => void;
|
|
525
|
+
begin: (method: string, body: Record<string, unknown>) => () => void;
|
|
494
526
|
hasPendingTarget: (target: { chatId: number; threadId?: number }) => boolean;
|
|
495
527
|
listPendingTargets: () => { chatId: number; threadId: number }[];
|
|
496
528
|
listPendingChats: () => number[];
|
|
497
529
|
}
|
|
498
530
|
|
|
499
531
|
function parseTelegramApiTargetInteger(value: unknown): number | undefined {
|
|
500
|
-
const parsed =
|
|
501
|
-
typeof value === "
|
|
502
|
-
|
|
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;
|
|
503
541
|
}
|
|
504
542
|
|
|
505
543
|
export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActivityRuntime {
|
|
506
|
-
const pending = new Map<
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
544
|
+
const pending = new Map<
|
|
545
|
+
string,
|
|
546
|
+
{
|
|
547
|
+
target: { chatId: number; threadId: number };
|
|
548
|
+
count: number;
|
|
549
|
+
}
|
|
550
|
+
>();
|
|
510
551
|
const pendingChats = new Map<number, number>();
|
|
511
552
|
const messageScopedMethods = new Set([
|
|
512
553
|
"deleteMessage",
|
|
@@ -519,8 +560,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
519
560
|
const chatId = parseTelegramApiTargetInteger(body.chat_id);
|
|
520
561
|
const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
|
|
521
562
|
const messageId = parseTelegramApiTargetInteger(body.message_id);
|
|
522
|
-
const chatScoped =
|
|
523
|
-
|
|
563
|
+
const chatScoped =
|
|
564
|
+
chatId !== undefined &&
|
|
565
|
+
threadId === undefined &&
|
|
566
|
+
messageId !== undefined &&
|
|
567
|
+
messageScopedMethods.has(method);
|
|
524
568
|
if (chatId === undefined || (threadId === undefined && !chatScoped)) {
|
|
525
569
|
return () => undefined;
|
|
526
570
|
}
|
|
@@ -528,7 +572,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
528
572
|
if (key) {
|
|
529
573
|
const existing = pending.get(key);
|
|
530
574
|
if (existing) existing.count += 1;
|
|
531
|
-
else
|
|
575
|
+
else
|
|
576
|
+
pending.set(key, {
|
|
577
|
+
target: { chatId, threadId: threadId! },
|
|
578
|
+
count: 1,
|
|
579
|
+
});
|
|
532
580
|
} else {
|
|
533
581
|
pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
|
|
534
582
|
}
|
|
@@ -548,8 +596,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
|
|
|
548
596
|
};
|
|
549
597
|
},
|
|
550
598
|
hasPendingTarget(target) {
|
|
551
|
-
return
|
|
552
|
-
|
|
599
|
+
return (
|
|
600
|
+
pendingChats.has(target.chatId) ||
|
|
601
|
+
(target.threadId !== undefined &&
|
|
602
|
+
pending.has(`${target.chatId}:${target.threadId}`))
|
|
603
|
+
);
|
|
553
604
|
},
|
|
554
605
|
listPendingTargets() {
|
|
555
606
|
return Array.from(pending.values(), ({ target }) => ({ ...target }));
|
|
@@ -577,15 +628,19 @@ export function createTelegramApiTargetTrackingClient(
|
|
|
577
628
|
}
|
|
578
629
|
};
|
|
579
630
|
return {
|
|
580
|
-
call: (method, body, options) =>
|
|
581
|
-
method,
|
|
582
|
-
body,
|
|
583
|
-
() => client.call(method, body, options),
|
|
584
|
-
),
|
|
631
|
+
call: (method, body, options) =>
|
|
632
|
+
track(method, body, () => client.call(method, body, options)),
|
|
585
633
|
callMultipart: (method, fields, fileField, filePath, fileName, options) =>
|
|
586
|
-
track(method, fields, () =>
|
|
587
|
-
|
|
588
|
-
|
|
634
|
+
track(method, fields, () =>
|
|
635
|
+
client.callMultipart(
|
|
636
|
+
method,
|
|
637
|
+
fields,
|
|
638
|
+
fileField,
|
|
639
|
+
filePath,
|
|
640
|
+
fileName,
|
|
641
|
+
options,
|
|
642
|
+
),
|
|
643
|
+
),
|
|
589
644
|
downloadFile: client.downloadFile,
|
|
590
645
|
answerCallbackQuery: client.answerCallbackQuery,
|
|
591
646
|
...(client.answerGuestQuery
|
|
@@ -620,13 +675,19 @@ export interface TelegramApiWorkspaceAdmissionPort {
|
|
|
620
675
|
resumed: boolean;
|
|
621
676
|
}
|
|
622
677
|
| { kind: "blocked"; reason: "retirement-fenced" };
|
|
623
|
-
releaseAdmission: (
|
|
678
|
+
releaseAdmission: (
|
|
679
|
+
expected: TelegramApiWorkspaceAdmissionLeaseLike,
|
|
680
|
+
) => boolean;
|
|
624
681
|
}
|
|
625
682
|
|
|
626
683
|
export class TelegramApiWorkspaceAdmissionError extends Error {
|
|
627
|
-
readonly code:
|
|
684
|
+
readonly code:
|
|
685
|
+
"blocked" | "unavailable" | "release-lost" | "duplicate-operation";
|
|
628
686
|
|
|
629
|
-
constructor(
|
|
687
|
+
constructor(
|
|
688
|
+
code: TelegramApiWorkspaceAdmissionError["code"],
|
|
689
|
+
message: string,
|
|
690
|
+
) {
|
|
630
691
|
super(message);
|
|
631
692
|
this.name = "TelegramApiWorkspaceAdmissionError";
|
|
632
693
|
this.code = code;
|
|
@@ -744,8 +805,9 @@ export function createTelegramApiWorkspaceAdmissionClient(
|
|
|
744
805
|
}
|
|
745
806
|
|
|
746
807
|
export interface TelegramBridgeApiRuntimeDeps {
|
|
747
|
-
captureRequestErrorHandler?: (
|
|
748
|
-
|
|
808
|
+
captureRequestErrorHandler?: (
|
|
809
|
+
body: Record<string, unknown>,
|
|
810
|
+
) => ((error: unknown) => Promise<void>) | undefined;
|
|
749
811
|
client: TelegramApiClient;
|
|
750
812
|
tempDir: string;
|
|
751
813
|
maxFileSizeBytes: number;
|
|
@@ -762,20 +824,10 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
762
824
|
chatActionMaxGates?: number;
|
|
763
825
|
}
|
|
764
826
|
|
|
765
|
-
export interface TelegramBridgeApiRuntime
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
options?: TelegramApiCallOptions,
|
|
770
|
-
) => Promise<TResponse>;
|
|
771
|
-
callMultipart: <TResponse>(
|
|
772
|
-
method: string,
|
|
773
|
-
fields: Record<string, string>,
|
|
774
|
-
fileField: string,
|
|
775
|
-
filePath: string,
|
|
776
|
-
fileName: string,
|
|
777
|
-
options?: TelegramApiCallOptions,
|
|
778
|
-
) => Promise<TResponse>;
|
|
827
|
+
export interface TelegramBridgeApiRuntime extends Pick<
|
|
828
|
+
TelegramApiClient,
|
|
829
|
+
"call" | "callMultipart"
|
|
830
|
+
> {
|
|
779
831
|
downloadFile: (
|
|
780
832
|
fileId: string,
|
|
781
833
|
suggestedName: string,
|
|
@@ -788,6 +840,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
788
840
|
) => Promise<TelegramUpdate[]>;
|
|
789
841
|
setMyCommands: (
|
|
790
842
|
commands: readonly { command: string; description: string }[],
|
|
843
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
791
844
|
) => Promise<boolean>;
|
|
792
845
|
sendChatAction: (
|
|
793
846
|
chatId: number,
|
|
@@ -812,15 +865,20 @@ export interface TelegramBridgeApiRuntime {
|
|
|
812
865
|
message_thread_id?: number;
|
|
813
866
|
},
|
|
814
867
|
) => Promise<boolean>;
|
|
815
|
-
sendMessage: (
|
|
868
|
+
sendMessage: (
|
|
869
|
+
body: TelegramSendMessageBody,
|
|
870
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
871
|
+
) => Promise<TelegramSentMessage>;
|
|
816
872
|
sendRichMessage: (
|
|
817
873
|
body: TelegramSendRichMessageBody,
|
|
874
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
818
875
|
) => Promise<TelegramSentMessage>;
|
|
819
876
|
sendRichMessageDraft: (
|
|
820
877
|
body: TelegramSendRichMessageDraftBody,
|
|
821
878
|
) => Promise<boolean>;
|
|
822
879
|
editMessageText: (
|
|
823
880
|
body: TelegramEditMessageTextBody,
|
|
881
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
824
882
|
) => Promise<"edited" | "unchanged">;
|
|
825
883
|
editMessageReplyMarkup: (
|
|
826
884
|
chatId: number,
|
|
@@ -830,6 +888,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
830
888
|
answerCallbackQuery: (
|
|
831
889
|
callbackQueryId: string,
|
|
832
890
|
text?: string,
|
|
891
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
833
892
|
) => Promise<void>;
|
|
834
893
|
answerGuestQuery: (
|
|
835
894
|
guestQueryId: string,
|
|
@@ -870,15 +929,31 @@ export function createTelegramAttachmentFileName(
|
|
|
870
929
|
botScope?: string,
|
|
871
930
|
): string {
|
|
872
931
|
const chat = source.chat;
|
|
873
|
-
const clean = (value: string): string =>
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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));
|
|
877
945
|
const dot = generatedName.lastIndexOf(".");
|
|
878
946
|
const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
|
|
879
|
-
const tail = source.userFileName
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
);
|
|
882
957
|
}
|
|
883
958
|
|
|
884
959
|
class TelegramApiMalformedSuccessError extends Error {
|
|
@@ -961,14 +1036,23 @@ export function getTelegramApiErrorRequestTarget(
|
|
|
961
1036
|
}
|
|
962
1037
|
|
|
963
1038
|
export function isTelegramStaleTargetHttpError(error: unknown): boolean {
|
|
964
|
-
if (!(error instanceof TelegramApiHttpError) || error.status !== 400)
|
|
965
|
-
|
|
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
|
+
);
|
|
966
1044
|
}
|
|
967
1045
|
|
|
968
1046
|
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
969
|
-
export function isTelegramApiRequestRejected(
|
|
970
|
-
|
|
971
|
-
|
|
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
|
+
);
|
|
972
1056
|
}
|
|
973
1057
|
|
|
974
1058
|
export function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
@@ -1010,14 +1094,20 @@ export function isRetryableTelegramApiError(error: unknown): boolean {
|
|
|
1010
1094
|
}
|
|
1011
1095
|
|
|
1012
1096
|
export function getTelegramApiRetryAfterMs(error: unknown): number | undefined {
|
|
1013
|
-
return error instanceof TelegramApiHttpError &&
|
|
1097
|
+
return error instanceof TelegramApiHttpError &&
|
|
1098
|
+
error.retryAfterSeconds !== undefined
|
|
1014
1099
|
? Math.max(0, error.retryAfterSeconds * 1000)
|
|
1015
1100
|
: undefined;
|
|
1016
1101
|
}
|
|
1017
1102
|
|
|
1018
1103
|
export function isTelegramMessageUnavailableError(error: unknown): boolean {
|
|
1019
|
-
return
|
|
1020
|
-
|
|
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
|
+
);
|
|
1021
1111
|
}
|
|
1022
1112
|
|
|
1023
1113
|
function getTelegramRetryDelayMs(
|
|
@@ -1042,10 +1132,7 @@ function throwIfTelegramApiCallAborted(signal: AbortSignal | undefined): void {
|
|
|
1042
1132
|
if (signal?.aborted) throw getTelegramApiAbortReason(signal);
|
|
1043
1133
|
}
|
|
1044
1134
|
|
|
1045
|
-
function sleepTelegramRetry(
|
|
1046
|
-
ms: number,
|
|
1047
|
-
signal?: AbortSignal,
|
|
1048
|
-
): Promise<void> {
|
|
1135
|
+
function sleepTelegramRetry(ms: number, signal?: AbortSignal): Promise<void> {
|
|
1049
1136
|
if (signal?.aborted) return Promise.reject(getTelegramApiAbortReason(signal));
|
|
1050
1137
|
return new Promise((resolve, reject) => {
|
|
1051
1138
|
const timeout = setTimeout(() => {
|
|
@@ -1120,7 +1207,12 @@ async function publishTelegramDownload(
|
|
|
1120
1207
|
} catch (error) {
|
|
1121
1208
|
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
1122
1209
|
// Windows may briefly deny replacement while another publisher/scanner holds the file.
|
|
1123
|
-
if (
|
|
1210
|
+
if (
|
|
1211
|
+
process.platform !== "win32" ||
|
|
1212
|
+
(code !== "EPERM" && code !== "EACCES") ||
|
|
1213
|
+
attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS
|
|
1214
|
+
)
|
|
1215
|
+
throw error;
|
|
1124
1216
|
await sleepTelegramRetry(50 * 2 ** attempt, signal);
|
|
1125
1217
|
}
|
|
1126
1218
|
}
|
|
@@ -1162,8 +1254,11 @@ async function parseTelegramApiResponse<TResponse>(
|
|
|
1162
1254
|
`Telegram API ${method} failed: ${status}${description}`,
|
|
1163
1255
|
response.status,
|
|
1164
1256
|
Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
|
|
1165
|
-
data?.ok === false &&
|
|
1166
|
-
|
|
1257
|
+
data?.ok === false &&
|
|
1258
|
+
data.error_code === response.status &&
|
|
1259
|
+
[400, 401, 403, 404, 429].includes(response.status)
|
|
1260
|
+
? method
|
|
1261
|
+
: undefined,
|
|
1167
1262
|
);
|
|
1168
1263
|
}
|
|
1169
1264
|
if (!data) {
|
|
@@ -1451,9 +1546,18 @@ function withTelegramTransportDiagnostics(
|
|
|
1451
1546
|
|
|
1452
1547
|
async function callTelegramWithRetry<TResponse>(
|
|
1453
1548
|
method: string,
|
|
1454
|
-
request: (
|
|
1549
|
+
request: (
|
|
1550
|
+
family: TelegramNetworkFamily | undefined,
|
|
1551
|
+
beforeRequest: () => void,
|
|
1552
|
+
) => Promise<Response>,
|
|
1455
1553
|
options: TelegramApiCallOptions | undefined,
|
|
1456
1554
|
): Promise<TResponse> {
|
|
1555
|
+
const assertAuthority = options?.assertAuthority;
|
|
1556
|
+
let requestIssued = false;
|
|
1557
|
+
const beforeRequest = () => {
|
|
1558
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1559
|
+
requestIssued = true;
|
|
1560
|
+
};
|
|
1457
1561
|
const retrySafe =
|
|
1458
1562
|
options?.retrySafety === "safe" ||
|
|
1459
1563
|
(options?.retrySafety !== "non-idempotent" &&
|
|
@@ -1485,15 +1589,24 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1485
1589
|
};
|
|
1486
1590
|
for (let attempt = 0; ; attempt += 1) {
|
|
1487
1591
|
throwIfTelegramApiCallAborted(options?.signal);
|
|
1592
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1488
1593
|
try {
|
|
1489
|
-
|
|
1594
|
+
const result = unwrapTelegramApiResult(
|
|
1490
1595
|
method,
|
|
1491
1596
|
await parseTelegramApiResponse<TResponse>(
|
|
1492
|
-
await callTelegramTransportRequest(
|
|
1597
|
+
await callTelegramTransportRequest(
|
|
1598
|
+
(family) => request(family, beforeRequest),
|
|
1599
|
+
retrySafe,
|
|
1600
|
+
),
|
|
1493
1601
|
method,
|
|
1494
1602
|
),
|
|
1495
1603
|
);
|
|
1604
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1605
|
+
return result;
|
|
1496
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);
|
|
1497
1610
|
const retryable =
|
|
1498
1611
|
isRetryableTelegramApiError(error) &&
|
|
1499
1612
|
!(
|
|
@@ -1577,17 +1690,20 @@ export async function callTelegram<TResponse>(
|
|
|
1577
1690
|
try {
|
|
1578
1691
|
return await callTelegramWithRetry(
|
|
1579
1692
|
method,
|
|
1580
|
-
async (family) =>
|
|
1581
|
-
|
|
1693
|
+
async (family, beforeRequest) => {
|
|
1694
|
+
const encodedBody = JSON.stringify(body);
|
|
1695
|
+
beforeRequest();
|
|
1696
|
+
return telegramFetch(
|
|
1582
1697
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1583
1698
|
{
|
|
1584
1699
|
method: "POST",
|
|
1585
1700
|
headers: { "content-type": "application/json" },
|
|
1586
|
-
body:
|
|
1701
|
+
body: encodedBody,
|
|
1587
1702
|
signal: options?.signal,
|
|
1588
1703
|
},
|
|
1589
1704
|
family,
|
|
1590
|
-
)
|
|
1705
|
+
);
|
|
1706
|
+
},
|
|
1591
1707
|
options,
|
|
1592
1708
|
);
|
|
1593
1709
|
} catch (error) {
|
|
@@ -1628,11 +1744,13 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1628
1744
|
options?: TelegramApiCallOptions,
|
|
1629
1745
|
): Promise<TResponse> {
|
|
1630
1746
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
1747
|
+
options = options && { ...options };
|
|
1748
|
+
assertTelegramApiCallAuthority(options?.assertAuthority, false);
|
|
1631
1749
|
const fileBlob = await openAsBlob(filePath);
|
|
1632
1750
|
try {
|
|
1633
1751
|
return await callTelegramWithRetry(
|
|
1634
1752
|
method,
|
|
1635
|
-
async (family) => {
|
|
1753
|
+
async (family, beforeRequest) => {
|
|
1636
1754
|
if (family) {
|
|
1637
1755
|
const multipart = await buildTelegramMultipartBody(
|
|
1638
1756
|
fields,
|
|
@@ -1640,6 +1758,7 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1640
1758
|
fileBlob,
|
|
1641
1759
|
fileName,
|
|
1642
1760
|
);
|
|
1761
|
+
beforeRequest();
|
|
1643
1762
|
return telegramFetch(
|
|
1644
1763
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1645
1764
|
{
|
|
@@ -1656,6 +1775,7 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1656
1775
|
form.set(key, value);
|
|
1657
1776
|
}
|
|
1658
1777
|
form.set(fileField, fileBlob, fileName);
|
|
1778
|
+
beforeRequest();
|
|
1659
1779
|
return telegramFetch(
|
|
1660
1780
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1661
1781
|
{
|
|
@@ -1721,11 +1841,6 @@ export async function downloadTelegramFile(
|
|
|
1721
1841
|
return targetPath;
|
|
1722
1842
|
}
|
|
1723
1843
|
|
|
1724
|
-
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
1725
|
-
export function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined {
|
|
1726
|
-
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
1844
|
export async function answerTelegramCallbackQuery(
|
|
1730
1845
|
botToken: string | undefined,
|
|
1731
1846
|
callbackQueryId: string,
|
|
@@ -1739,6 +1854,9 @@ export async function answerTelegramCallbackQuery(
|
|
|
1739
1854
|
text
|
|
1740
1855
|
? { callback_query_id: callbackQueryId, text }
|
|
1741
1856
|
: { callback_query_id: callbackQueryId },
|
|
1857
|
+
options.assertAuthority
|
|
1858
|
+
? { assertAuthority: options.assertAuthority }
|
|
1859
|
+
: undefined,
|
|
1742
1860
|
);
|
|
1743
1861
|
} catch (error) {
|
|
1744
1862
|
options.recordRuntimeEvent?.(
|
|
@@ -1748,6 +1866,7 @@ export async function answerTelegramCallbackQuery(
|
|
|
1748
1866
|
method: "answerCallbackQuery",
|
|
1749
1867
|
}),
|
|
1750
1868
|
);
|
|
1869
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
1751
1870
|
}
|
|
1752
1871
|
}
|
|
1753
1872
|
|
|
@@ -1852,18 +1971,25 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1852
1971
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1853
1972
|
});
|
|
1854
1973
|
const admittedClient = deps.workspaceAdmission
|
|
1855
|
-
? createTelegramApiWorkspaceAdmissionClient(
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
+
},
|
|
1861
1984
|
},
|
|
1862
|
-
|
|
1985
|
+
)
|
|
1863
1986
|
: client;
|
|
1864
1987
|
const runtime = createTelegramBridgeApiRuntime({
|
|
1865
1988
|
client: deps.targetActivity
|
|
1866
|
-
? createTelegramApiTargetTrackingClient(
|
|
1989
|
+
? createTelegramApiTargetTrackingClient(
|
|
1990
|
+
admittedClient,
|
|
1991
|
+
deps.targetActivity,
|
|
1992
|
+
)
|
|
1867
1993
|
: admittedClient,
|
|
1868
1994
|
tempDir: getTelegramApiTempDir(),
|
|
1869
1995
|
maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
|
|
@@ -1877,10 +2003,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1877
2003
|
async deleteWorkspaceThread(authorize) {
|
|
1878
2004
|
// The exclusive deletion permit replaces ordinary target admission.
|
|
1879
2005
|
const target = authorize();
|
|
1880
|
-
const deleted = await client.call<boolean>(
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
+
);
|
|
1884
2018
|
},
|
|
1885
2019
|
};
|
|
1886
2020
|
}
|
|
@@ -1895,7 +2029,9 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1895
2029
|
try {
|
|
1896
2030
|
await handler?.(error);
|
|
1897
2031
|
} catch (recoveryError) {
|
|
1898
|
-
deps.recordRuntimeEvent("api", recoveryError, {
|
|
2032
|
+
deps.recordRuntimeEvent("api", recoveryError, {
|
|
2033
|
+
phase: "stale-target-recovery",
|
|
2034
|
+
});
|
|
1899
2035
|
}
|
|
1900
2036
|
};
|
|
1901
2037
|
const now = deps.now ?? Date.now;
|
|
@@ -1955,7 +2091,8 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1955
2091
|
}
|
|
1956
2092
|
let gate = chatActionGates.get(chatActionKeys.action);
|
|
1957
2093
|
if (!gate) {
|
|
1958
|
-
if (chatActionGates.size >= chatActionMaxGates)
|
|
2094
|
+
if (chatActionGates.size >= chatActionMaxGates)
|
|
2095
|
+
return true as TResponse;
|
|
1959
2096
|
gate = { notBeforeMs: 0 };
|
|
1960
2097
|
chatActionGates.set(chatActionKeys.action, gate);
|
|
1961
2098
|
}
|
|
@@ -2024,9 +2161,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2024
2161
|
return await deps.client.call<TResponse>(method, body, options);
|
|
2025
2162
|
} catch (error) {
|
|
2026
2163
|
await recoverRequestError(recoverError, error);
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
|
|
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
|
+
) {
|
|
2030
2171
|
return true as TResponse;
|
|
2031
2172
|
}
|
|
2032
2173
|
deps.recordRuntimeEvent(
|
|
@@ -2083,7 +2224,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2083
2224
|
return await deps.client.downloadFile(
|
|
2084
2225
|
fileId,
|
|
2085
2226
|
source
|
|
2086
|
-
? createTelegramAttachmentFileName(
|
|
2227
|
+
? createTelegramAttachmentFileName(
|
|
2228
|
+
source,
|
|
2229
|
+
suggestedName,
|
|
2230
|
+
deps.getBotScope?.(),
|
|
2231
|
+
)
|
|
2087
2232
|
: suggestedName,
|
|
2088
2233
|
deps.tempDir,
|
|
2089
2234
|
{
|
|
@@ -2107,8 +2252,8 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2107
2252
|
),
|
|
2108
2253
|
getUpdates: (body, signal) =>
|
|
2109
2254
|
callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
|
|
2110
|
-
setMyCommands: (commands) =>
|
|
2111
|
-
callRecorded<boolean>("setMyCommands", { commands }),
|
|
2255
|
+
setMyCommands: (commands, options) =>
|
|
2256
|
+
callRecorded<boolean>("setMyCommands", { commands }, options),
|
|
2112
2257
|
sendChatAction: (chatId, action, options) =>
|
|
2113
2258
|
callRecorded<boolean>("sendChatAction", {
|
|
2114
2259
|
chat_id: chatId,
|
|
@@ -2152,16 +2297,16 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2152
2297
|
body.message_thread_id = options.message_thread_id;
|
|
2153
2298
|
return callRecorded<boolean>("sendMessageDraft", body);
|
|
2154
2299
|
},
|
|
2155
|
-
sendMessage: (body) =>
|
|
2156
|
-
callRecorded<TelegramSentMessage>("sendMessage", body),
|
|
2157
|
-
sendRichMessage: (body) =>
|
|
2158
|
-
callRecorded<TelegramSentMessage>("sendRichMessage", body),
|
|
2300
|
+
sendMessage: (body, options) =>
|
|
2301
|
+
callRecorded<TelegramSentMessage>("sendMessage", body, options),
|
|
2302
|
+
sendRichMessage: (body, options) =>
|
|
2303
|
+
callRecorded<TelegramSentMessage>("sendRichMessage", body, options),
|
|
2159
2304
|
sendRichMessageDraft: (body) =>
|
|
2160
2305
|
callRecorded<boolean>("sendRichMessageDraft", body),
|
|
2161
|
-
editMessageText: async (body) => {
|
|
2306
|
+
editMessageText: async (body, options) => {
|
|
2162
2307
|
const recoverError = deps.captureRequestErrorHandler?.(body);
|
|
2163
2308
|
try {
|
|
2164
|
-
await deps.client.call("editMessageText", body);
|
|
2309
|
+
await deps.client.call("editMessageText", body, options);
|
|
2165
2310
|
return "edited";
|
|
2166
2311
|
} catch (error) {
|
|
2167
2312
|
if (isTelegramMessageNotModifiedError(error)) return "unchanged";
|
|
@@ -2183,7 +2328,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2183
2328
|
reply_markup: replyMarkup,
|
|
2184
2329
|
});
|
|
2185
2330
|
},
|
|
2186
|
-
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
|
+
}
|
|
2187
2346
|
try {
|
|
2188
2347
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
2189
2348
|
} catch (error) {
|
|
@@ -2194,6 +2353,7 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2194
2353
|
method: "answerCallbackQuery",
|
|
2195
2354
|
}),
|
|
2196
2355
|
);
|
|
2356
|
+
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
2197
2357
|
}
|
|
2198
2358
|
},
|
|
2199
2359
|
answerGuestQuery: (
|
|
@@ -2259,16 +2419,19 @@ export function createTelegramApiClient(
|
|
|
2259
2419
|
const token = getBotToken();
|
|
2260
2420
|
// Cooldown keys retain only the public bot-id prefix, not the credential.
|
|
2261
2421
|
const botId = token?.match(/^(\d+):/)?.[1];
|
|
2262
|
-
const isDraft =
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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;
|
|
2266
2428
|
if (draftKey) {
|
|
2267
2429
|
const nowMs = now();
|
|
2268
2430
|
for (const [key, deadline] of draftRetryNotBeforeByTarget) {
|
|
2269
2431
|
if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
|
|
2270
2432
|
}
|
|
2271
|
-
if (draftRetryNotBeforeByTarget.has(draftKey))
|
|
2433
|
+
if (draftRetryNotBeforeByTarget.has(draftKey))
|
|
2434
|
+
return false as TResponse;
|
|
2272
2435
|
}
|
|
2273
2436
|
try {
|
|
2274
2437
|
// A draft is a replaceable snapshot, not a body to replay after backoff.
|
|
@@ -2299,10 +2462,18 @@ export function createTelegramApiClient(
|
|
|
2299
2462
|
});
|
|
2300
2463
|
} catch (error) {
|
|
2301
2464
|
if (draftKey && isRetryableTelegramApiError(error)) {
|
|
2302
|
-
draftRetryNotBeforeByTarget.set(
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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
|
+
);
|
|
2306
2477
|
}
|
|
2307
2478
|
throw error;
|
|
2308
2479
|
}
|
|
@@ -2334,12 +2505,16 @@ export function createTelegramApiClient(
|
|
|
2334
2505
|
options,
|
|
2335
2506
|
);
|
|
2336
2507
|
},
|
|
2337
|
-
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
2508
|
+
answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
|
|
2509
|
+
const assertAuthority = authorityOptions?.assertAuthority;
|
|
2510
|
+
const callbackOptions = assertAuthority
|
|
2511
|
+
? { ...options, assertAuthority }
|
|
2512
|
+
: options;
|
|
2338
2513
|
await answerTelegramCallbackQuery(
|
|
2339
2514
|
getBotToken(),
|
|
2340
2515
|
callbackQueryId,
|
|
2341
2516
|
text,
|
|
2342
|
-
|
|
2517
|
+
callbackOptions,
|
|
2343
2518
|
);
|
|
2344
2519
|
},
|
|
2345
2520
|
};
|