Files
tiennm99 e5125fcd93 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.
2026-09-15 13:37:03 +07:00

32 lines
1.2 KiB
Go

package blacklist
import (
"strings"
"golang.org/x/text/unicode/norm"
)
// Normalize converts user text to the single form used both as a storage key
// and as matcher input. It returns ok=false when nothing comparable is left.
//
// The three steps, in this order:
//
// 1. NFKC. Canonical composition is what makes Vietnamese portable: some
// clients send "má" as "m" + "a" + combining acute (NFD) while others send
// the precomposed character (NFC), and without this they are different
// strings that would never match each other. The compatibility half also
// folds full-width and other presentation variants onto their plain forms.
// It does not strip combining marks — "ma" and "má" stay distinct entries,
// which is the behaviour this module wants.
// 2. Lowercase, applied after normalization because case folding can otherwise
// interact with composition.
// 3. Whitespace collapse, so "cat dog" and "cat dog" are one rule and a
// newline is just a separator.
func Normalize(s string) (string, bool) {
fields := strings.Fields(strings.ToLower(norm.NFKC.String(s)))
if len(fields) == 0 {
return "", false
}
return strings.Join(fields, " "), true
}