mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:30:44 +00:00
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 headc74bbdd1ff. - Required merge gates passed before the squash merge. Prepared head SHA:c74bbdd1ffReview: https://github.com/openclaw/openclaw/pull/76735#issuecomment-4366417178 Co-authored-by: Ayaan Zaidi <hi@obviy.us> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.<id>.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`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
16
extensions/telegram/src/bot-info.ts
Normal file
16
extensions/telegram/src/bot-info.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ChannelAccountSnapshot, "accountId">) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -67,6 +67,7 @@ type TelegramPollingSessionOpts = {
|
||||
accountId: string;
|
||||
runtime: Parameters<typeof createTelegramBot>[0]["runtime"];
|
||||
proxyFetch: Parameters<typeof createTelegramBot>[0]["proxyFetch"];
|
||||
botInfo?: Parameters<typeof createTelegramBot>[0]["botInfo"];
|
||||
abortSignal?: AbortSignal;
|
||||
runnerOptions: RunOptions<unknown>;
|
||||
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<boolean> {
|
||||
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),
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user