diff --git a/README.md b/README.md index 205707e4052..10078d1d400 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker ### [Telegram](https://docs.openclaw.ai/channels/telegram) - Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` (env wins). -- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` as needed. +- Optional: set `channels.telegram.groups` (with `channels.telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `channels.telegram.allowFrom` or `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` as needed. ```json5 { diff --git a/docs/channels/grammy.md b/docs/channels/grammy.md index 2deb19df20c..1b73394ef7e 100644 --- a/docs/channels/grammy.md +++ b/docs/channels/grammy.md @@ -18,7 +18,7 @@ title: grammY - **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default. - **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`. - **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`. -- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` is set (otherwise it long-polls). +- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls). - **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; replies route back to the same channel. - **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`. - **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index aa87eb85773..1d2fef69715 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -395,7 +395,7 @@ Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups ## Long-polling vs webhook - Default: long-polling (no public URL required). -- Webhook mode: set `channels.telegram.webhookUrl` (optionally `channels.telegram.webhookSecret` + `channels.telegram.webhookPath`). +- Webhook mode: set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret` (optionally `channels.telegram.webhookPath`). - The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default. - If your public URL is different, use a reverse proxy and point `channels.telegram.webhookUrl` at the public endpoint. @@ -732,8 +732,8 @@ Provider options: - `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts. - `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP). -- `channels.telegram.webhookUrl`: enable webhook mode. -- `channels.telegram.webhookSecret`: webhook secret (optional). +- `channels.telegram.webhookUrl`: enable webhook mode (requires `channels.telegram.webhookSecret`). +- `channels.telegram.webhookSecret`: webhook secret (required when webhookUrl is set). - `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`). - `channels.telegram.actions.reactions`: gate Telegram tool reactions. - `channels.telegram.actions.sendMessage`: gate Telegram tool message sends. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 05bafc275a4..faf19a98c49 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -1091,7 +1091,7 @@ Set `channels.telegram.configWrites: false` to block Telegram-initiated config w autoSelectFamily: false, }, proxy: "socks5://localhost:9050", - webhookUrl: "https://example.com/telegram-webhook", + webhookUrl: "https://example.com/telegram-webhook", // requires webhookSecret webhookSecret: "secret", webhookPath: "/telegram-webhook", }, diff --git a/src/config/telegram-webhook-secret.test.ts b/src/config/telegram-webhook-secret.test.ts new file mode 100644 index 00000000000..dce093ae806 --- /dev/null +++ b/src/config/telegram-webhook-secret.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; + +import { validateConfigObject } from "./config.js"; + +describe("Telegram webhook config", () => { + it("accepts webhookUrl when webhookSecret is configured", () => { + const res = validateConfigObject({ + channels: { + telegram: { + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + }, + }, + }); + expect(res.ok).toBe(true); + }); + + it("rejects webhookUrl without webhookSecret", () => { + const res = validateConfigObject({ + channels: { + telegram: { + webhookUrl: "https://example.com/telegram-webhook", + }, + }, + }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues[0]?.path).toBe("channels.telegram.webhookSecret"); + } + }); + + it("accepts account webhookUrl when base webhookSecret is configured", () => { + const res = validateConfigObject({ + channels: { + telegram: { + webhookSecret: "secret", + accounts: { + ops: { + webhookUrl: "https://example.com/telegram-webhook", + }, + }, + }, + }, + }); + expect(res.ok).toBe(true); + }); + + it("rejects account webhookUrl without webhookSecret", () => { + const res = validateConfigObject({ + channels: { + telegram: { + accounts: { + ops: { + webhookUrl: "https://example.com/telegram-webhook", + }, + }, + }, + }, + }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues[0]?.path).toBe("channels.telegram.accounts.ops.webhookSecret"); + } + }); +}); diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index b852cdfd312..3d99a26fb7e 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -164,6 +164,43 @@ export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({ 'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"', }); validateTelegramCustomCommands(value, ctx); + + const baseWebhookUrl = typeof value.webhookUrl === "string" ? value.webhookUrl.trim() : ""; + const baseWebhookSecret = + typeof value.webhookSecret === "string" ? value.webhookSecret.trim() : ""; + if (baseWebhookUrl && !baseWebhookSecret) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "channels.telegram.webhookUrl requires channels.telegram.webhookSecret", + path: ["webhookSecret"], + }); + } + if (!value.accounts) { + return; + } + for (const [accountId, account] of Object.entries(value.accounts)) { + if (!account) { + continue; + } + if (account.enabled === false) { + continue; + } + const accountWebhookUrl = + typeof account.webhookUrl === "string" ? account.webhookUrl.trim() : ""; + if (!accountWebhookUrl) { + continue; + } + const accountSecret = + typeof account.webhookSecret === "string" ? account.webhookSecret.trim() : ""; + if (!accountSecret && !baseWebhookSecret) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "channels.telegram.accounts.*.webhookUrl requires channels.telegram.webhookSecret or channels.telegram.accounts.*.webhookSecret", + path: ["accounts", accountId, "webhookSecret"], + }); + } + } }); export const DiscordDmSchema = z