feat(blacklist): add per-topic text deny-list with whitelist exceptions

Six public commands let any member of a chat curate two lists of text and
ask whether a given text is blocked: /blacklist_add, /blacklist_del,
/whitelist_add, /whitelist_del, /blacklist_rules and /blacklist_check.

The module is passive. It never reads ordinary chat messages and never
deletes, warns or restricts anyone; the lists stay inert until
/blacklist_check asks about a specific text.

Scope is one forum topic, keyed (Chat.ID, MessageThreadID) and gated on
IsTopicMessage so a reply chain in a plain supergroup does not become its
own unreachable scope. A plain group, a DM and a forum's General topic all
resolve to one chat-wide list.

Matching is substring, after NFKC composition, case folding and whitespace
collapse. Diacritics stay significant, so ma, má and mà are three entries.
A whitelist entry rescues a blacklist match only when it spans that match,
which is what keeps "I met an assassin, dumbass" blocked.

Entry text is percent-encoded before it becomes a storage key, since keys
forbid '/' and cap at 1500 bytes, and is capped at 200 bytes before and
after normalization because NFKC can expand as well as contract.

/blacklist_rules reads each list with Scan, so listing costs one round trip
per list rather than a Get per entry.
This commit is contained in:
tiennm99 committed 2026-09-15 13:37:03 +07:00
1 parent d2272bd84d
commit e5125fcd93
20 files changed
+2161 -1

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Blacklist
The `blacklist` module lets a chat keep a list of forbidden text and a list of
exceptions, then ask whether a given text is blocked.
| Command | Parameters | What it does |
|---|---|---|
| `/blacklist_add` | `[text...]` | Adds text to the blacklist, or the message you replied to |
| `/blacklist_del` | `<text...>` | Removes text from the blacklist |
| `/whitelist_add` | `[text...]` | Adds an exception, or the message you replied to |
| `/whitelist_del` | `<text...>` | Removes an exception |
| `/blacklist_rules` | — | Lists both lists in one message |
| `/blacklist_check` | `<text...>` | Judges a text against both lists |
All are public and single-shot.
## The bot does not police the chat
This is the first thing to know, because the module's name promises something it
deliberately does not do. Nothing happens automatically. The bot never reads
ordinary messages, never deletes anything, and never warns or restricts anyone.
The lists sit inert until `/blacklist_check` asks about a specific text.
Automatic moderation would need three things this bot does not have: a
message-level hook in the dispatcher, privacy mode disabled in BotFather so
Telegram delivers ordinary group messages at all, and admin rights with
permission to delete in every group. All three are out of scope by choice.
## Lists belong to a topic, not to the bot
Each thread keeps its own two lists. In a forum supergroup, entries added in one
topic are invisible in the next. A non-forum group has one set of lists; a
private chat with the bot has its own, shared with nobody.
This is the most common surprise: running `/blacklist_check` in the wrong topic
gives a different answer than the same command one topic over, and it is not a
bug. Every reply says "in this topic" for that reason.
Within a thread, anyone can add and anyone can remove — including entries
someone else added. The lists belong to the conversation, so the permission to
edit them does too. A per-owner rule would strand entries whose author has left
the group.
## How the whitelist works
The whitelist is not a second independent list. It is an exception layer over
the blacklist, and it rescues a match only when it **covers** that match in the
text being checked.
With `ass` blacklisted and `assassin` whitelisted:
| Checked text | Verdict | Why |
|---|---|---|
| `assassin` | Allowed | The whitelist entry spans the whole match |
| `dumbass` | Blacklisted | Nothing whitelisted covers this `ass` |
| `I met an assassin, dumbass` | **Blacklisted** | The first match is rescued; the second is not |
The third row is the one worth remembering. A whitelist entry does not make a
whole message safe — it only rescues the occurrences it actually contains.
## Matching
Matching is by substring, after the text is normalized:
- **Case does not matter.** `Cat`, `CAT` and `cat` are one rule.
- **Spacing does not matter.** `cat dog` and `cat dog` are one rule, and a
newline is just a space.
- **Diacritics do matter.** `ma`, `má` and `mà` are three separate entries. To
catch all three, add all three.
- **The same word matches across devices.** Vietnamese typed on an iPhone and on
an Android phone can differ in how the accents are encoded; normalization
resolves that, so the two forms match each other.
- **Full-width and other presentation variants fold** onto their plain forms.
`/blacklist_check` accepts text of any length. Entries themselves are capped at
200 bytes — roughly 200 plain letters, or about 65 Vietnamese characters.
## Listing
`/blacklist_rules` prints both lists in one message, each with its entry count,
showing entries as they were typed rather than in the normalized form. Each
entry is tappable to copy, ready to paste into a `_del` command.
Telegram caps a message at 4096 characters. A list longer than that is trimmed
with a count of what was left out; both headings always appear, so a long
blacklist never hides the whitelist entirely.