Skip to content

Fix #102: confine the bot to the chosen topics of a forum - #150

Merged
kozalosev merged 2 commits into
mainfrom
feature/topic-restriction
Aug 3, 2026
Merged

Fix #102: confine the bot to the chosen topics of a forum#150
kozalosev merged 2 commits into
mainfrom
feature/topic-restriction

Conversation

@kozalosev

Copy link
Copy Markdown
Owner

Closes #102.

/topics opens an admin-only picker in a forum group. It shows whether the bot works in the topic it was invoked in, how many topics it is confined to overall, and offers the one action that makes sense there:

State Buttons
Works everywhere (default) 🔒 Work in this topic only
Restricted, this topic allowed 🚫 Forbid in this topic · 🌍 Allow in all topics
Restricted, this topic forbidden ✅ Allow in this topic · 🌍 Allow in all topics

Forbidding the last allowed topic goes back to "works everywhere" rather than leaving the bot with nowhere to speak.

A command sent to a forbidden topic gets a self-destructing notice; a button press there gets an alert.

Design notes

No migration. The allowlist is an id-keyed set in the existing Chats.settings jsonb, next to the chat language. An object rather than an array because it merges and deletes in one statement and dedupes by key for free — note that jsonb - integer deletes an array element by index, not by value. Every write is a single statement, so two admins tapping at once can't clobber each other; reads go through a TTL cache (CHAT_TOPICS_CACHE_TIME_SECS) that the writes refresh.

Topics carry no names. The Bot API can't be asked for one, and a list of #42 labels says less than a count — so nothing is stored or shown for them, and each topic is managed from inside itself.

Gate placement is load-bearing. It sits at the very top of the dispatcher, above even the ban gate, which is safe because it writes nothing for the sender — so it covers every command, not just the game ones. Its callback twin covers the buttons: a keyboard outlives the message it came with, so without it the game could still be played from a forbidden topic by tapping an older message. /topics is registered above the gate — that placement is the whole exemption and needs no special case inside it, and a test pins the ordering down.

Only our commands are answered for. Matched against the same bot_commands() the menu is built from, plus the @username Telegram appends. A group usually holds several bots, and answering for another one's command would be exactly the noise this feature exists to remove. The gate fails open on a database error — an unreachable database must not lock a chat out.

Bonus fix. The daily-shrink broadcast now names the topic outright, which also fixes a failure that predates this: a forum whose General topic is closed refuses a message sent without one.

Out of scope

Inline mode (#76): an inline query carries no thread, and a message's topic isn't derivable from its own id — the inline_message_id decoded in tghack.rs holds only (dc_id, peer, message_id, access_hash).

Testing

152 tests pass, clippy clean. New coverage: the jsonb round-trips (including that the topics and the chat language never wipe each other, asserted after every write), the gate predicates for messages and callbacks, and the picker's button set per state — asserted on payloads rather than labels, so it doesn't depend on the locale.

Manually checked in a real forum group.

Deployment

CHAT_TOPICS_CACHE_TIME_SECS is new and optional (3600 by default) — it needs the environment: line in the server-configs docker-compose.yml, but no .env.sops entry since the default is fine. The Grafana dashboard should also learn about command_topics_usage_total, chat_topics_get_total and topic_restricted_total.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LiWvKdbmK4148oZJ4DvrGC

kozalosev and others added 2 commits August 3, 2026 10:03
`/topics` opens an admin-only picker that allows or forbids the topic it was
invoked in, or lifts the restriction altogether. A chat that never touched the
setting works everywhere, and forbidding the last allowed topic goes back to
that rather than leaving the bot with nowhere to speak.

The allowlist is an id-keyed set in the existing `Chats.settings` jsonb, next to
the chat language, so there is no migration. An object rather than an array
because it merges and deletes in one statement and dedupes by key for free --
`jsonb - integer` would delete an array element by index, not by value. Every
write is a single statement, so two admins tapping at once cannot clobber each
other, and reads go through a TTL cache the writes refresh.

Topics carry no names: the Bot API cannot be asked for one, and a list of `#42`
labels says less than a count. So the picker speaks only about the topic it was
opened in, plus how many topics the bot is confined to overall, and each topic
is managed from inside itself.

The gate sits at the very top of the dispatcher -- above even the ban gate,
which is safe because it writes nothing for the sender -- so it covers every
command rather than the game ones only. Its callback twin covers the buttons: a
keyboard outlives the message it came with, so without it the game could still
be played from a forbidden topic by tapping an older message. `/topics` is
registered above the gate, which is the whole exemption and needs no special
case inside it.

The gate answers only for commands that are ours, matched against the same
`bot_commands()` the menu is built from and against the `@username` Telegram
appends: a group usually holds several bots, and answering for another one's
command would be exactly the noise this feature exists to remove. It fails open
on a database error -- an unreachable database must not lock a chat out.

The daily-shrink broadcast now names the topic outright, which also fixes a
failure that predates this: a forum whose General topic is closed refuses a
message sent without one.

Inline mode is out of scope (#76): an inline query carries no thread, and a
message's topic is not derivable from its own id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LiWvKdbmK4148oZJ4DvrGC
@kozalosev kozalosev added the enhancement New feature or request label Aug 3, 2026
@kozalosev kozalosev moved this to In Progress in DickGrowerBot Aug 3, 2026
@kozalosev
kozalosev merged commit 741c49b into main Aug 3, 2026
2 checks passed
@kozalosev
kozalosev deleted the feature/topic-restriction branch August 3, 2026 08:36
@github-project-automation github-project-automation Bot moved this from In Progress to Done in DickGrowerBot Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Support enforcing of the bot use in a single chat topic only

1 participant