@llblab/pi-telegram 0.17.5 → 0.18.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 +67 -32
- package/BACKLOG.md +59 -19
- package/CHANGELOG.md +36 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/telegram-api.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
11
11
|
import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { request as requestHttps } from "node:https";
|
|
12
13
|
import { homedir } from "node:os";
|
|
13
14
|
import { join, resolve } from "node:path";
|
|
14
15
|
import { Readable, Transform } from "node:stream";
|
|
@@ -45,6 +46,21 @@ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
|
|
|
45
46
|
TELEGRAM_FILE_MAX_BYTES,
|
|
46
47
|
);
|
|
47
48
|
|
|
49
|
+
export type TelegramNetworkFamilyPolicy =
|
|
50
|
+
| "auto"
|
|
51
|
+
| "ipv4"
|
|
52
|
+
| "ipv6"
|
|
53
|
+
| "ipv4-fallback";
|
|
54
|
+
|
|
55
|
+
const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
|
|
56
|
+
const TELEGRAM_NETWORK_FAMILY_VALUES = new Set<TelegramNetworkFamilyPolicy>([
|
|
57
|
+
"auto",
|
|
58
|
+
"ipv4",
|
|
59
|
+
"ipv6",
|
|
60
|
+
"ipv4-fallback",
|
|
61
|
+
]);
|
|
62
|
+
type TelegramNetworkFamily = 4 | 6;
|
|
63
|
+
|
|
48
64
|
export interface TelegramUser {
|
|
49
65
|
id: number;
|
|
50
66
|
is_bot: boolean;
|
|
@@ -189,7 +205,9 @@ export interface TelegramSentMessage {
|
|
|
189
205
|
|
|
190
206
|
export interface TelegramReplyParameters {
|
|
191
207
|
message_id: number;
|
|
192
|
-
allow_sending_without_reply
|
|
208
|
+
allow_sending_without_reply?: boolean;
|
|
209
|
+
chat_id?: number;
|
|
210
|
+
message_thread_id?: number;
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
export type TelegramSendMessageBody = Record<string, unknown> & {
|
|
@@ -349,9 +367,19 @@ export interface TelegramBridgeApiRuntime {
|
|
|
349
367
|
setMyCommands: (
|
|
350
368
|
commands: readonly { command: string; description: string }[],
|
|
351
369
|
) => Promise<boolean>;
|
|
352
|
-
sendChatAction: (
|
|
353
|
-
|
|
354
|
-
|
|
370
|
+
sendChatAction: (
|
|
371
|
+
chatId: number,
|
|
372
|
+
action: string,
|
|
373
|
+
options?: { message_thread_id?: number },
|
|
374
|
+
) => Promise<boolean>;
|
|
375
|
+
sendTypingAction: (
|
|
376
|
+
chatId: number,
|
|
377
|
+
options?: { message_thread_id?: number },
|
|
378
|
+
) => Promise<unknown>;
|
|
379
|
+
sendRecordVoiceAction: (
|
|
380
|
+
chatId: number,
|
|
381
|
+
options?: { message_thread_id?: number },
|
|
382
|
+
) => Promise<unknown>;
|
|
355
383
|
sendMessageDraft: (
|
|
356
384
|
chatId: number,
|
|
357
385
|
draftId: number,
|
|
@@ -537,9 +565,271 @@ function unwrapTelegramApiResult<TResponse>(
|
|
|
537
565
|
return data.result;
|
|
538
566
|
}
|
|
539
567
|
|
|
568
|
+
function getTelegramNetworkFamilyPolicy(
|
|
569
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
570
|
+
): TelegramNetworkFamilyPolicy {
|
|
571
|
+
const value = env[TELEGRAM_NETWORK_FAMILY_ENV]?.trim().toLowerCase();
|
|
572
|
+
if (
|
|
573
|
+
TELEGRAM_NETWORK_FAMILY_VALUES.has(value as TelegramNetworkFamilyPolicy)
|
|
574
|
+
) {
|
|
575
|
+
return value as TelegramNetworkFamilyPolicy;
|
|
576
|
+
}
|
|
577
|
+
return "ipv4-fallback";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getTelegramNetworkFamily(
|
|
581
|
+
policy: TelegramNetworkFamilyPolicy,
|
|
582
|
+
): TelegramNetworkFamily | undefined {
|
|
583
|
+
if (policy === "ipv4") return 4;
|
|
584
|
+
if (policy === "ipv6") return 6;
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function isTelegramTransportFailure(error: unknown): boolean {
|
|
589
|
+
if (!(error instanceof Error)) return false;
|
|
590
|
+
if (error.name === "AbortError") return false;
|
|
591
|
+
if (error instanceof TypeError && /fetch failed/i.test(error.message)) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (error instanceof AggregateError) return true;
|
|
595
|
+
const code = getErrorCode(error);
|
|
596
|
+
if (
|
|
597
|
+
code === "ECONNREFUSED" ||
|
|
598
|
+
code === "ETIMEDOUT" ||
|
|
599
|
+
code === "ENETUNREACH" ||
|
|
600
|
+
code === "EHOSTUNREACH" ||
|
|
601
|
+
code === "ECONNRESET" ||
|
|
602
|
+
code === "EAI_AGAIN"
|
|
603
|
+
) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
return isTelegramTransportFailure(error.cause);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getTelegramRequestBodyBuffer(
|
|
610
|
+
body: BodyInit | null | undefined,
|
|
611
|
+
): Buffer | undefined {
|
|
612
|
+
if (body === undefined || body === null) return undefined;
|
|
613
|
+
if (typeof body === "string") return Buffer.from(body);
|
|
614
|
+
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
615
|
+
throw new Error("Unsupported Telegram HTTPS request body");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function buildTelegramMultipartBody(
|
|
619
|
+
fields: Record<string, string>,
|
|
620
|
+
fileField: string,
|
|
621
|
+
fileBlob: Blob,
|
|
622
|
+
fileName: string,
|
|
623
|
+
): Promise<{ body: Buffer; contentType: string }> {
|
|
624
|
+
const boundary = `pi-telegram-${randomUUID()}`;
|
|
625
|
+
const chunks: Buffer[] = [];
|
|
626
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
627
|
+
chunks.push(
|
|
628
|
+
Buffer.from(
|
|
629
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`,
|
|
630
|
+
),
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
chunks.push(
|
|
634
|
+
Buffer.from(
|
|
635
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${fileField}"; filename="${fileName}"\r\nContent-Type: ${fileBlob.type || "application/octet-stream"}\r\n\r\n`,
|
|
636
|
+
),
|
|
637
|
+
Buffer.from(await fileBlob.arrayBuffer()),
|
|
638
|
+
Buffer.from(`\r\n--${boundary}--\r\n`),
|
|
639
|
+
);
|
|
640
|
+
return {
|
|
641
|
+
body: Buffer.concat(chunks),
|
|
642
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function telegramHttpsFetch(
|
|
647
|
+
input: string | URL | Request,
|
|
648
|
+
init: RequestInit,
|
|
649
|
+
family: TelegramNetworkFamily,
|
|
650
|
+
): Promise<Response> {
|
|
651
|
+
const url = new URL(
|
|
652
|
+
typeof input === "string" || input instanceof URL ? input : input.url,
|
|
653
|
+
);
|
|
654
|
+
const body = getTelegramRequestBodyBuffer(init.body);
|
|
655
|
+
const headers = new Headers(init.headers);
|
|
656
|
+
if (body && !headers.has("content-length")) {
|
|
657
|
+
headers.set("content-length", String(body.byteLength));
|
|
658
|
+
}
|
|
659
|
+
return new Promise<Response>((resolve, reject) => {
|
|
660
|
+
const req = requestHttps(
|
|
661
|
+
url,
|
|
662
|
+
{
|
|
663
|
+
method: init.method ?? "GET",
|
|
664
|
+
family,
|
|
665
|
+
headers: Object.fromEntries(headers.entries()),
|
|
666
|
+
},
|
|
667
|
+
(res) => {
|
|
668
|
+
const responseHeaders = new Headers();
|
|
669
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
670
|
+
if (Array.isArray(value)) responseHeaders.set(key, value.join(", "));
|
|
671
|
+
else if (value !== undefined) responseHeaders.set(key, String(value));
|
|
672
|
+
}
|
|
673
|
+
resolve(
|
|
674
|
+
new Response(Readable.toWeb(res) as ReadableStream<Uint8Array>, {
|
|
675
|
+
status: res.statusCode ?? 200,
|
|
676
|
+
statusText: res.statusMessage,
|
|
677
|
+
headers: responseHeaders,
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
680
|
+
},
|
|
681
|
+
);
|
|
682
|
+
req.on("error", reject);
|
|
683
|
+
if (init.signal) {
|
|
684
|
+
if (init.signal.aborted)
|
|
685
|
+
req.destroy(new DOMException("Aborted", "AbortError"));
|
|
686
|
+
else {
|
|
687
|
+
init.signal.addEventListener(
|
|
688
|
+
"abort",
|
|
689
|
+
() => req.destroy(new DOMException("Aborted", "AbortError")),
|
|
690
|
+
{ once: true },
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
req.end(body);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let telegramHttpsFetchForTesting: typeof telegramHttpsFetch | undefined;
|
|
699
|
+
|
|
700
|
+
export function setTelegramApiHttpsFetchForTesting(
|
|
701
|
+
fetchImpl: typeof telegramHttpsFetch | undefined,
|
|
702
|
+
): () => void {
|
|
703
|
+
const previous = telegramHttpsFetchForTesting;
|
|
704
|
+
telegramHttpsFetchForTesting = fetchImpl;
|
|
705
|
+
return () => {
|
|
706
|
+
telegramHttpsFetchForTesting = previous;
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function telegramFetch(
|
|
711
|
+
input: string | URL | Request,
|
|
712
|
+
init: RequestInit = {},
|
|
713
|
+
family?: TelegramNetworkFamily,
|
|
714
|
+
): Promise<Response> {
|
|
715
|
+
if (!family) return fetch(input, init);
|
|
716
|
+
return (telegramHttpsFetchForTesting ?? telegramHttpsFetch)(
|
|
717
|
+
input,
|
|
718
|
+
init,
|
|
719
|
+
family,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function callTelegramTransportRequest(
|
|
724
|
+
request: (family?: TelegramNetworkFamily) => Promise<Response>,
|
|
725
|
+
): Promise<Response> {
|
|
726
|
+
const policy = getTelegramNetworkFamilyPolicy();
|
|
727
|
+
if (policy === "auto") return request();
|
|
728
|
+
const family = getTelegramNetworkFamily(policy);
|
|
729
|
+
if (family) return request(family);
|
|
730
|
+
try {
|
|
731
|
+
return await request();
|
|
732
|
+
} catch (error) {
|
|
733
|
+
if (!isTelegramTransportFailure(error)) throw error;
|
|
734
|
+
return request(4);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function getErrorCode(error: Error): string | undefined {
|
|
739
|
+
const maybeCode = (error as { code?: unknown }).code;
|
|
740
|
+
return typeof maybeCode === "string" ? maybeCode : undefined;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function getErrorAddress(error: Error): string | undefined {
|
|
744
|
+
const maybeAddress = (error as { address?: unknown }).address;
|
|
745
|
+
return typeof maybeAddress === "string" ? maybeAddress : undefined;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function getErrorPort(error: Error): number | undefined {
|
|
749
|
+
const maybePort = (error as { port?: unknown }).port;
|
|
750
|
+
return typeof maybePort === "number" ? maybePort : undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function getErrorFamily(error: Error): number | string | undefined {
|
|
754
|
+
const maybeFamily = (error as { family?: unknown }).family;
|
|
755
|
+
if (typeof maybeFamily === "number" || typeof maybeFamily === "string") {
|
|
756
|
+
return maybeFamily;
|
|
757
|
+
}
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function describeTelegramErrorSummary(error: Error): {
|
|
762
|
+
name: string;
|
|
763
|
+
message: string;
|
|
764
|
+
code?: string;
|
|
765
|
+
} {
|
|
766
|
+
return {
|
|
767
|
+
name: error.name,
|
|
768
|
+
message: error.message,
|
|
769
|
+
...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function describeTelegramTransportAttempt(error: Error): {
|
|
774
|
+
name: string;
|
|
775
|
+
code?: string;
|
|
776
|
+
address?: string;
|
|
777
|
+
port?: number;
|
|
778
|
+
family?: number | string;
|
|
779
|
+
} {
|
|
780
|
+
return {
|
|
781
|
+
name: error.name,
|
|
782
|
+
...(getErrorCode(error) ? { code: getErrorCode(error) } : {}),
|
|
783
|
+
...(getErrorAddress(error) ? { address: getErrorAddress(error) } : {}),
|
|
784
|
+
...(getErrorPort(error) ? { port: getErrorPort(error) } : {}),
|
|
785
|
+
...(getErrorFamily(error) ? { family: getErrorFamily(error) } : {}),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function describeTelegramTransportError(error: unknown):
|
|
790
|
+
| {
|
|
791
|
+
error: { name: string; message: string; code?: string };
|
|
792
|
+
cause?: { name: string; message: string; code?: string };
|
|
793
|
+
attempts?: Array<{
|
|
794
|
+
name: string;
|
|
795
|
+
code?: string;
|
|
796
|
+
address?: string;
|
|
797
|
+
port?: number;
|
|
798
|
+
family?: number | string;
|
|
799
|
+
}>;
|
|
800
|
+
}
|
|
801
|
+
| undefined {
|
|
802
|
+
if (!isTelegramTransportFailure(error) || !(error instanceof Error)) {
|
|
803
|
+
return undefined;
|
|
804
|
+
}
|
|
805
|
+
const cause = error.cause instanceof Error ? error.cause : undefined;
|
|
806
|
+
const aggregate =
|
|
807
|
+
error instanceof AggregateError
|
|
808
|
+
? error
|
|
809
|
+
: cause instanceof AggregateError
|
|
810
|
+
? cause
|
|
811
|
+
: undefined;
|
|
812
|
+
const attempts = aggregate?.errors
|
|
813
|
+
.filter((attempt): attempt is Error => attempt instanceof Error)
|
|
814
|
+
.map(describeTelegramTransportAttempt);
|
|
815
|
+
return {
|
|
816
|
+
error: describeTelegramErrorSummary(error),
|
|
817
|
+
...(cause ? { cause: describeTelegramErrorSummary(cause) } : {}),
|
|
818
|
+
...(attempts && attempts.length > 0 ? { attempts } : {}),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function withTelegramTransportDiagnostics(
|
|
823
|
+
error: unknown,
|
|
824
|
+
details: Record<string, unknown>,
|
|
825
|
+
): Record<string, unknown> {
|
|
826
|
+
const transport = describeTelegramTransportError(error);
|
|
827
|
+
return transport ? { ...details, transport } : details;
|
|
828
|
+
}
|
|
829
|
+
|
|
540
830
|
async function callTelegramWithRetry<TResponse>(
|
|
541
831
|
method: string,
|
|
542
|
-
request: () => Promise<Response>,
|
|
832
|
+
request: (family?: TelegramNetworkFamily) => Promise<Response>,
|
|
543
833
|
options: TelegramApiCallOptions | undefined,
|
|
544
834
|
): Promise<TResponse> {
|
|
545
835
|
const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
|
|
@@ -549,7 +839,10 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
549
839
|
try {
|
|
550
840
|
return unwrapTelegramApiResult(
|
|
551
841
|
method,
|
|
552
|
-
await parseTelegramApiResponse<TResponse>(
|
|
842
|
+
await parseTelegramApiResponse<TResponse>(
|
|
843
|
+
await callTelegramTransportRequest(request),
|
|
844
|
+
method,
|
|
845
|
+
),
|
|
553
846
|
);
|
|
554
847
|
} catch (error) {
|
|
555
848
|
if (attempt >= maxAttempts - 1 || !isRetryableTelegramApiError(error)) {
|
|
@@ -611,13 +904,17 @@ export async function callTelegram<TResponse>(
|
|
|
611
904
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
612
905
|
return callTelegramWithRetry(
|
|
613
906
|
method,
|
|
614
|
-
async () =>
|
|
615
|
-
|
|
616
|
-
method
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
907
|
+
async (family) =>
|
|
908
|
+
telegramFetch(
|
|
909
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
910
|
+
{
|
|
911
|
+
method: "POST",
|
|
912
|
+
headers: { "content-type": "application/json" },
|
|
913
|
+
body: JSON.stringify(body),
|
|
914
|
+
signal: options?.signal,
|
|
915
|
+
},
|
|
916
|
+
family,
|
|
917
|
+
),
|
|
621
918
|
options,
|
|
622
919
|
);
|
|
623
920
|
}
|
|
@@ -654,17 +951,38 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
654
951
|
const fileBlob = await openAsBlob(filePath);
|
|
655
952
|
return callTelegramWithRetry(
|
|
656
953
|
method,
|
|
657
|
-
async () => {
|
|
954
|
+
async (family) => {
|
|
955
|
+
if (family) {
|
|
956
|
+
const multipart = await buildTelegramMultipartBody(
|
|
957
|
+
fields,
|
|
958
|
+
fileField,
|
|
959
|
+
fileBlob,
|
|
960
|
+
fileName,
|
|
961
|
+
);
|
|
962
|
+
return telegramFetch(
|
|
963
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
964
|
+
{
|
|
965
|
+
method: "POST",
|
|
966
|
+
headers: { "content-type": multipart.contentType },
|
|
967
|
+
body: multipart.body as unknown as BodyInit,
|
|
968
|
+
signal: options?.signal,
|
|
969
|
+
},
|
|
970
|
+
family,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
658
973
|
const form = new FormData();
|
|
659
974
|
for (const [key, value] of Object.entries(fields)) {
|
|
660
975
|
form.set(key, value);
|
|
661
976
|
}
|
|
662
977
|
form.set(fileField, fileBlob, fileName);
|
|
663
|
-
return
|
|
664
|
-
method
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
978
|
+
return telegramFetch(
|
|
979
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
980
|
+
{
|
|
981
|
+
method: "POST",
|
|
982
|
+
body: form,
|
|
983
|
+
signal: options?.signal,
|
|
984
|
+
},
|
|
985
|
+
);
|
|
668
986
|
},
|
|
669
987
|
options,
|
|
670
988
|
);
|
|
@@ -690,9 +1008,12 @@ export async function downloadTelegramFile(
|
|
|
690
1008
|
tempDir,
|
|
691
1009
|
`${randomUUID()}-${sanitizeFileName(suggestedName)}`,
|
|
692
1010
|
);
|
|
693
|
-
const response = await
|
|
694
|
-
|
|
695
|
-
|
|
1011
|
+
const response = await callTelegramTransportRequest((family) =>
|
|
1012
|
+
telegramFetch(
|
|
1013
|
+
`${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
|
|
1014
|
+
{ signal: options?.signal },
|
|
1015
|
+
family,
|
|
1016
|
+
),
|
|
696
1017
|
);
|
|
697
1018
|
if (!response.ok) {
|
|
698
1019
|
throw new Error(`Failed to download Telegram file: ${response.status}`);
|
|
@@ -730,9 +1051,13 @@ export async function answerTelegramCallbackQuery(
|
|
|
730
1051
|
: { callback_query_id: callbackQueryId },
|
|
731
1052
|
);
|
|
732
1053
|
} catch (error) {
|
|
733
|
-
options.recordRuntimeEvent?.(
|
|
734
|
-
|
|
735
|
-
|
|
1054
|
+
options.recordRuntimeEvent?.(
|
|
1055
|
+
"api",
|
|
1056
|
+
error,
|
|
1057
|
+
withTelegramTransportDiagnostics(error, {
|
|
1058
|
+
method: "answerCallbackQuery",
|
|
1059
|
+
}),
|
|
1060
|
+
);
|
|
736
1061
|
}
|
|
737
1062
|
}
|
|
738
1063
|
|
|
@@ -752,10 +1077,17 @@ export async function deleteTelegramMessage(
|
|
|
752
1077
|
}
|
|
753
1078
|
|
|
754
1079
|
export function createTelegramChatActionSender<TAction extends string>(
|
|
755
|
-
sendChatAction: (
|
|
1080
|
+
sendChatAction: (
|
|
1081
|
+
chatId: number,
|
|
1082
|
+
action: TAction,
|
|
1083
|
+
options?: { message_thread_id?: number },
|
|
1084
|
+
) => Promise<unknown>,
|
|
756
1085
|
action: TAction,
|
|
757
|
-
): (
|
|
758
|
-
|
|
1086
|
+
): (
|
|
1087
|
+
chatId: number,
|
|
1088
|
+
options?: { message_thread_id?: number },
|
|
1089
|
+
) => Promise<unknown> {
|
|
1090
|
+
return (chatId, options) => sendChatAction(chatId, action, options);
|
|
759
1091
|
}
|
|
760
1092
|
|
|
761
1093
|
export function createTelegramNativeMarkdownDraftSender(deps: {
|
|
@@ -801,9 +1133,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
801
1133
|
options?: TelegramApiCallOptions,
|
|
802
1134
|
): Promise<TResponse> => {
|
|
803
1135
|
try {
|
|
804
|
-
return await deps.client.call(method, body, options);
|
|
1136
|
+
return await deps.client.call<TResponse>(method, body, options);
|
|
805
1137
|
} catch (error) {
|
|
806
|
-
deps.recordRuntimeEvent(
|
|
1138
|
+
deps.recordRuntimeEvent(
|
|
1139
|
+
"api",
|
|
1140
|
+
error,
|
|
1141
|
+
withTelegramTransportDiagnostics(error, { method }),
|
|
1142
|
+
);
|
|
807
1143
|
throw error;
|
|
808
1144
|
}
|
|
809
1145
|
};
|
|
@@ -833,7 +1169,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
833
1169
|
options,
|
|
834
1170
|
);
|
|
835
1171
|
} catch (error) {
|
|
836
|
-
deps.recordRuntimeEvent(
|
|
1172
|
+
deps.recordRuntimeEvent(
|
|
1173
|
+
"multipart",
|
|
1174
|
+
error,
|
|
1175
|
+
withTelegramTransportDiagnostics(error, { method, fileName }),
|
|
1176
|
+
);
|
|
837
1177
|
throw error;
|
|
838
1178
|
}
|
|
839
1179
|
},
|
|
@@ -853,7 +1193,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
853
1193
|
},
|
|
854
1194
|
);
|
|
855
1195
|
} catch (error) {
|
|
856
|
-
deps.recordRuntimeEvent(
|
|
1196
|
+
deps.recordRuntimeEvent(
|
|
1197
|
+
"download",
|
|
1198
|
+
error,
|
|
1199
|
+
withTelegramTransportDiagnostics(error, { suggestedName }),
|
|
1200
|
+
);
|
|
857
1201
|
throw error;
|
|
858
1202
|
}
|
|
859
1203
|
},
|
|
@@ -867,24 +1211,33 @@ export function createTelegramBridgeApiRuntime(
|
|
|
867
1211
|
callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
|
|
868
1212
|
setMyCommands: (commands) =>
|
|
869
1213
|
callRecorded<boolean>("setMyCommands", { commands }),
|
|
870
|
-
sendChatAction: (chatId, action) =>
|
|
1214
|
+
sendChatAction: (chatId, action, options) =>
|
|
871
1215
|
callRecorded<boolean>("sendChatAction", {
|
|
872
1216
|
chat_id: chatId,
|
|
873
1217
|
action,
|
|
1218
|
+
...(options?.message_thread_id !== undefined
|
|
1219
|
+
? { message_thread_id: options.message_thread_id }
|
|
1220
|
+
: {}),
|
|
874
1221
|
}),
|
|
875
1222
|
sendTypingAction: createTelegramChatActionSender(
|
|
876
|
-
(chatId, action) =>
|
|
1223
|
+
(chatId, action, options) =>
|
|
877
1224
|
callRecorded<boolean>("sendChatAction", {
|
|
878
1225
|
chat_id: chatId,
|
|
879
1226
|
action,
|
|
1227
|
+
...(options?.message_thread_id !== undefined
|
|
1228
|
+
? { message_thread_id: options.message_thread_id }
|
|
1229
|
+
: {}),
|
|
880
1230
|
}),
|
|
881
1231
|
"typing",
|
|
882
1232
|
),
|
|
883
1233
|
sendRecordVoiceAction: createTelegramChatActionSender(
|
|
884
|
-
(chatId, action) =>
|
|
1234
|
+
(chatId, action, options) =>
|
|
885
1235
|
callRecorded<boolean>("sendChatAction", {
|
|
886
1236
|
chat_id: chatId,
|
|
887
1237
|
action,
|
|
1238
|
+
...(options?.message_thread_id !== undefined
|
|
1239
|
+
? { message_thread_id: options.message_thread_id }
|
|
1240
|
+
: {}),
|
|
888
1241
|
}),
|
|
889
1242
|
"record_voice",
|
|
890
1243
|
),
|
|
@@ -913,7 +1266,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
913
1266
|
return "edited";
|
|
914
1267
|
} catch (error) {
|
|
915
1268
|
if (isTelegramMessageNotModifiedError(error)) return "unchanged";
|
|
916
|
-
deps.recordRuntimeEvent(
|
|
1269
|
+
deps.recordRuntimeEvent(
|
|
1270
|
+
"api",
|
|
1271
|
+
error,
|
|
1272
|
+
withTelegramTransportDiagnostics(error, {
|
|
1273
|
+
method: "editMessageText",
|
|
1274
|
+
}),
|
|
1275
|
+
);
|
|
917
1276
|
throw error;
|
|
918
1277
|
}
|
|
919
1278
|
},
|
|
@@ -921,15 +1280,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
921
1280
|
try {
|
|
922
1281
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
923
1282
|
} catch (error) {
|
|
924
|
-
deps.recordRuntimeEvent(
|
|
925
|
-
|
|
926
|
-
|
|
1283
|
+
deps.recordRuntimeEvent(
|
|
1284
|
+
"api",
|
|
1285
|
+
error,
|
|
1286
|
+
withTelegramTransportDiagnostics(error, {
|
|
1287
|
+
method: "answerCallbackQuery",
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
927
1290
|
}
|
|
928
1291
|
},
|
|
929
1292
|
answerGuestQuery: (
|
|
930
1293
|
guestQueryId: string,
|
|
931
1294
|
text: string | undefined,
|
|
932
|
-
options:
|
|
1295
|
+
options:
|
|
1296
|
+
| { parseMode?: string; richMessage?: TelegramInputRichMessage }
|
|
1297
|
+
| undefined,
|
|
933
1298
|
) => {
|
|
934
1299
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
935
1300
|
if (text !== undefined || options?.richMessage) {
|
package/lib/text-groups.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface TelegramTextGroupMessage {
|
|
|
12
12
|
message_id: number;
|
|
13
13
|
media_group_id?: string;
|
|
14
14
|
chat: { id: number };
|
|
15
|
+
message_thread_id?: number;
|
|
15
16
|
from?: { id: number; is_bot?: boolean };
|
|
16
17
|
text?: string;
|
|
17
18
|
caption?: string;
|
|
@@ -70,7 +71,10 @@ function getTelegramTextGroupKey(
|
|
|
70
71
|
if (message.media_group_id) return undefined;
|
|
71
72
|
if (!message.from || message.from.is_bot) return undefined;
|
|
72
73
|
if (typeof message.text !== "string") return undefined;
|
|
73
|
-
|
|
74
|
+
const threadKey = typeof message.message_thread_id === "number"
|
|
75
|
+
? `thread:${message.message_thread_id}`
|
|
76
|
+
: "private";
|
|
77
|
+
return `${message.chat.id}:${threadKey}:${message.from.id}`;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
function canStartTelegramTextGroup(
|