diff --git a/src/channels/plugins/onboarding/telegram.ts b/src/channels/plugins/onboarding/telegram.ts index 34badafb198..e9910270fc9 100644 --- a/src/channels/plugins/onboarding/telegram.ts +++ b/src/channels/plugins/onboarding/telegram.ts @@ -7,6 +7,7 @@ import { resolveDefaultTelegramAccountId, resolveTelegramAccount, } from "../../../telegram/accounts.js"; +import { fetchTelegramChatId } from "../../../telegram/api.js"; import { formatDocsLink } from "../../../terminal/links.js"; import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; @@ -85,25 +86,7 @@ async function promptTelegramAllowFrom(params: { return null; } const username = stripped.startsWith("@") ? stripped : `@${stripped}`; - const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`; - try { - const res = await fetch(url); - if (!res.ok) { - return null; - } - const data = (await res.json().catch(() => null)) as { - ok?: boolean; - result?: { id?: number | string }; - } | null; - const id = data?.ok ? data?.result?.id : undefined; - if (typeof id === "number" || typeof id === "string") { - return String(id); - } - return null; - } catch { - // Network error during username lookup - return null to prompt user for numeric ID - return null; - } + return await fetchTelegramChatId({ token, chatId: username }); }; const parseInput = (value: string) => diff --git a/src/channels/telegram/api.test.ts b/src/channels/telegram/api.test.ts new file mode 100644 index 00000000000..cb322289305 --- /dev/null +++ b/src/channels/telegram/api.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it, vi } from "vitest"; +import { fetchTelegramChatId } from "./api.js"; + +describe("fetchTelegramChatId", () => { + it("returns stringified id when Telegram getChat succeeds", async () => { + const fetchMock = vi.fn(async () => ({ + ok: true, + json: async () => ({ ok: true, result: { id: 12345 } }), + })); + vi.stubGlobal("fetch", fetchMock); + + const id = await fetchTelegramChatId({ + token: "abc", + chatId: "@user", + }); + + expect(id).toBe("12345"); + expect(fetchMock).toHaveBeenCalledWith( + "https://api.telegram.org/botabc/getChat?chat_id=%40user", + undefined, + ); + }); + + it("returns null when response is not ok", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: false, + json: async () => ({}), + })), + ); + + const id = await fetchTelegramChatId({ + token: "abc", + chatId: "@user", + }); + + expect(id).toBeNull(); + }); + + it("returns null on transport failures", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => { + throw new Error("network failed"); + }), + ); + + const id = await fetchTelegramChatId({ + token: "abc", + chatId: "@user", + }); + + expect(id).toBeNull(); + }); +}); diff --git a/src/channels/telegram/api.ts b/src/channels/telegram/api.ts new file mode 100644 index 00000000000..8831caa2b8a --- /dev/null +++ b/src/channels/telegram/api.ts @@ -0,0 +1,24 @@ +export async function fetchTelegramChatId(params: { + token: string; + chatId: string; + signal?: AbortSignal; +}): Promise { + const url = `https://api.telegram.org/bot${params.token}/getChat?chat_id=${encodeURIComponent(params.chatId)}`; + try { + const res = await fetch(url, params.signal ? { signal: params.signal } : undefined); + if (!res.ok) { + return null; + } + const data = (await res.json().catch(() => null)) as { + ok?: boolean; + result?: { id?: number | string }; + } | null; + const id = data?.ok ? data?.result?.id : undefined; + if (typeof id === "number" || typeof id === "string") { + return String(id); + } + return null; + } catch { + return null; + } +} diff --git a/src/commands/doctor-config-flow.ts b/src/commands/doctor-config-flow.ts index 4cfc673bf15..876c698ccee 100644 --- a/src/commands/doctor-config-flow.ts +++ b/src/commands/doctor-config-flow.ts @@ -5,6 +5,7 @@ import { isNumericTelegramUserId, normalizeTelegramAllowFromEntry, } from "../channels/telegram/allow-from.js"; +import { fetchTelegramChatId } from "../channels/telegram/api.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/config.js"; import { @@ -329,18 +330,13 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 4000); try { - const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`; - const res = await fetch(url, { signal: controller.signal }).catch(() => null); - if (!res || !res.ok) { - continue; - } - const data = (await res.json().catch(() => null)) as { - ok?: boolean; - result?: { id?: number | string }; - } | null; - const id = data?.ok ? data?.result?.id : undefined; - if (typeof id === "number" || typeof id === "string") { - return String(id); + const id = await fetchTelegramChatId({ + token, + chatId: username, + signal: controller.signal, + }); + if (id) { + return id; } } catch { // ignore and try next token