mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-14 22:17:43 +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.
85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
package blacklist
|
|
|
|
import "testing"
|
|
|
|
// The case the whole containment rule exists for. A whitelist entry rescues
|
|
// only the match it spans, so a text carrying both a rescued and an unrescued
|
|
// occurrence is still blocked.
|
|
func TestCheck_WhitelistRescuesOnlyWhatItSpans(t *testing.T) {
|
|
black := []string{"ass"}
|
|
white := []string{"assassin"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
blocked bool
|
|
entry string
|
|
rescuedBy string
|
|
}{
|
|
{name: "rescued", text: "assassin", blocked: false, entry: "ass", rescuedBy: "assassin"},
|
|
{name: "not rescued", text: "dumbass", blocked: true, entry: "ass"},
|
|
{name: "one of each", text: "i met an assassin, dumbass", blocked: true, entry: "ass"},
|
|
{name: "no match at all", text: "hello there"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := Check(tc.text, black, white)
|
|
if got.Blocked != tc.blocked || got.Entry != tc.entry || got.RescuedBy != tc.rescuedBy {
|
|
t.Fatalf("Check(%q) = %+v; want blocked=%v entry=%q rescuedBy=%q",
|
|
tc.text, got, tc.blocked, tc.entry, tc.rescuedBy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheck_WithoutWhitelistNothingIsRescued(t *testing.T) {
|
|
if got := Check("assassin", []string{"ass"}, nil); !got.Blocked {
|
|
t.Fatalf("Check = %+v; want blocked", got)
|
|
}
|
|
}
|
|
|
|
func TestCheck_IdenticalWhitelistEntryRescuesWholeText(t *testing.T) {
|
|
got := Check("cat", []string{"cat"}, []string{"cat"})
|
|
if got.Blocked || got.Entry != "cat" || got.RescuedBy != "cat" {
|
|
t.Fatalf("Check = %+v; want rescued by an identical entry", got)
|
|
}
|
|
}
|
|
|
|
// A whitelist entry that overlaps a match without containing it is not a
|
|
// rescue: "bca" covers only the tail of the "ab" at index 0.
|
|
func TestCheck_OverlapWithoutContainmentDoesNotRescue(t *testing.T) {
|
|
if got := Check("abca", []string{"ab"}, []string{"bca"}); !got.Blocked {
|
|
t.Fatalf("Check = %+v; want blocked", got)
|
|
}
|
|
}
|
|
|
|
// Overlapping occurrences of one entry must all be considered — scanning by
|
|
// whole-match strides would miss the second "aa" here and wrongly allow it.
|
|
func TestCheck_FindsOverlappingOccurrences(t *testing.T) {
|
|
// "xaa" is whitelisted, spanning the first "aa" only.
|
|
if got := Check("xaaa", []string{"aa"}, []string{"xaa"}); !got.Blocked {
|
|
t.Fatalf("Check = %+v; want blocked by the second, unspanned occurrence", got)
|
|
}
|
|
}
|
|
|
|
func TestCheck_EmptyTextMatchesNothing(t *testing.T) {
|
|
if got := Check("", []string{"cat"}, nil); got != (Verdict{}) {
|
|
t.Fatalf("Check = %+v; want zero verdict", got)
|
|
}
|
|
}
|
|
|
|
// DocStore.List gives no ordering guarantee, so the verdict must not depend on
|
|
// the order entries arrive in.
|
|
func TestCheck_IsOrderIndependent(t *testing.T) {
|
|
text := "the cat and the dog"
|
|
forward := Check(text, []string{"cat", "dog"}, nil)
|
|
reverse := Check(text, []string{"dog", "cat"}, nil)
|
|
if forward != reverse {
|
|
t.Fatalf("order changed the verdict: %+v vs %+v", forward, reverse)
|
|
}
|
|
if forward.Entry != "cat" {
|
|
t.Fatalf("Entry = %q; want the first entry in sorted order", forward.Entry)
|
|
}
|
|
}
|