From 5b94c4ce9396b3b38e33be167ce564683736a0b8 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 3 May 2026 20:46:32 +0530 Subject: [PATCH] fix(telegram): start polling after webhook cleanup timeout (#76735) Summary: - The branch changes Telegram polling startup to reuse the successful probe `getMe` result as grammY `botInfo` ... es` after recoverable `deleteWebhook` failures, and updates Telegram docs, changelog, and regression tests. - Reproducibility: yes. for the narrow PR bug: source inspection shows current main can block before polling o ... d timeout coverage that reaches `run()`. The full linked high-RTT report remains only partially reproduced. Automerge notes: - Ran the ClawSweeper repair loop before final review. - Included post-review commit in the final squash: fix(telegram): start polling after webhook cleanup timeout - Included post-review commit in the final squash: fix(telegram): extract bot info contract Validation: - ClawSweeper review passed for head c74bbdd1ff95695909c1543fa09be001dc68d2b9. - Required merge gates passed before the squash merge. Prepared head SHA: c74bbdd1ff95695909c1543fa09be001dc68d2b9 Review: https://github.com/openclaw/openclaw/pull/76735#issuecomment-4366417178 Co-authored-by: Ayaan Zaidi Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + docs/channels/telegram.md | 3 +- extensions/telegram/src/bot-core.ts | 6 +- extensions/telegram/src/bot-info.ts | 16 ++++ .../bot.create-telegram-bot.test-harness.ts | 8 +- .../src/bot.create-telegram-bot.test.ts | 26 ++++++ extensions/telegram/src/bot.types.ts | 3 + .../telegram/src/channel.gateway.test.ts | 39 +++++++++ extensions/telegram/src/channel.ts | 4 + extensions/telegram/src/monitor.test.ts | 37 +++------ extensions/telegram/src/monitor.ts | 1 + extensions/telegram/src/monitor.types.ts | 2 + .../telegram/src/polling-session.test.ts | 24 ++++++ extensions/telegram/src/polling-session.ts | 30 +------ extensions/telegram/src/probe.test.ts | 23 +++++- extensions/telegram/src/probe.ts | 82 ++++++++++++++----- 16 files changed, 228 insertions(+), 77 deletions(-) create mode 100644 extensions/telegram/src/bot-info.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5627f0f6a12..1b6b876ff96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc. - Discord/status: honor explicit `messages.statusReactions.enabled: true` in tool-only guild channels so queued ack reactions can progress through thinking/done lifecycle reactions instead of stopping at the initial emoji. Thanks @Marvinthebored. - Agents/OpenAI: omit Chat Completions `reasoning_effort` for `gpt-5.4-mini` only when function tools are present while preserving tool-free Chat and Responses reasoning support, preventing Telegram-routed fallback runs from hanging after OpenAI rejects tool payloads. Fixes #76176. Thanks @ThisIsAdilah and @chinar-amrutkar. +- Telegram: reuse the successful startup `getMe` probe for grammY polling startup and continue into `getUpdates` after recoverable `deleteWebhook` cleanup failures, reducing high-latency Bot API control-plane calls before long polling starts. Refs #76388. Thanks @jackiedepp. - Agents/models: forward model `maxTokens` as the default output-token limit for OpenAI-compatible Responses and Completions transports when no runtime override is provided, preventing provider defaults from silently truncating larger outputs. (#76645) Thanks @joeyfrasier. - macOS CLI/onboarding: honor sensitive wizard text steps in `openclaw-mac wizard` with termios no-echo input, suppressing saved credential previews while preserving long API keys and gateway tokens. Fixes #76698. Thanks @anurag-bg-neu and @sallyom. - Control UI/Skills: fix skill detail modal silently failing to open in all browsers by deferring `showModal()` until the dialog element is connected to the DOM; the Lit `ref` callback fired before connection causing a `DOMException: HTMLDialogElement.showModal: Dialog element is not connected` on every skill click. Thanks @nickmopen. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 3030f91efff..a4f51e1b346 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -855,7 +855,8 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance - `getMe returned 401` is a Telegram authentication failure for the configured bot token. - Re-copy or regenerate the bot token in BotFather, then update `channels.telegram.botToken`, `channels.telegram.tokenFile`, `channels.telegram.accounts..botToken`, or `TELEGRAM_BOT_TOKEN` for the default account. - `deleteWebhook 401 Unauthorized` during startup is also an auth failure; treating it as "no webhook exists" would only defer the same bad-token failure to later API calls. - - If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw checks `getWebhookInfo`; when Telegram reports an empty webhook URL, polling continues because cleanup is already satisfied. + - If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw continues into long polling instead of making another pre-poll control-plane call. A still-active webhook surfaces as a `getUpdates` conflict; OpenClaw then rebuilds the Telegram transport and retries webhook cleanup. + - After a successful startup `getMe` probe, OpenClaw reuses that bot identity for grammY polling startup so the runner does not need a second `getMe` before the first `getUpdates`. diff --git a/extensions/telegram/src/bot-core.ts b/extensions/telegram/src/bot-core.ts index cbf88db7d55..06701855ee3 100644 --- a/extensions/telegram/src/bot-core.ts +++ b/extensions/telegram/src/bot-core.ts @@ -348,7 +348,11 @@ export function createTelegramBotCore( } : undefined; - const bot = new botRuntime.Bot(opts.token, client ? { client } : undefined); + const botConfig = + client || opts.botInfo + ? { ...(client ? { client } : {}), ...(opts.botInfo ? { botInfo: opts.botInfo } : {}) } + : undefined; + const bot = new botRuntime.Bot(opts.token, botConfig); bot.api.config.use(botRuntime.apiThrottler()); // Catch all errors from bot middleware to prevent unhandled rejections bot.catch((err) => { diff --git a/extensions/telegram/src/bot-info.ts b/extensions/telegram/src/bot-info.ts new file mode 100644 index 00000000000..76fac8ef8c4 --- /dev/null +++ b/extensions/telegram/src/bot-info.ts @@ -0,0 +1,16 @@ +export type TelegramBotInfo = { + id: number; + is_bot: true; + first_name: string; + last_name?: string; + username: string; + language_code?: string; + can_join_groups: boolean; + can_read_all_group_messages: boolean; + can_manage_bots: boolean; + supports_inline_queries: boolean; + can_connect_to_business: boolean; + has_main_web_app: boolean; + has_topics_enabled: boolean; + allows_users_to_create_topics: boolean; +}; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index 529eb19fc0b..d765f77659a 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -266,7 +266,9 @@ const grammySpies = vi.hoisted(() => ({ onSpy: vi.fn(), stopSpy: vi.fn(), commandSpy: vi.fn(), - botCtorSpy: vi.fn((_: string, __?: { client?: { fetch?: typeof fetch } }) => undefined), + botCtorSpy: vi.fn( + (_: string, __?: { client?: { fetch?: typeof fetch }; botInfo?: unknown }) => undefined, + ), answerCallbackQuerySpy: vi.fn(async () => undefined) as AnyAsyncMock, sendChatActionSpy: vi.fn(), editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock, @@ -290,7 +292,7 @@ export const onSpy: AnyMock = grammySpies.onSpy; export const stopSpy: AnyMock = grammySpies.stopSpy; export const commandSpy: AnyMock = grammySpies.commandSpy; export const botCtorSpy: MockFn< - (token: string, options?: { client?: { fetch?: typeof fetch } }) => void + (token: string, options?: { client?: { fetch?: typeof fetch }; botInfo?: unknown }) => void > = grammySpies.botCtorSpy; export const answerCallbackQuerySpy: AnyAsyncMock = grammySpies.answerCallbackQuerySpy; export const sendChatActionSpy: AnyMock = grammySpies.sendChatActionSpy; @@ -341,7 +343,7 @@ export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = { catch = vi.fn(); constructor( public token: string, - public options?: { client?: { fetch?: typeof fetch } }, + public options?: { client?: { fetch?: typeof fetch }; botInfo?: unknown }, ) { (grammySpies.botCtorSpy as unknown as (token: string, options?: unknown) => void)( token, diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 0c7d4969d55..2c8546e5088 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -278,6 +278,32 @@ describe("createTelegramBot", () => { ); }); + it("passes startup probe botInfo to grammY", () => { + const botInfo = { + id: 123456, + is_bot: true, + first_name: "OpenClaw", + username: "openclaw_bot", + can_join_groups: true, + can_read_all_group_messages: false, + can_manage_bots: false, + supports_inline_queries: false, + can_connect_to_business: false, + has_main_web_app: false, + has_topics_enabled: false, + allows_users_to_create_topics: false, + } as const; + + createTelegramBot({ token: "tok", botInfo }); + + expect(botCtorSpy).toHaveBeenCalledWith( + "tok", + expect.objectContaining({ + botInfo, + }), + ); + }); + it("normalizes full Telegram bot endpoint apiRoot before passing it to grammY", () => { loadConfig.mockReturnValue({ channels: { diff --git a/extensions/telegram/src/bot.types.ts b/extensions/telegram/src/bot.types.ts index 24ddcb2c6af..c00c861d113 100644 --- a/extensions/telegram/src/bot.types.ts +++ b/extensions/telegram/src/bot.types.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-types"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import type { TelegramBotDeps } from "./bot-deps.js"; +import type { TelegramBotInfo } from "./bot-info.js"; import type { TelegramTransport } from "./fetch.js"; export type TelegramBotOptions = { @@ -14,6 +15,8 @@ export type TelegramBotOptions = { replyToMode?: ReplyToMode; proxyFetch?: typeof fetch; config?: OpenClawConfig; + /** Bot identity returned by the startup getMe probe. Avoids a duplicate grammY init getMe before polling. */ + botInfo?: TelegramBotInfo; /** Signal to abort in-flight Telegram API fetch requests (e.g. getUpdates) on shutdown. */ fetchAbortSignal?: AbortSignal; /** Minimum grammY client timeout when timeoutSeconds is configured on long-polling bots. */ diff --git a/extensions/telegram/src/channel.gateway.test.ts b/extensions/telegram/src/channel.gateway.test.ts index bc64c63005a..3cb24dac36c 100644 --- a/extensions/telegram/src/channel.gateway.test.ts +++ b/extensions/telegram/src/channel.gateway.test.ts @@ -156,6 +156,45 @@ describe("telegramPlugin gateway startup", () => { ); }); + it("passes successful startup probe botInfo into the polling monitor", async () => { + installTelegramRuntime(); + const botInfo = { + id: 123456, + is_bot: true, + first_name: "OpenClaw", + username: "openclaw_bot", + can_join_groups: true, + can_read_all_group_messages: false, + can_manage_bots: false, + supports_inline_queries: false, + can_connect_to_business: false, + has_main_web_app: false, + has_topics_enabled: false, + allows_users_to_create_topics: false, + } as const; + probeTelegram.mockResolvedValue({ + ok: true, + status: null, + error: null, + elapsedMs: 12, + bot: { + id: botInfo.id, + username: botInfo.username, + }, + botInfo, + }); + monitorTelegramProvider.mockResolvedValue(undefined); + + const { task } = startTelegramAccount(); + + await expect(task).resolves.toBeUndefined(); + expect(monitorTelegramProvider).toHaveBeenCalledWith( + expect.objectContaining({ + botInfo, + }), + ); + }); + it("honors higher per-account timeoutSeconds for startup probe", async () => { installTelegramRuntime(); probeTelegram.mockResolvedValue({ diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 3545f01f533..ec8236c5cec 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -40,6 +40,7 @@ import { resolveTelegramAutoThreadId } from "./action-threading.js"; import { lookupTelegramChatId } from "./api-fetch.js"; import { telegramApprovalCapability } from "./approval-native.js"; import * as auditModule from "./audit.js"; +import type { TelegramBotInfo } from "./bot-info.js"; import { buildTelegramGroupPeerId } from "./bot/helpers.js"; import { telegramMessageActions as telegramMessageActionsImpl } from "./channel-actions.js"; import { @@ -897,6 +898,7 @@ export const telegramPlugin = createChatChannelPlugin({ const token = (account.token ?? "").trim(); let telegramBotLabel = ""; let unauthorizedTokenReason: string | null = null; + let botInfo: TelegramBotInfo | undefined; try { const probe = await resolveTelegramProbe()( token, @@ -913,6 +915,7 @@ export const telegramPlugin = createChatChannelPlugin({ if (username) { telegramBotLabel = ` (@${username})`; } + botInfo = probe.ok ? probe.botInfo : undefined; if (!probe.ok && probe.status === 401) { unauthorizedTokenReason = formatTelegramUnauthorizedTokenError(account); } @@ -944,6 +947,7 @@ export const telegramPlugin = createChatChannelPlugin({ webhookHost: account.config.webhookHost, webhookPort: account.config.webhookPort, webhookCertPath: account.config.webhookCertPath, + botInfo, setStatus, }); }, diff --git a/extensions/telegram/src/monitor.test.ts b/extensions/telegram/src/monitor.test.ts index 757540a833d..a44f856468d 100644 --- a/extensions/telegram/src/monitor.test.ts +++ b/extensions/telegram/src/monitor.test.ts @@ -495,50 +495,35 @@ describe("monitorTelegramProvider (grammY)", () => { expect(order).toEqual(["deleteWebhook", "run"]); }); - it("retries recoverable deleteWebhook failures before polling", async () => { + it("starts polling after recoverable deleteWebhook failures", async () => { const abort = new AbortController(); const cleanupError = makeRecoverableFetchError(); api.deleteWebhook.mockReset(); - api.getWebhookInfo.mockReset().mockResolvedValueOnce({ url: "https://example.test/hook" }); - api.deleteWebhook.mockRejectedValueOnce(cleanupError).mockResolvedValueOnce(true); - mockRunOnceAndAbort(abort); - - await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal }); - - expect(api.deleteWebhook).toHaveBeenCalledTimes(2); - expect(api.getWebhookInfo).toHaveBeenCalledTimes(1); - expectRecoverableRetryState(1); - }); - - it("continues polling when deleteWebhook transiently fails but webhook is already absent", async () => { - const abort = new AbortController(); - const cleanupError = makeRecoverableFetchError(); - api.deleteWebhook.mockReset(); - api.getWebhookInfo.mockReset().mockResolvedValueOnce({ url: "" }); + api.getWebhookInfo.mockReset(); api.deleteWebhook.mockRejectedValueOnce(cleanupError); mockRunOnceAndAbort(abort); await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal }); expect(api.deleteWebhook).toHaveBeenCalledTimes(1); - expect(api.getWebhookInfo).toHaveBeenCalledTimes(1); - expect(runSpy).toHaveBeenCalledTimes(1); - expect(sleepWithAbort).not.toHaveBeenCalled(); + expect(api.getWebhookInfo).not.toHaveBeenCalled(); + expectRecoverableRetryState(1); }); - it("retries cleanup when deleteWebhook and webhook confirmation both transiently fail", async () => { + it("does not run webhook confirmation when deleteWebhook transiently fails", async () => { const abort = new AbortController(); const cleanupError = makeRecoverableFetchError(); api.deleteWebhook.mockReset(); - api.getWebhookInfo.mockReset().mockRejectedValueOnce(makeRecoverableFetchError()); - api.deleteWebhook.mockRejectedValueOnce(cleanupError).mockResolvedValueOnce(true); + api.getWebhookInfo.mockReset(); + api.deleteWebhook.mockRejectedValueOnce(cleanupError); mockRunOnceAndAbort(abort); await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal }); - expect(api.deleteWebhook).toHaveBeenCalledTimes(2); - expect(api.getWebhookInfo).toHaveBeenCalledTimes(1); - expectRecoverableRetryState(1); + expect(api.deleteWebhook).toHaveBeenCalledTimes(1); + expect(api.getWebhookInfo).not.toHaveBeenCalled(); + expect(runSpy).toHaveBeenCalledTimes(1); + expect(sleepWithAbort).not.toHaveBeenCalled(); }); it("retries setup-time recoverable errors before starting polling", async () => { diff --git a/extensions/telegram/src/monitor.ts b/extensions/telegram/src/monitor.ts index baeff5ed50a..bbf60d5faff 100644 --- a/extensions/telegram/src/monitor.ts +++ b/extensions/telegram/src/monitor.ts @@ -248,6 +248,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { accountId: account.accountId, runtime: opts.runtime, proxyFetch, + botInfo: opts.botInfo, abortSignal: opts.abortSignal, runnerOptions: createTelegramRunnerOptions(cfg), getLastUpdateId: () => lastUpdateId, diff --git a/extensions/telegram/src/monitor.types.ts b/extensions/telegram/src/monitor.types.ts index 198bae18f09..0d354465a23 100644 --- a/extensions/telegram/src/monitor.types.ts +++ b/extensions/telegram/src/monitor.types.ts @@ -4,6 +4,7 @@ import type { } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +import type { TelegramBotInfo } from "./bot-info.js"; export type MonitorTelegramOpts = { token?: string; @@ -20,6 +21,7 @@ export type MonitorTelegramOpts = { proxyFetch?: typeof fetch; webhookUrl?: string; webhookCertPath?: string; + botInfo?: TelegramBotInfo; setStatus?: (patch: Omit) => void; }; diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index e86923c5ac2..553fb750080 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -572,6 +572,30 @@ describe("TelegramPollingSession", () => { expect(createTelegramTransport).toHaveBeenCalledTimes(1); }); + it("starts polling when webhook cleanup times out during startup", async () => { + const abort = new AbortController(); + const cleanupError = new Error("Telegram deleteWebhook timed out after 15000ms"); + const bot = makeBot(); + bot.api.deleteWebhook.mockRejectedValueOnce(cleanupError); + createTelegramBotMock.mockReturnValueOnce(bot); + runMock.mockReturnValueOnce({ + task: async () => { + abort.abort(); + }, + stop: vi.fn(async () => undefined), + isRunning: () => false, + }); + + const session = createPollingSession({ + abortSignal: abort.signal, + }); + + await session.runUntilAbort(); + + expect(bot.api.deleteWebhook).toHaveBeenCalledTimes(1); + expect(runMock).toHaveBeenCalledTimes(1); + }); + it("does not trigger stall restart shortly after a getUpdates error", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); diff --git a/extensions/telegram/src/polling-session.ts b/extensions/telegram/src/polling-session.ts index e1f6ea7afa9..a1d37c7ab7c 100644 --- a/extensions/telegram/src/polling-session.ts +++ b/extensions/telegram/src/polling-session.ts @@ -67,6 +67,7 @@ type TelegramPollingSessionOpts = { accountId: string; runtime: Parameters[0]["runtime"]; proxyFetch: Parameters[0]["proxyFetch"]; + botInfo?: Parameters[0]["botInfo"]; abortSignal?: AbortSignal; runnerOptions: RunOptions; getLastUpdateId: () => number | null; @@ -187,6 +188,7 @@ export class TelegramPollingSession { proxyFetch: this.opts.proxyFetch, config: this.opts.config, accountId: this.opts.accountId, + botInfo: this.opts.botInfo, fetchAbortSignal: fetchAbortController.signal, minimumClientTimeoutSeconds: TELEGRAM_POLLING_CLIENT_TIMEOUT_FLOOR_SECONDS, updateOffset: { @@ -217,10 +219,9 @@ export class TelegramPollingSession { this.#webhookCleared = true; return "ready"; } catch (err) { - if (await this.#confirmWebhookAlreadyAbsent(bot, err)) { - this.#webhookCleared = true; + if (isRecoverableTelegramNetworkError(err, { context: "unknown" })) { this.opts.log( - "[telegram] deleteWebhook failed, but getWebhookInfo confirmed no webhook is set; continuing with polling.", + `[telegram] deleteWebhook failed with a recoverable network error; continuing to polling so getUpdates can confirm webhook state: ${formatErrorMessage(err)}`, ); return "ready"; } @@ -232,29 +233,6 @@ export class TelegramPollingSession { } } - async #confirmWebhookAlreadyAbsent( - bot: TelegramBot, - deleteWebhookError: unknown, - ): Promise { - if (!isRecoverableTelegramNetworkError(deleteWebhookError, { context: "unknown" })) { - return false; - } - try { - const webhookInfo = await withTelegramApiErrorLogging({ - operation: "getWebhookInfo", - runtime: this.opts.runtime, - shouldLog: (err) => !isRecoverableTelegramNetworkError(err, { context: "unknown" }), - fn: () => bot.api.getWebhookInfo(), - }); - return typeof webhookInfo?.url === "string" && webhookInfo.url.trim().length === 0; - } catch (err) { - if (!isRecoverableTelegramNetworkError(err, { context: "unknown" })) { - throw err; - } - return false; - } - } - async #runPollingCycle(bot: TelegramBot): Promise<"continue" | "exit"> { const liveness = new TelegramPollingLivenessTracker({ onPollSuccess: (finishedAt) => this.#status.notePollSuccess(finishedAt), diff --git a/extensions/telegram/src/probe.test.ts b/extensions/telegram/src/probe.test.ts index d19e08f3368..0415f7f1c95 100644 --- a/extensions/telegram/src/probe.test.ts +++ b/extensions/telegram/src/probe.test.ts @@ -33,7 +33,20 @@ describe("probeTelegram retry logic", () => { ok: true, json: vi.fn().mockResolvedValue({ ok: true, - result: { id: 123, username: "test_bot" }, + result: { + id: 123, + is_bot: true, + first_name: "Test", + username: "test_bot", + can_join_groups: true, + can_read_all_group_messages: false, + can_manage_bots: false, + supports_inline_queries: false, + can_connect_to_business: false, + has_main_web_app: false, + has_topics_enabled: false, + allows_users_to_create_topics: false, + }, }), }); } @@ -181,6 +194,14 @@ describe("probeTelegram retry logic", () => { expect(result.ok).toBe(true); expect(result.webhook).toBeUndefined(); + expect(result.botInfo).toEqual( + expect.objectContaining({ + id: 123, + is_bot: true, + first_name: "Test", + username: "test_bot", + }), + ); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.telegram.org/bottest-token/getMe"); }); diff --git a/extensions/telegram/src/probe.ts b/extensions/telegram/src/probe.ts index 444a2f67026..4516a45d4aa 100644 --- a/extensions/telegram/src/probe.ts +++ b/extensions/telegram/src/probe.ts @@ -2,6 +2,7 @@ import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-types"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { fetchWithTimeout } from "openclaw/plugin-sdk/text-runtime"; +import type { TelegramBotInfo } from "./bot-info.js"; import { resolveTelegramApiBase, resolveTelegramFetch } from "./fetch.js"; import { makeProxyFetch } from "./proxy.js"; @@ -10,11 +11,19 @@ export type TelegramProbe = BaseProbeResult & { elapsedMs: number; bot?: { id?: number | null; + isBot?: boolean | null; + firstName?: string | null; username?: string | null; canJoinGroups?: boolean | null; canReadAllGroupMessages?: boolean | null; + canManageBots?: boolean | null; supportsInlineQueries?: boolean | null; + canConnectToBusiness?: boolean | null; + hasMainWebApp?: boolean | null; + hasTopicsEnabled?: boolean | null; + allowsUsersToCreateTopics?: boolean | null; }; + botInfo?: TelegramBotInfo; webhook?: { url?: string | null; hasCustomCert?: boolean | null }; }; @@ -94,6 +103,41 @@ function resolveProbeFetcher(token: string, options?: TelegramProbeOptions): typ return resolved; } +function normalizeBoolean(value: unknown): boolean | null { + return typeof value === "boolean" ? value : null; +} + +function normalizeTelegramBotInfo(value: unknown): TelegramBotInfo | undefined { + if (!value || typeof value !== "object") { + return undefined; + } + const bot = value as Record; + if ( + typeof bot.id !== "number" || + bot.is_bot !== true || + typeof bot.first_name !== "string" || + typeof bot.username !== "string" + ) { + return undefined; + } + return { + id: bot.id, + is_bot: true, + first_name: bot.first_name, + username: bot.username, + ...(typeof bot.last_name === "string" ? { last_name: bot.last_name } : {}), + ...(typeof bot.language_code === "string" ? { language_code: bot.language_code } : {}), + can_join_groups: normalizeBoolean(bot.can_join_groups) ?? false, + can_read_all_group_messages: normalizeBoolean(bot.can_read_all_group_messages) ?? false, + can_manage_bots: normalizeBoolean(bot.can_manage_bots) ?? false, + supports_inline_queries: normalizeBoolean(bot.supports_inline_queries) ?? false, + can_connect_to_business: normalizeBoolean(bot.can_connect_to_business) ?? false, + has_main_web_app: normalizeBoolean(bot.has_main_web_app) ?? false, + has_topics_enabled: normalizeBoolean(bot.has_topics_enabled) ?? false, + allows_users_to_create_topics: normalizeBoolean(bot.allows_users_to_create_topics) ?? false, + }; +} + export async function probeTelegram( token: string, timeoutMs: number, @@ -157,13 +201,7 @@ export async function probeTelegram( const meJson = (await meRes.json()) as { ok?: boolean; description?: string; - result?: { - id?: number; - username?: string; - can_join_groups?: boolean; - can_read_all_group_messages?: boolean; - supports_inline_queries?: boolean; - }; + result?: unknown; }; if (!meRes.ok || !meJson?.ok) { result.status = meRes.status; @@ -171,19 +209,25 @@ export async function probeTelegram( return { ...result, elapsedMs: Date.now() - started }; } + const botInfo = normalizeTelegramBotInfo(meJson.result); + const rawBot = meJson.result && typeof meJson.result === "object" ? meJson.result : {}; + const bot = rawBot as Record; + if (botInfo) { + result.botInfo = botInfo; + } result.bot = { - id: meJson.result?.id ?? null, - username: meJson.result?.username ?? null, - canJoinGroups: - typeof meJson.result?.can_join_groups === "boolean" ? meJson.result?.can_join_groups : null, - canReadAllGroupMessages: - typeof meJson.result?.can_read_all_group_messages === "boolean" - ? meJson.result?.can_read_all_group_messages - : null, - supportsInlineQueries: - typeof meJson.result?.supports_inline_queries === "boolean" - ? meJson.result?.supports_inline_queries - : null, + id: typeof bot.id === "number" ? bot.id : null, + isBot: normalizeBoolean(bot.is_bot), + firstName: typeof bot.first_name === "string" ? bot.first_name : null, + username: typeof bot.username === "string" ? bot.username : null, + canJoinGroups: normalizeBoolean(bot.can_join_groups), + canReadAllGroupMessages: normalizeBoolean(bot.can_read_all_group_messages), + canManageBots: normalizeBoolean(bot.can_manage_bots), + supportsInlineQueries: normalizeBoolean(bot.supports_inline_queries), + canConnectToBusiness: normalizeBoolean(bot.can_connect_to_business), + hasMainWebApp: normalizeBoolean(bot.has_main_web_app), + hasTopicsEnabled: normalizeBoolean(bot.has_topics_enabled), + allowsUsersToCreateTopics: normalizeBoolean(bot.allows_users_to_create_topics), }; if (includeWebhookInfo) {