From f6e6d4eb5957c4d587e8b4760747e16f0c958220 Mon Sep 17 00:00:00 2001 From: Fdvkey Date: Mon, 20 Jul 2026 15:33:15 +0700 Subject: [PATCH] feat: implement group filter management with caching and database integration --- cmds/group/filter.go | 171 +++++++++++++++++++++++++++++++++++++++++++ cmds/register.go | 1 + database/db.go | 29 ++++++++ database/models.go | 14 ++++ go.mod | 8 +- go.sum | 24 +++--- main.go | 8 ++ 7 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 cmds/group/filter.go create mode 100644 database/db.go create mode 100644 database/models.go diff --git a/cmds/group/filter.go b/cmds/group/filter.go new file mode 100644 index 0000000..d6c0635 --- /dev/null +++ b/cmds/group/filter.go @@ -0,0 +1,171 @@ +package group + +import ( + "strings" + "sync" + + "neo/core" + hc "neo/context" + "neo/database" +) + +var ( + // filterCache stores group filters. Map of ChatJID string -> map of Key string -> Content string + filterCache = make(map[string]map[string]string) + cacheMu sync.RWMutex +) + +func init() { + core.Default.Register(&core.Command{ + Name: "filter", + Aliases: []string{"f"}, + Description: "Manage group filters", + Category: "Group", + Permissions: []core.Permission{core.PermissionGroupOnly}, + Run: HandleFilterCommand, + }) + + // Register the middleware to catch incoming messages + core.Default.Use(FilterMiddleware) +} + +// LoadCache loads all filters from DB into memory +func LoadCache() { + cacheMu.Lock() + defer cacheMu.Unlock() + + var filters []database.Filter + database.DB.Find(&filters) + + for _, f := range filters { + if filterCache[f.GroupID] == nil { + filterCache[f.GroupID] = make(map[string]string) + } + filterCache[f.GroupID][f.Key] = f.Content + } +} + +func HandleFilterCommand(ctx *hc.Ctx) { + args := ctx.Arguments() + if len(args) < 1 { + ctx.Reply("Usage:\n.filter add key|content\n.filter del key") + return + } + + action := args[0] + chatID := ctx.ChatJID().String() + + switch action { + case "add": + fullArgs := ctx.FullArgs() + // Remove the 'add ' prefix from full arguments string + contentStart := strings.Index(fullArgs, "add") + 3 + if contentStart >= len(fullArgs) { + ctx.Reply("Invalid format. Use: .filter add key|content") + return + } + + payload := strings.TrimSpace(fullArgs[contentStart:]) + + parts := strings.SplitN(payload, "|", 2) + if len(parts) < 2 { + ctx.Reply("Invalid format. Use: .filter add key|content") + return + } + + key := strings.ToLower(strings.TrimSpace(parts[0])) + content := strings.TrimSpace(parts[1]) + + if key == "" || content == "" { + ctx.Reply("Key or content cannot be empty.") + return + } + + // Update DB + var filter database.Filter + result := database.DB.Where(database.Filter{Key: key, GroupID: chatID}).FirstOrCreate(&filter, database.Filter{ + Key: key, + GroupID: chatID, + Content: content, + }) + + if result.RowsAffected == 0 { + // Update if exists + filter.Content = content + database.DB.Save(&filter) + } + + // Update Cache + cacheMu.Lock() + if filterCache[chatID] == nil { + filterCache[chatID] = make(map[string]string) + } + filterCache[chatID][key] = content + cacheMu.Unlock() + + ctx.Reply("Filter '" + key + "' added successfully.") + + case "del": + if len(args) < 2 { + ctx.Reply("Usage: .filter del key") + return + } + key := strings.ToLower(strings.Join(args[1:], " ")) + + // Delete from DB + database.DB.Where(database.Filter{Key: key, GroupID: chatID}).Delete(&database.Filter{}) + + // Delete from cache + cacheMu.Lock() + if groupFilters, ok := filterCache[chatID]; ok { + delete(groupFilters, key) + } + cacheMu.Unlock() + + ctx.Reply("Filter '" + key + "' deleted successfully.") + + default: + ctx.Reply("Unknown action. Use add or del.") + } +} + +func FilterMiddleware(ctx *hc.Ctx) bool { + if !ctx.IsGroup() { + return false + } + + // Get message text safely handling different message types similarly to core.go + text := hc.ParseMessageText(ctx.Event()) + + if text == "" { + return false + } + + // Don't intercept normal commands + prefixes := []string{"!", "/", "."} + for _, p := range prefixes { + if strings.HasPrefix(text, p) { + return false + } + } + + lowerText := strings.ToLower(strings.TrimSpace(text)) + chatID := ctx.ChatJID().String() + + cacheMu.RLock() + groupFilters, exists := filterCache[chatID] + if !exists { + cacheMu.RUnlock() + return false + } + + content, hasFilter := groupFilters[lowerText] + cacheMu.RUnlock() + + if hasFilter { + ctx.Reply(content) + return true // Stop processing, we handled it + } + + return false +} diff --git a/cmds/register.go b/cmds/register.go index 8cdede6..d1c7b0d 100644 --- a/cmds/register.go +++ b/cmds/register.go @@ -3,6 +3,7 @@ package cmds import ( _ "neo/cmds/download" _ "neo/cmds/general" + _ "neo/cmds/group" _ "neo/cmds/media" _ "neo/cmds/misc" ) diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..d07125e --- /dev/null +++ b/database/db.go @@ -0,0 +1,29 @@ +package database + +import ( + "log" + "os" + "path/filepath" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +func InitDB(dbPath string) { + err := os.MkdirAll(filepath.Dir(dbPath), 0755) + if err != nil { + log.Fatalf("Failed to create database directory: %v", err) + } + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + + DB = db +} diff --git a/database/models.go b/database/models.go new file mode 100644 index 0000000..4624957 --- /dev/null +++ b/database/models.go @@ -0,0 +1,14 @@ +package database + +import "gorm.io/gorm" + +type Filter struct { + gorm.Model + Key string `gorm:"type:varchar(255);uniqueIndex:idx_group_key"` + GroupID string `gorm:"type:varchar(255);uniqueIndex:idx_group_key"` + Content string `gorm:"type:text"` +} + +func AutoMigrate() { + DB.AutoMigrate(&Filter{}) +} diff --git a/go.mod b/go.mod index 6b760f1..856e525 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,25 @@ module neo go 1.25.5 require ( + github.com/davidbyttow/govips/v2 v2.18.0 github.com/gabriel-vasile/mimetype v1.4.13 github.com/joho/godotenv v1.5.1 - github.com/mattn/go-sqlite3 v1.14.47 + github.com/mattn/go-sqlite3 v1.14.48 github.com/mdp/qrterminal/v3 v3.2.1 go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b google.golang.org/protobuf v1.36.11 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.2 ) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/beeper/argo-go v1.1.2 // indirect github.com/coder/websocket v1.8.15 // indirect - github.com/davidbyttow/govips/v2 v2.18.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect diff --git a/go.sum b/go.sum index e1c6d00..7b83f1e 100644 --- a/go.sum +++ b/go.sum @@ -22,14 +22,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo= -github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= @@ -50,38 +54,30 @@ go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y= go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go index 8611cb5..ad0f23e 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,9 @@ import ( "syscall" _ "neo/cmds" + "neo/cmds/group" "neo/core" + "neo/database" "github.com/davidbyttow/govips/v2/vips" "github.com/joho/godotenv" @@ -37,6 +39,12 @@ func main() { vips.Startup(nil) defer vips.Shutdown() + go func() { + database.InitDB(os.Getenv("SQLITE_PATH")) + database.AutoMigrate() + group.LoadCache() + }() + dbLog := waLog.Stdout("Database", "DEBUG", true) ctx := context.Background() container, err := sqlstore.New(ctx, "sqlite3", fmt.Sprintf("file:%s?_foreign_keys=on", os.Getenv("SESSION_PATH")), dbLog)