@llblab/pi-telegram 0.17.5 → 0.18.1
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 +42 -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 +410 -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 +9 -2
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +409 -41
- 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
|
}
|
|
@@ -631,7 +928,10 @@ export async function fetchTelegramBotIdentity(
|
|
|
631
928
|
botToken: string,
|
|
632
929
|
fetchImpl: typeof fetch = fetch,
|
|
633
930
|
): Promise<TelegramBotIdentityResponse> {
|
|
634
|
-
const
|
|
931
|
+
const url = `${TELEGRAM_API_BASE}/bot${botToken}/getMe`;
|
|
932
|
+
const response = await callTelegramTransportRequest((family) =>
|
|
933
|
+
fetchImpl === fetch ? telegramFetch(url, {}, family) : fetchImpl(url),
|
|
934
|
+
);
|
|
635
935
|
return response.json() as Promise<TelegramBotIdentityResponse>;
|
|
636
936
|
}
|
|
637
937
|
|
|
@@ -654,17 +954,38 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
654
954
|
const fileBlob = await openAsBlob(filePath);
|
|
655
955
|
return callTelegramWithRetry(
|
|
656
956
|
method,
|
|
657
|
-
async () => {
|
|
957
|
+
async (family) => {
|
|
958
|
+
if (family) {
|
|
959
|
+
const multipart = await buildTelegramMultipartBody(
|
|
960
|
+
fields,
|
|
961
|
+
fileField,
|
|
962
|
+
fileBlob,
|
|
963
|
+
fileName,
|
|
964
|
+
);
|
|
965
|
+
return telegramFetch(
|
|
966
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
967
|
+
{
|
|
968
|
+
method: "POST",
|
|
969
|
+
headers: { "content-type": multipart.contentType },
|
|
970
|
+
body: multipart.body as unknown as BodyInit,
|
|
971
|
+
signal: options?.signal,
|
|
972
|
+
},
|
|
973
|
+
family,
|
|
974
|
+
);
|
|
975
|
+
}
|
|
658
976
|
const form = new FormData();
|
|
659
977
|
for (const [key, value] of Object.entries(fields)) {
|
|
660
978
|
form.set(key, value);
|
|
661
979
|
}
|
|
662
980
|
form.set(fileField, fileBlob, fileName);
|
|
663
|
-
return
|
|
664
|
-
method
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
981
|
+
return telegramFetch(
|
|
982
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
983
|
+
{
|
|
984
|
+
method: "POST",
|
|
985
|
+
body: form,
|
|
986
|
+
signal: options?.signal,
|
|
987
|
+
},
|
|
988
|
+
);
|
|
668
989
|
},
|
|
669
990
|
options,
|
|
670
991
|
);
|
|
@@ -690,9 +1011,12 @@ export async function downloadTelegramFile(
|
|
|
690
1011
|
tempDir,
|
|
691
1012
|
`${randomUUID()}-${sanitizeFileName(suggestedName)}`,
|
|
692
1013
|
);
|
|
693
|
-
const response = await
|
|
694
|
-
|
|
695
|
-
|
|
1014
|
+
const response = await callTelegramTransportRequest((family) =>
|
|
1015
|
+
telegramFetch(
|
|
1016
|
+
`${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
|
|
1017
|
+
{ signal: options?.signal },
|
|
1018
|
+
family,
|
|
1019
|
+
),
|
|
696
1020
|
);
|
|
697
1021
|
if (!response.ok) {
|
|
698
1022
|
throw new Error(`Failed to download Telegram file: ${response.status}`);
|
|
@@ -730,9 +1054,13 @@ export async function answerTelegramCallbackQuery(
|
|
|
730
1054
|
: { callback_query_id: callbackQueryId },
|
|
731
1055
|
);
|
|
732
1056
|
} catch (error) {
|
|
733
|
-
options.recordRuntimeEvent?.(
|
|
734
|
-
|
|
735
|
-
|
|
1057
|
+
options.recordRuntimeEvent?.(
|
|
1058
|
+
"api",
|
|
1059
|
+
error,
|
|
1060
|
+
withTelegramTransportDiagnostics(error, {
|
|
1061
|
+
method: "answerCallbackQuery",
|
|
1062
|
+
}),
|
|
1063
|
+
);
|
|
736
1064
|
}
|
|
737
1065
|
}
|
|
738
1066
|
|
|
@@ -752,10 +1080,17 @@ export async function deleteTelegramMessage(
|
|
|
752
1080
|
}
|
|
753
1081
|
|
|
754
1082
|
export function createTelegramChatActionSender<TAction extends string>(
|
|
755
|
-
sendChatAction: (
|
|
1083
|
+
sendChatAction: (
|
|
1084
|
+
chatId: number,
|
|
1085
|
+
action: TAction,
|
|
1086
|
+
options?: { message_thread_id?: number },
|
|
1087
|
+
) => Promise<unknown>,
|
|
756
1088
|
action: TAction,
|
|
757
|
-
): (
|
|
758
|
-
|
|
1089
|
+
): (
|
|
1090
|
+
chatId: number,
|
|
1091
|
+
options?: { message_thread_id?: number },
|
|
1092
|
+
) => Promise<unknown> {
|
|
1093
|
+
return (chatId, options) => sendChatAction(chatId, action, options);
|
|
759
1094
|
}
|
|
760
1095
|
|
|
761
1096
|
export function createTelegramNativeMarkdownDraftSender(deps: {
|
|
@@ -801,9 +1136,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
801
1136
|
options?: TelegramApiCallOptions,
|
|
802
1137
|
): Promise<TResponse> => {
|
|
803
1138
|
try {
|
|
804
|
-
return await deps.client.call(method, body, options);
|
|
1139
|
+
return await deps.client.call<TResponse>(method, body, options);
|
|
805
1140
|
} catch (error) {
|
|
806
|
-
deps.recordRuntimeEvent(
|
|
1141
|
+
deps.recordRuntimeEvent(
|
|
1142
|
+
"api",
|
|
1143
|
+
error,
|
|
1144
|
+
withTelegramTransportDiagnostics(error, { method }),
|
|
1145
|
+
);
|
|
807
1146
|
throw error;
|
|
808
1147
|
}
|
|
809
1148
|
};
|
|
@@ -833,7 +1172,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
833
1172
|
options,
|
|
834
1173
|
);
|
|
835
1174
|
} catch (error) {
|
|
836
|
-
deps.recordRuntimeEvent(
|
|
1175
|
+
deps.recordRuntimeEvent(
|
|
1176
|
+
"multipart",
|
|
1177
|
+
error,
|
|
1178
|
+
withTelegramTransportDiagnostics(error, { method, fileName }),
|
|
1179
|
+
);
|
|
837
1180
|
throw error;
|
|
838
1181
|
}
|
|
839
1182
|
},
|
|
@@ -853,7 +1196,11 @@ export function createTelegramBridgeApiRuntime(
|
|
|
853
1196
|
},
|
|
854
1197
|
);
|
|
855
1198
|
} catch (error) {
|
|
856
|
-
deps.recordRuntimeEvent(
|
|
1199
|
+
deps.recordRuntimeEvent(
|
|
1200
|
+
"download",
|
|
1201
|
+
error,
|
|
1202
|
+
withTelegramTransportDiagnostics(error, { suggestedName }),
|
|
1203
|
+
);
|
|
857
1204
|
throw error;
|
|
858
1205
|
}
|
|
859
1206
|
},
|
|
@@ -867,24 +1214,33 @@ export function createTelegramBridgeApiRuntime(
|
|
|
867
1214
|
callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
|
|
868
1215
|
setMyCommands: (commands) =>
|
|
869
1216
|
callRecorded<boolean>("setMyCommands", { commands }),
|
|
870
|
-
sendChatAction: (chatId, action) =>
|
|
1217
|
+
sendChatAction: (chatId, action, options) =>
|
|
871
1218
|
callRecorded<boolean>("sendChatAction", {
|
|
872
1219
|
chat_id: chatId,
|
|
873
1220
|
action,
|
|
1221
|
+
...(options?.message_thread_id !== undefined
|
|
1222
|
+
? { message_thread_id: options.message_thread_id }
|
|
1223
|
+
: {}),
|
|
874
1224
|
}),
|
|
875
1225
|
sendTypingAction: createTelegramChatActionSender(
|
|
876
|
-
(chatId, action) =>
|
|
1226
|
+
(chatId, action, options) =>
|
|
877
1227
|
callRecorded<boolean>("sendChatAction", {
|
|
878
1228
|
chat_id: chatId,
|
|
879
1229
|
action,
|
|
1230
|
+
...(options?.message_thread_id !== undefined
|
|
1231
|
+
? { message_thread_id: options.message_thread_id }
|
|
1232
|
+
: {}),
|
|
880
1233
|
}),
|
|
881
1234
|
"typing",
|
|
882
1235
|
),
|
|
883
1236
|
sendRecordVoiceAction: createTelegramChatActionSender(
|
|
884
|
-
(chatId, action) =>
|
|
1237
|
+
(chatId, action, options) =>
|
|
885
1238
|
callRecorded<boolean>("sendChatAction", {
|
|
886
1239
|
chat_id: chatId,
|
|
887
1240
|
action,
|
|
1241
|
+
...(options?.message_thread_id !== undefined
|
|
1242
|
+
? { message_thread_id: options.message_thread_id }
|
|
1243
|
+
: {}),
|
|
888
1244
|
}),
|
|
889
1245
|
"record_voice",
|
|
890
1246
|
),
|
|
@@ -913,7 +1269,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
913
1269
|
return "edited";
|
|
914
1270
|
} catch (error) {
|
|
915
1271
|
if (isTelegramMessageNotModifiedError(error)) return "unchanged";
|
|
916
|
-
deps.recordRuntimeEvent(
|
|
1272
|
+
deps.recordRuntimeEvent(
|
|
1273
|
+
"api",
|
|
1274
|
+
error,
|
|
1275
|
+
withTelegramTransportDiagnostics(error, {
|
|
1276
|
+
method: "editMessageText",
|
|
1277
|
+
}),
|
|
1278
|
+
);
|
|
917
1279
|
throw error;
|
|
918
1280
|
}
|
|
919
1281
|
},
|
|
@@ -921,15 +1283,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
921
1283
|
try {
|
|
922
1284
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|
|
923
1285
|
} catch (error) {
|
|
924
|
-
deps.recordRuntimeEvent(
|
|
925
|
-
|
|
926
|
-
|
|
1286
|
+
deps.recordRuntimeEvent(
|
|
1287
|
+
"api",
|
|
1288
|
+
error,
|
|
1289
|
+
withTelegramTransportDiagnostics(error, {
|
|
1290
|
+
method: "answerCallbackQuery",
|
|
1291
|
+
}),
|
|
1292
|
+
);
|
|
927
1293
|
}
|
|
928
1294
|
},
|
|
929
1295
|
answerGuestQuery: (
|
|
930
1296
|
guestQueryId: string,
|
|
931
1297
|
text: string | undefined,
|
|
932
|
-
options:
|
|
1298
|
+
options:
|
|
1299
|
+
| { parseMode?: string; richMessage?: TelegramInputRichMessage }
|
|
1300
|
+
| undefined,
|
|
933
1301
|
) => {
|
|
934
1302
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
935
1303
|
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(
|