Skip to content

feat(commands): add /group to manage reply policy from chat - #5974

Open
CarmeloCampos wants to merge 2 commits into
HKUDS:mainfrom
CarmeloCampos:feat/group-command
Open

CarmeloCampos wants to merge 2 commits into
HKUDS:mainfrom
CarmeloCampos:feat/group-command

Conversation

@CarmeloCampos

Copy link
Copy Markdown
Contributor

⚠️ Depends on #5973

Review and merge #5973 first. This branch is stacked on it, so until #5973
lands this PR's diff also contains #5973's commits.

Once #5973 is merged, this PR reduces to just the command, the store, and their
tests. I will rebase onto main as soon as #5973 lands — the diff above is not
what merges.

Note on mechanics: this repo does not have GitHub's native stacked-PR feature
enabled, and GitHub rejects a cross-fork base branch, so the stack is expressed
through this PR-to-PR dependency rather than a stacked base. Rebased on main
the change is exactly:

  • nanobot/group_policy/ (store)
  • nanobot/command/builtin.py (/group)
  • nanobot/channels/telegram/runtime.py (resolve through the store)
  • docs/chat-commands.md
  • tests/test_group_policy.py

Summary

Editing channels.<channel>.groupPolicyOverrides in config.json requires touching JSON and restarting the gateway. For the common case — a topic is noisy, quiet it now — that is the wrong tool: the person seeing the noise is in the chat, and a restart drops context.

Add a /group command backed by a small runtime store, so an operator can adjust the current chat or topic from where they are.

Refs #5972.

Command

/group              → show the effective policy for this chat/topic
/group mention      → only reply here when mentioned
/group open         → reply to every message here
/group reset        → clear the override for this chat/topic

The command always acts on the calling chat/topic (from chat_id and message_thread_id), so a user never has to know or type a raw thread id.

How it layers with config

Runtime overrides supplement the static config rather than replace it, so durable policy can stay in config.json while /group handles exceptions. Most specific scope wins:

  1. runtime topic override
  2. runtime chat override
  3. static topic override (config.json)
  4. static chat override (config.json)
  5. channel-wide groupPolicy

Scopes are keyed by channel as well (telegram:<chat_id>[:<thread_id>]), so Telegram and Discord overrides cannot collide.

Storage

nanobot/group_policy/ mirrors the existing nanobot.pairing.store pattern deliberately — JSON file under the data dir, process lock, atomic writes, no external database — because it is the same shape of problem (small per-scope settings, mutated from chat, read on every inbound message):

<data_dir>/group_policy.json
{"overrides": {"telegram:-100:7819": "mention"}}

_load() validates on read so a corrupted file degrades to "no overrides" instead of raising on every message, while a transiently unreadable file propagates — mirroring how pairing/store.py avoids persisting an empty view over real data.

Changes

  • nanobot/group_policy/ — store + resolve_policy() used by both the command and the Telegram channel.
  • nanobot/command/builtin.py — /group handler and palette entry.
  • nanobot/channels/telegram/runtime.py — resolves through resolve_policy(); /group added to the forwarded command list.
  • docs/chat-commands.md — command table.

Tests

New tests/test_group_policy.py (13 tests), using an isolated temp data dir:

  • default policy when nothing is set
  • static chat override; static topic beats static chat
  • runtime override beats static config
  • runtime topic beats runtime chat, and siblings keep the chat value
  • no leakage across chats (same thread id) or across channels
  • reset clears one scope and leaves the parent intact
  • unsupported policy rejected; list filters by channel; overrides persist to disk
$ pytest tests/test_group_policy.py tests/channels/ -q
309 passed

Notes for Reviewers

  • The command replies with render_as: "text" and only acts in group scopes; in a DM it explains that it applies to groups.
  • The effective-policy lookup reads the same values the channel uses, so /group with no argument reports what the next message will actually get — including overrides set via config.json.
  • Full suite has the same 4 pre-existing environment failures as main (missing local rg/poppler), unrelated to this change.
  • If feat(telegram): per-chat and per-topic group policy overrides #5973 is rejected in favor of a different key format, only the static_overrides lookup in resolve_policy() needs adjusting; the store and command stay as-is.

One Telegram bot can serve several chats and forum topics, but groupPolicy is
channel-wide. A single busy supergroup therefore forces one behavior
everywhere: either the bot answers every message, or it only answers when
mentioned, in every chat and topic at once.

Add groupPolicyOverrides so each scope can differ. Keys are "<chat_id>" for a
whole chat or "<chat_id>:<thread_id>" for one forum topic; the most specific
match wins and unlisted scopes fall back to groupPolicy. Topic keys are
chat_id-qualified because Telegram forum topics have no chat id of their own:
they share the parent supergroup's chat_id and differ only by
message_thread_id.

This mirrors the per-group groupPolicyOverrides Napcat already exposes, adapted
to Telegram's topic model. With the channel-wide policy left at "open", one
announcement topic can be silenced with "mention" while the rest of the chat
stays open.

Refs HKUDS#5971
Editing channels.<channel>.groupPolicyOverrides in config.json requires
touching JSON and restarting the gateway. That is a poor fit for the common
case: a topic gets noisy, and someone wants it quiet right now, from the chat
where the noise is happening.

Add a /group command backed by a small runtime store
(`nanobot/group_policy`, mirroring nanobot.pairing.store: JSON file, process
lock, atomic writes). It resolves the effective policy with the most specific
scope winning:

1. runtime topic override
2. runtime chat override
3. static topic override (config.json)
4. static chat override (config.json)
5. channel-wide groupPolicy

Runtime overrides therefore layer on top of config instead of replacing it, so
an operator can keep the durable policy in config.json and use /group for
exceptions. Scopes are keyed by channel as well, so Telegram and Discord
overrides cannot collide.

Telegram resolves policy through this store, and its chat commands list now
forwards /group.
@chengyongru chengyongru added channel documentation Improvements or additions to documentation feature priority: p2 Normal backlog: minor bug, enhancement, docs, cleanup, edge case, or unvalidated proposal. test labels Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel conflict documentation Improvements or additions to documentation feature priority: p2 Normal backlog: minor bug, enhancement, docs, cleanup, edge case, or unvalidated proposal. test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants