diff --git a/CHANGELOG.md b/CHANGELOG.md index 3273c291c83..c6e22d344bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai - fix(gateway): honor minimal discovery mode for wide-area DNS-SD [AI]. (#80903) Thanks @pgondhi987. - slack: enforce reaction notification policy [AI]. (#80907) Thanks @pgondhi987. - Enforce gateway command scopes by caller context [AI]. (#80891) Thanks @pgondhi987. +- Telegram/groups: in single-account setups, treat an explicit empty `accounts..groups: {}` map the same as undefined so the root `channels.telegram.groups` allowlist still applies, instead of silently dropping every group update under the default `groupPolicy: "allowlist"`. Multi-account semantics are unchanged so per-account explicit-empty groups still scope-disable a single account without affecting siblings; the explicit way to block all groups for any account remains `groupPolicy: "disabled"`. Fixes #79427. Thanks @nikolaykazakovvs-ux. - Enforce Slack plugin approval button authorization [AI]. (#80899) Thanks @pgondhi987. - Recognize PowerShell -ec inline commands [AI]. (#80893) Thanks @pgondhi987. - fix(qqbot): authorize approval button callbacks [AI]. (#80892) Thanks @pgondhi987. diff --git a/extensions/telegram/src/account-config.ts b/extensions/telegram/src/account-config.ts index 2652e8c0b91..76fb538c8ad 100644 --- a/extensions/telegram/src/account-config.ts +++ b/extensions/telegram/src/account-config.ts @@ -68,9 +68,19 @@ export function mergeTelegramAccountConfig( const account = resolveTelegramAccountConfig(cfg, accountId) ?? {}; // Multi-account bots must not inherit channel-level groups unless explicitly set. + // Single-account bots fall back to root `channels.telegram.groups` when the + // account does not declare its own groups — including the empty-literal case + // `accounts..groups: {}`, which is almost always a config-migration + // artifact rather than an intentional "block all" declaration (use + // `groupPolicy: "disabled"` for that). const configuredAccountIds = Object.keys(cfg.channels?.telegram?.accounts ?? {}); const isMultiAccount = configuredAccountIds.length > 1; - const groups = account.groups ?? (isMultiAccount ? undefined : channelGroups); + const hasAccountGroups = account.groups && Object.keys(account.groups).length > 0; + const groups = isMultiAccount + ? account.groups + : hasAccountGroups + ? account.groups + : channelGroups; const allowFrom = resolveMergedAllowFrom({ baseAllowFrom: base.allowFrom, accountAllowFrom: account.allowFrom, diff --git a/extensions/telegram/src/accounts.test.ts b/extensions/telegram/src/accounts.test.ts index a0cea0c6408..b0dc49d8eed 100644 --- a/extensions/telegram/src/accounts.test.ts +++ b/extensions/telegram/src/accounts.test.ts @@ -536,6 +536,24 @@ describe("resolveTelegramAccount groups inheritance (#30673)", () => { expect(resolved.config.groups).toEqual({ "-100123": { requireMention: false } }); }); + it("inherits channel-level groups when single-account explicitly sets `groups: {}` (regression: #79427)", () => { + const resolved = resolveTelegramAccount({ + cfg: { + channels: { + telegram: { + groups: { "-100123": { requireMention: false } }, + accounts: { + default: { botToken: "123:default", groups: {} }, + }, + }, + }, + }, + accountId: "default", + }); + + expect(resolved.config.groups).toEqual({ "-100123": { requireMention: false } }); + }); + it("does NOT inherit channel-level groups to secondary account in multi-account setup", () => { const resolved = resolveTelegramAccount({ cfg: createMultiAccountGroupsConfig(), diff --git a/src/config/group-policy.test.ts b/src/config/group-policy.test.ts index be09a155655..9ab39a87f36 100644 --- a/src/config/group-policy.test.ts +++ b/src/config/group-policy.test.ts @@ -169,6 +169,68 @@ describe("resolveChannelGroupPolicy", () => { }), ).toBe(false); }); + + it("falls back to root channel groups when account.groups is an empty object (regression: #79427)", () => { + const cfg = { + channels: { + telegram: { + groupPolicy: "allowlist", + groups: { + "-100123": { requireMention: false }, + }, + accounts: { + default: { botToken: "123:default", groups: {} }, + }, + }, + }, + } as OpenClawConfig; + + const policy = resolveChannelGroupPolicy({ + cfg, + channel: "telegram", + groupId: "-100123", + accountId: "default", + }); + + expect(policy.allowlistEnabled).toBe(true); + expect(policy.allowed).toBe(true); + }); + + it("uses populated account.groups instead of root when both are configured", () => { + const cfg = { + channels: { + telegram: { + groupPolicy: "allowlist", + groups: { + "-100root": { requireMention: false }, + }, + accounts: { + default: { + botToken: "123:default", + groups: { "-100account": { requireMention: false } }, + }, + }, + }, + }, + } as OpenClawConfig; + + expect( + resolveChannelGroupPolicy({ + cfg, + channel: "telegram", + groupId: "-100account", + accountId: "default", + }).allowed, + ).toBe(true); + expect( + resolveChannelGroupPolicy({ + cfg, + channel: "telegram", + groupId: "-100root", + accountId: "default", + }).allowed, + ).toBe(false); + }); }); describe("resolveToolsBySender", () => { diff --git a/src/config/group-policy.ts b/src/config/group-policy.ts index 9cdd09f26f5..b032f54d36b 100644 --- a/src/config/group-policy.ts +++ b/src/config/group-policy.ts @@ -340,6 +340,21 @@ function resolveChannelGroups( return undefined; } const accountGroups = resolveAccountEntry(channelConfig.accounts, normalizedAccountId)?.groups; + // In a single-account setup, treat an explicit empty account groups map + // (`accounts..groups: {}`) the same as undefined for fallback: the empty + // literal is almost always a config-migration artifact, not an intentional + // "block all groups" declaration — the explicit way to block is + // `groupPolicy: "disabled"` (or omitting the group from a populated + // allowlist). Without this, an empty `{}` paired with the default + // `groupPolicy: "allowlist"` silently denies every group update even though + // root `channels..groups` is populated. Multi-account contexts keep + // the existing semantics so per-account explicit-empty groups still scope + // disable a single account without affecting siblings. + const isMultiAccount = Object.keys(channelConfig.accounts ?? {}).length > 1; + if (!isMultiAccount) { + const hasAccountGroups = accountGroups && Object.keys(accountGroups).length > 0; + return hasAccountGroups ? accountGroups : channelConfig.groups; + } return accountGroups ?? channelConfig.groups; }