diff --git a/CHANGELOG.md b/CHANGELOG.md index 6baba0492..0f69e1363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Telegram: remove last `@ts-nocheck` from `bot-handlers.ts`, use Grammy types directly, deduplicate `StickerMetadata`. Zero `@ts-nocheck` remaining in `src/telegram/`. (#9206) - Telegram: remove `@ts-nocheck` from `bot-message.ts`, type deps via `Omit`, widen `allMedia` to `TelegramMediaRef[]`. (#9180) - Telegram: remove `@ts-nocheck` from `bot.ts`, fix duplicate `bot.catch` error handler (Grammy overrides), remove dead reaction `message_thread_id` routing, harden sticker cache guard. (#9077) +- Telegram: allow per-group and per-topic `groupPolicy` overrides under `channels.telegram.groups`. (#9775) Thanks @nicolasstanley. - Feishu: expand channel handling (posts with images, doc links, routing, reactions/typing, replies, native commands). (#8975) Thanks @jiulingyun. - Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan. - Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 45f6d30f4..655749d87 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -392,6 +392,23 @@ Two independent controls: Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `channels.telegram.groups` +To allow **any group member** to talk in a specific group (while still keeping control commands restricted to authorized senders), set a per-group override: + +```json5 +{ + channels: { + telegram: { + groups: { + "-1001234567890": { + groupPolicy: "open", + requireMention: false, + }, + }, + }, + }, +} +``` + ## Long-polling vs webhook - Default: long-polling (no public URL required). @@ -714,12 +731,14 @@ Provider options: - `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.telegram.groupAllowFrom`: group sender allowlist (ids/usernames). - `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). + - `channels.telegram.groups..groupPolicy`: per-group override for groupPolicy (`open | allowlist | disabled`). - `channels.telegram.groups..requireMention`: mention gating default. - `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none). - `channels.telegram.groups..allowFrom`: per-group sender allowlist override. - `channels.telegram.groups..systemPrompt`: extra system prompt for the group. - `channels.telegram.groups..enabled`: disable the group when `false`. - `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group). + - `channels.telegram.groups..topics..groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`). - `channels.telegram.groups..topics..requireMention`: per-topic mention gating override. - `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist). - `channels.telegram.accounts..capabilities.inlineButtons`: per-account override. diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 1f5c0972e..fcad3154e 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -142,6 +142,8 @@ export type TelegramAccountConfig = { export type TelegramTopicConfig = { requireMention?: boolean; + /** Per-topic override for group message policy (open|disabled|allowlist). */ + groupPolicy?: GroupPolicy; /** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */ skills?: string[]; /** If false, disable the bot for this topic. */ @@ -154,6 +156,8 @@ export type TelegramTopicConfig = { export type TelegramGroupConfig = { requireMention?: boolean; + /** Per-group override for group message policy (open|disabled|allowlist). */ + groupPolicy?: GroupPolicy; /** Optional tool policy overrides for this group. */ tools?: GroupToolPolicyConfig; toolsBySender?: GroupToolPolicyBySenderConfig; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index c0ffc4858..8dc2bff6a 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -37,6 +37,7 @@ const TelegramCapabilitiesSchema = z.union([ export const TelegramTopicSchema = z .object({ requireMention: z.boolean().optional(), + groupPolicy: GroupPolicySchema.optional(), skills: z.array(z.string()).optional(), enabled: z.boolean().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), @@ -47,6 +48,7 @@ export const TelegramTopicSchema = z export const TelegramGroupSchema = z .object({ requireMention: z.boolean().optional(), + groupPolicy: GroupPolicySchema.optional(), tools: ToolPolicySchema, toolsBySender: ToolPolicyBySenderSchema, skills: z.array(z.string()).optional(), diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 6aac69687..58e1a7aca 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -363,7 +363,13 @@ export const registerTelegramHandlers = ({ } } const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open"; + const groupPolicy = firstDefined( + topicConfig?.groupPolicy, + groupConfig?.groupPolicy, + telegramCfg.groupPolicy, + defaultGroupPolicy, + "open", + ); if (groupPolicy === "disabled") { logVerbose(`Blocked telegram group message (groupPolicy: disabled)`); return; @@ -719,7 +725,13 @@ export const registerTelegramHandlers = ({ // - "disabled": block all group messages entirely // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open"; + const groupPolicy = firstDefined( + topicConfig?.groupPolicy, + groupConfig?.groupPolicy, + telegramCfg.groupPolicy, + defaultGroupPolicy, + "open", + ); if (groupPolicy === "disabled") { logVerbose(`Blocked telegram group message (groupPolicy: disabled)`); return; diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 3602f5ea7..b67bb3f08 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -2013,6 +2013,78 @@ describe("createTelegramBot", () => { expect(replySpy).not.toHaveBeenCalled(); }); + it("allows group messages for per-group groupPolicy open override (global groupPolicy allowlist)", async () => { + onSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType; + replySpy.mockReset(); + loadConfig.mockReturnValue({ + channels: { + telegram: { + groupPolicy: "allowlist", + groups: { + "-100123456789": { + groupPolicy: "open", + requireMention: false, + }, + }, + }, + }, + }); + readChannelAllowFromStore.mockResolvedValueOnce(["123456789"]); + + createTelegramBot({ token: "tok" }); + const handler = getOnHandler("message") as (ctx: Record) => Promise; + + await handler({ + message: { + chat: { id: -100123456789, type: "group", title: "Test Group" }, + from: { id: 999999, username: "random" }, + text: "hello", + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).toHaveBeenCalledTimes(1); + }); + + it("blocks control commands from unauthorized senders in per-group open groups", async () => { + onSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType; + replySpy.mockReset(); + loadConfig.mockReturnValue({ + channels: { + telegram: { + groupPolicy: "allowlist", + groups: { + "-100123456789": { + groupPolicy: "open", + requireMention: false, + }, + }, + }, + }, + }); + readChannelAllowFromStore.mockResolvedValueOnce(["123456789"]); + + createTelegramBot({ token: "tok" }); + const handler = getOnHandler("message") as (ctx: Record) => Promise; + + await handler({ + message: { + chat: { id: -100123456789, type: "group", title: "Test Group" }, + from: { id: 999999, username: "random" }, + text: "/status", + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).not.toHaveBeenCalled(); + }); + it("allows control commands with TG-prefixed groupAllowFrom entries", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType;