feat(commands): add /group to manage reply policy from chat - #5974
Open
CarmeloCampos wants to merge 2 commits into
Open
CarmeloCampos wants to merge 2 commits into
CarmeloCampos wants to merge 2 commits into
Conversation
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.
CarmeloCampos
force-pushed
the
feat/group-command
branch
from
September 29, 2026 20:34
df6fcf7 to
d01a1bb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
mainas soon as #5973 lands — the diff above is notwhat 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
mainthe change is exactly:
nanobot/group_policy/(store)nanobot/command/builtin.py(/group)nanobot/channels/telegram/runtime.py(resolve through the store)docs/chat-commands.mdtests/test_group_policy.pySummary
Editing
channels.<channel>.groupPolicyOverridesinconfig.jsonrequires 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
/groupcommand backed by a small runtime store, so an operator can adjust the current chat or topic from where they are.Refs #5972.
Command
The command always acts on the calling chat/topic (from
chat_idandmessage_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.jsonwhile/grouphandles exceptions. Most specific scope wins:config.json)config.json)groupPolicyScopes are keyed by channel as well (
telegram:<chat_id>[:<thread_id>]), so Telegram and Discord overrides cannot collide.Storage
nanobot/group_policy/mirrors the existingnanobot.pairing.storepattern 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):_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 howpairing/store.pyavoids 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—/grouphandler and palette entry.nanobot/channels/telegram/runtime.py— resolves throughresolve_policy();/groupadded 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:resetclears one scope and leaves the parent intactlistfilters by channel; overrides persist to diskNotes for Reviewers
render_as: "text"and only acts in group scopes; in a DM it explains that it applies to groups./groupwith no argument reports what the next message will actually get — including overrides set viaconfig.json.main(missing localrg/poppler), unrelated to this change.static_overrideslookup inresolve_policy()needs adjusting; the store and command stay as-is.