mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-13 02:14:55 +00:00
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.
63 lines
1.8 KiB
Go
63 lines
1.8 KiB
Go
package blacklist
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestScopePrefix_EndsAfterThreeColons(t *testing.T) {
|
|
got := scopePrefix(-1001234567890, 0, listBlack)
|
|
if want := "-1001234567890:0:b:"; got != want {
|
|
t.Fatalf("scopePrefix = %q, want %q", got, want)
|
|
}
|
|
if strings.Count(got, ":") != 3 {
|
|
t.Fatalf("prefix %q does not carry exactly three colons", got)
|
|
}
|
|
}
|
|
|
|
// Two threads of one chat must produce prefixes where neither is a prefix of
|
|
// the other, or a List for one thread would return the other's entries.
|
|
func TestScopePrefix_ThreadsDoNotNest(t *testing.T) {
|
|
a := scopePrefix(-100, 1, listBlack)
|
|
b := scopePrefix(-100, 12, listBlack)
|
|
if strings.HasPrefix(b, a) || strings.HasPrefix(a, b) {
|
|
t.Fatalf("thread prefixes nest: %q and %q", a, b)
|
|
}
|
|
}
|
|
|
|
func TestScopePrefix_ListsDoNotCollide(t *testing.T) {
|
|
if scopePrefix(-100, 0, listBlack) == scopePrefix(-100, 0, listWhite) {
|
|
t.Fatal("blacklist and whitelist share a prefix")
|
|
}
|
|
}
|
|
|
|
func TestEncodeKeyText_RoundTrips(t *testing.T) {
|
|
for _, in := range []string{
|
|
"plain",
|
|
"a/b",
|
|
"100%",
|
|
"%2F", // the literal text that a naive decoder turns into "/"
|
|
"%25", // likewise for "%"
|
|
"a:b", // the key delimiter, which needs no escaping
|
|
"a\nb",
|
|
"%2F/%25%", // every hazard at once
|
|
} {
|
|
enc := encodeKeyText(in)
|
|
if strings.Contains(enc, "/") {
|
|
t.Fatalf("encodeKeyText(%q) = %q still contains '/'", in, enc)
|
|
}
|
|
if got := decodeKeyText(enc); got != in {
|
|
t.Fatalf("round trip of %q gave %q (encoded %q)", in, got, enc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryKey_StaysWithinStorageLimits(t *testing.T) {
|
|
// Worst case: an entry at the cap made entirely of characters that encode
|
|
// to three bytes each.
|
|
key := entryKey(-1001234567890, 999999, listBlack, strings.Repeat("/", maxEntryBytes))
|
|
if len(key) > 1500 {
|
|
t.Fatalf("worst-case key is %d bytes, over the 1500-byte storage limit", len(key))
|
|
}
|
|
}
|