diff --git a/go.mod b/go.mod index 478d2e99..6beb20bf 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/FloatTech/AnimeAPI v1.5.1-0.20220901132657-2585bbc03bf6 github.com/FloatTech/floatbox v0.0.0-20220822040527-f059031fec44 github.com/FloatTech/sqlite v0.3.3 + github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b github.com/FloatTech/zbpctrl v1.4.1-0.20220715042842-93f081cb0133 - github.com/FloatTech/zbputils v1.5.1-0.20220826103123-0c73a585e38f + github.com/FloatTech/zbputils v1.5.1-0.20220906100116-30c0892066d6 github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c github.com/antchfx/htmlquery v1.2.5 github.com/corona10/goimagehash v1.0.3 @@ -36,7 +37,6 @@ require ( ) require ( - github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b // indirect github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc // indirect github.com/antchfx/xpath v1.2.1 // indirect github.com/cheekybits/genny v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9054a075..10a6cf48 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJG github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= github.com/FloatTech/zbpctrl v1.4.1-0.20220715042842-93f081cb0133 h1:nP9NI4I+vtwAbiU7wCJwjuzCzMZ/yJYg8h3667HGnv0= github.com/FloatTech/zbpctrl v1.4.1-0.20220715042842-93f081cb0133/go.mod h1:72BnjyBwQWUC8mqM9dPk5ZrjxXCilQCVp+jfgHATNdw= -github.com/FloatTech/zbputils v1.5.1-0.20220826103123-0c73a585e38f h1:R13GKhRZfB42nsCEqek/ZlGbKBopQOZFYwBU9qSCfhg= -github.com/FloatTech/zbputils v1.5.1-0.20220826103123-0c73a585e38f/go.mod h1:ZT91eCYR6y1HbLRTO5EB7o8K7qEPbCsX7vjhYIuMijc= +github.com/FloatTech/zbputils v1.5.1-0.20220906100116-30c0892066d6 h1:9+PCM0UzbtrpYSzkvy+AeVy6bpprmVd6BUs17Njao04= +github.com/FloatTech/zbputils v1.5.1-0.20220906100116-30c0892066d6/go.mod h1:ZT91eCYR6y1HbLRTO5EB7o8K7qEPbCsX7vjhYIuMijc= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c h1:cNPOdTNiVwxLpROLjXCgbIPvdkE+BwvxDvgmdYmWx6Q= github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c/go.mod h1:KqZzu7slNKROh3TSYEH/IUMG6f4M+1qubZ5e52QypsE= diff --git a/plugin/antiabuse/anti.go b/plugin/antiabuse/anti.go index ad2b2910..8317c16d 100644 --- a/plugin/antiabuse/anti.go +++ b/plugin/antiabuse/anti.go @@ -2,13 +2,15 @@ package antiabuse import ( - "fmt" + "strconv" "strings" "time" + "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/img/text" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" ) @@ -19,6 +21,7 @@ func init() { Help: "违禁词检测", PrivateDataFolder: "anti_abuse", }) + onceRule := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { managers = ctx.State["managers"].(*ctrl.Control[*zero.Ctx]).Manager db.DBPath = engine.DataFolder() + "anti_abuse.db" @@ -32,39 +35,62 @@ func init() { ctx.SendChain(message.Text("create table error: ", err)) return false } - err = recoverWord() - if err != nil { - ctx.SendChain(message.Text("recover data error: ", err)) + return true + }) + + engine.OnMessage(onceRule, zero.OnlyGroup, func(ctx *zero.Ctx) bool { + if !ctx.Event.IsToMe { + return true + } + uid := ctx.Event.UserID + gid := ctx.Event.GroupID + grp := strconv.FormatInt(gid, 36) + msg := strings.ReplaceAll(ctx.MessageString(), "\n", "") + msg = strings.ReplaceAll(msg, "\r", "") + msg = strings.ReplaceAll(msg, "\t", "") + msg = strings.ReplaceAll(msg, ";", "") + mu.RLock() + defer mu.RUnlock() + if db.CanFind(grp, "WHERE instr('"+msg+"', word)>=0") { + if err := managers.DoBlock(uid); err == nil { + cache.Set(uid, struct{}{}) + ctx.SetGroupBan(gid, uid, 4*3600) + ctx.SendChain(message.Text("检测到违禁词,已封禁/屏蔽4小时")) + } else { + ctx.SendChain(message.Text("block user error:", err)) + } return false } return true }) - engine.OnMessage(onceRule, zero.OnlyGroup, banRule) + engine.OnCommand("添加违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle( func(ctx *zero.Ctx) { args := ctx.State["args"].(string) if err := insertWord(ctx.Event.GroupID, args); err != nil { - ctx.SendChain(message.Text("error:", err)) + ctx.SendChain(message.Text("ERROR: ", err)) } else { - ctx.SendChain(message.Text(fmt.Sprintf("添加违禁词 %s 成功", args))) + ctx.SendChain(message.Text("添加违禁词 ", args, " 成功")) } }) + engine.OnCommand("删除违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle( func(ctx *zero.Ctx) { args := ctx.State["args"].(string) if err := deleteWord(ctx.Event.GroupID, args); err != nil { - ctx.SendChain(message.Text("error:", err)) + ctx.SendChain(message.Text("ERROR: ", err)) } else { - ctx.SendChain(message.Text(fmt.Sprintf("删除违禁词 %s 成功", args))) - } - }) - engine.OnCommand("查看违禁词", zero.OnlyGroup, onceRule).Handle( - func(ctx *zero.Ctx) { - if set, ok := wordMap[ctx.Event.GroupID]; !ok { - ctx.SendChain(message.Text("本群无违禁词")) - } else { - ctx.SendChain(message.Text("本群违禁词有:", strings.Join(set.ToSlice(), " |"))) + ctx.SendChain(message.Text("删除违禁词 ", args, " 成功")) } }) + engine.OnCommand("查看违禁词", zero.OnlyGroup, onceRule).Handle( + func(ctx *zero.Ctx) { + b, err := text.RenderToBase64(listWords(ctx.Event.GroupID), text.FontFile, 400, 20) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Text("本群违禁词有:\n"), message.Image("base64://"+binary.BytesToString(b))) + }) } diff --git a/plugin/antiabuse/db.go b/plugin/antiabuse/db.go new file mode 100644 index 00000000..7c157330 --- /dev/null +++ b/plugin/antiabuse/db.go @@ -0,0 +1,76 @@ +package antiabuse + +import ( + "errors" + "strconv" + "strings" + "sync" + "time" + "unsafe" + + sqlite "github.com/FloatTech/sqlite" + "github.com/FloatTech/ttl" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" +) + +var managers *ctrl.Manager[*zero.Ctx] // managers lazy load +var db = &sqlite.Sqlite{} +var mu sync.RWMutex + +func onDel(uid int64, _ struct{}) { + if managers == nil { + return + } + if err := managers.DoUnblock(uid); err != nil { + logrus.Error("do unblock error:", err) + } +} + +var cache = ttl.NewCacheOn(4*time.Hour, [4]func(int64, struct{}){nil, nil, onDel, nil}) + +type banWord struct { + Word string `db:"word"` +} + +var nilban = &banWord{} + +func insertWord(gid int64, word string) error { + grp := strconv.FormatInt(gid, 36) + mu.Lock() + defer mu.Unlock() + err := db.Create(grp, nilban) + if err != nil { + return err + } + return db.Insert(grp, (*banWord)(unsafe.Pointer(&word))) +} + +func deleteWord(gid int64, word string) error { + grp := strconv.FormatInt(gid, 36) + mu.Lock() + defer mu.Unlock() + if n, _ := db.Count(grp); n == 0 { + return errors.New("本群还没有违禁词~") + } + return db.Del(grp, "WHRER word='"+word+"'") +} + +func listWords(gid int64) string { + grp := strconv.FormatInt(gid, 36) + word := "" + ptr := (*banWord)(unsafe.Pointer(&word)) + sb := strings.Builder{} + mu.Lock() + defer mu.Unlock() + _ = db.FindFor(grp, ptr, "", func() error { + sb.WriteString(word) + sb.WriteString(" |") + return nil + }) + if sb.Len() <= 2 { + return "" + } + return sb.String()[:sb.Len()-2] +} diff --git a/plugin/antiabuse/set.go b/plugin/antiabuse/set.go deleted file mode 100644 index 92e5acef..00000000 --- a/plugin/antiabuse/set.go +++ /dev/null @@ -1,62 +0,0 @@ -package antiabuse - -import "sync" - -//Set defines HashSet structure -type Set[T comparable] struct { - sync.RWMutex - m map[T]struct{} -} - -// NewSet creates Set with optional key(s) -func NewSet[T comparable]() *Set[T] { - return &Set[T]{m: make(map[T]struct{})} -} - -// Add adds key(s) to Set -func (s *Set[T]) Add(key ...T) { - s.Lock() - defer s.Unlock() - for _, k := range key { - s.m[k] = struct{}{} - } -} - -// Include asserts key in Set -func (s *Set[T]) Include(key T) bool { - s.RLock() - defer s.RUnlock() - _, ok := s.m[key] - return ok -} - -// Iter calls f when traversing Set -func (s *Set[T]) Iter(f func(T) error) error { - s.RLock() - defer s.RUnlock() - var err error - for key := range s.m { - err = f(key) - if err != nil { - return err - } - } - return nil -} - -// Remove removes key from Set -func (s *Set[T]) Remove(key T) { - s.Lock() - defer s.Unlock() - delete(s.m, key) -} - -// ToSlice convert Set to slice -func (s *Set[T]) ToSlice() (res []T) { - s.RLock() - defer s.RUnlock() - for key := range s.m { - res = append(res, key) - } - return res -} diff --git a/plugin/antiabuse/utils.go b/plugin/antiabuse/utils.go deleted file mode 100644 index 8e603184..00000000 --- a/plugin/antiabuse/utils.go +++ /dev/null @@ -1,110 +0,0 @@ -package antiabuse - -import ( - "errors" - "fmt" - "hash/crc32" - "strings" - "time" - - sqlite "github.com/FloatTech/sqlite" - "github.com/FloatTech/ttl" - ctrl "github.com/FloatTech/zbpctrl" - "github.com/sirupsen/logrus" - zero "github.com/wdvxdr1123/ZeroBot" - "github.com/wdvxdr1123/ZeroBot/message" -) - -var managers *ctrl.Manager[*zero.Ctx] //managers lazy load -var errBreak = errors.New("break") -var db = &sqlite.Sqlite{} -var crc32Table = crc32.MakeTable(crc32.IEEE) -var wordMap = make(map[int64]*Set[string]) - -func onDel(uid int64, _ struct{}) { - if managers == nil { - return - } - if err := managers.DoUnblock(uid); err != nil { - logrus.Error("do unblock error:", err) - } -} - -var cache = ttl.NewCacheOn[int64, struct{}](4*time.Hour, [4]func(int64, struct{}){ - nil, nil, onDel, nil}) - -type banWord struct { - Crc32ID uint32 `db:"crc32_id"` - GroupID int64 `db:"group_id"` - Word string `db:"word"` -} - -func banRule(ctx *zero.Ctx) bool { - if !ctx.Event.IsToMe { - return true - } - uid := ctx.Event.UserID - gid := ctx.Event.GroupID - wordSet := wordMap[gid] - if wordSet == nil { - return true - } - err := wordSet.Iter(func(word string) error { - if strings.Contains(ctx.MessageString(), word) { - if err := managers.DoBlock(uid); err != nil { - return err - } - cache.Set(uid, struct{}{}) - return errBreak - } - return nil - }) - if err != nil && err != errBreak { - ctx.SendChain(message.Text("block user error:", err)) - return true - } - ctx.SetGroupBan(gid, uid, 4*3600) - ctx.SendChain(message.Text("检测到违禁词,已封禁/屏蔽4小时")) - return false -} - -func insertWord(gid int64, word string) error { - str := fmt.Sprintf("%d-%s", gid, word) - checksum := crc32.Checksum([]byte(str), crc32Table) - obj := &banWord{checksum, gid, word} - if _, ok := wordMap[gid]; !ok { - wordMap[gid] = NewSet[string]() - } - wordMap[gid].Add(word) - return db.Insert("banWord", obj) -} - -func deleteWord(gid int64, word string) error { - if _, ok := wordMap[gid]; !ok { - return errors.New("本群还没有违禁词~") - } - if !wordMap[gid].Include(word) { - return errors.New(word + " 不在本群违禁词集合中") - } - wordMap[gid].Remove(word) - str := fmt.Sprintf("%d-%s", gid, word) - checksum := crc32.Checksum([]byte(str), crc32Table) - sql := fmt.Sprintf("WHERE crc32_id = %d", checksum) - return db.Del("banWord", sql) -} - -func recoverWord() error { - if !db.CanFind("banWord", "") { - return nil - } - obj := &banWord{} - err := db.FindFor("banWord", obj, "", func() error { - if _, ok := wordMap[obj.GroupID]; !ok { - wordMap[obj.GroupID] = NewSet[string]() - } - wordMap[obj.GroupID].Add(obj.Word) - return nil - }, - ) - return err -} diff --git a/plugin/antiabuse/utils_test.go b/plugin/antiabuse/utils_test.go deleted file mode 100644 index c41334d5..00000000 --- a/plugin/antiabuse/utils_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package antiabuse - -import ( - "os" - "sort" - "testing" -) - -func TestInsertWord(t *testing.T) { - wordMap = make(map[int64]*Set[string]) - defer func() { - wordMap = make(map[int64]*Set[string]) - }() - path := "test.db" - defer func() { - err := os.Remove("test.db") - if err != nil { - t.Fatal(err) - } - }() - db.DBPath = path - err := db.Open(0) - defer func() { - err := db.Close() - if err != nil { - t.Fatal(err) - } - }() - err = db.Create("banWord", &banWord{}) - if err != nil { - t.Fatal(err) - } - defer func() { - err := db.Drop("banWord") - if err != nil { - t.Fatal(err) - } - }() - err = insertWord(123, "one") - if err != nil { - t.Fatal(err) - } - if ok := wordMap[123].Include("one"); !ok { - t.Fatal(`wordMap[123] should found "one" but not`) - } - if !db.CanFind("banWord", "WHERE group_id=123 AND word= 'one' ") { - t.Fatal(`db should found 123-one but not`) - } -} - -func TestDeleteWord(t *testing.T) { - wordMap = make(map[int64]*Set[string]) - defer func() { - wordMap = make(map[int64]*Set[string]) - }() - path := "test.db" - defer func() { - err := os.Remove("test.db") - if err != nil { - t.Fatal(err) - } - }() - db.DBPath = path - err := db.Open(0) - defer func() { - err := db.Close() - if err != nil { - t.Fatal(err) - } - }() - err = db.Create("banWord", &banWord{}) - if err != nil { - t.Fatal(err) - } - defer func() { - err := db.Drop("banWord") - if err != nil { - t.Fatal(err) - } - }() - err = insertWord(123, "one") - if err != nil { - t.Fatal(err) - } - err = deleteWord(123, "one") - if err != nil { - t.Fatal(err) - } -} - -func TestShowWord(t *testing.T) { - wordMap = make(map[int64]*Set[string]) - defer func() { - wordMap = make(map[int64]*Set[string]) - }() - path := "test.db" - defer func() { - err := os.Remove("test.db") - if err != nil { - t.Fatal(err) - } - }() - db.DBPath = path - err := db.Open(0) - defer func() { - err := db.Close() - if err != nil { - t.Fatal(err) - } - }() - err = db.Create("banWord", &banWord{}) - if err != nil { - t.Fatal(err) - } - defer func() { - err := db.Drop("banWord") - if err != nil { - t.Fatal(err) - } - }() - err = insertWord(123, "one") - if err != nil { - t.Fatal(err) - } - err = insertWord(123, "one") - if err != nil { - t.Fatal(err) - } - err = insertWord(123, "two") - if err != nil { - t.Fatal(err) - } - var db123 []string - var map123 []string - obj := &banWord{} - err = db.FindFor("banWord", obj, "WHERE group_id=123", func() error { - db123 = append(db123, obj.Word) - return nil - }) - if err != nil { - t.Fatal(err) - } - sort.Strings(db123) - if len(db123) != 2 || db123[0] != "one" || db123[1] != "two" { - t.Fatal("db should found 123-one and 123-two but not") - } - map123 = wordMap[123].ToSlice() - sort.Strings(map123) - if len(map123) != 2 || map123[0] != "one" || map123[1] != "two" { - t.Fatal("wordMap[123] should found 123-one and 123-two but not") - } -} - -func TestRecoverWord(t *testing.T) { - wordMap = make(map[int64]*Set[string]) - defer func() { - wordMap = make(map[int64]*Set[string]) - }() - path := "test.db" - defer func() { - err := os.Remove("test.db") - if err != nil { - t.Fatal(err) - } - }() - db.DBPath = path - err := db.Open(0) - defer func() { - err := db.Close() - if err != nil { - t.Fatal(err) - } - }() - err = db.Create("banWord", &banWord{}) - if err != nil { - t.Fatal(err) - } - defer func() { - err := db.Drop("banWord") - if err != nil { - t.Fatal(err) - } - }() - err = insertWord(123, "one") - if err != nil { - t.Fatal(err) - } - err = insertWord(123, "two") - if err != nil { - t.Fatal(err) - } - wordMap = make(map[int64]*Set[string]) - err = recoverWord() - if err != nil { - t.Fatal(err) - } - map123 := wordMap[123].ToSlice() - sort.Strings(map123) - if len(map123) != 2 || map123[0] != "one" || map123[1] != "two" { - t.Fatal("wordMap[123] should found 123-one and 123-two but not") - } -}