Spaces:
Sleeping
Sleeping
Download database/database.go from Thaimozhi/Go-Filter-Bot: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/spaces/Thaimozhi/Go-Filter-Bot/resolve/refs%2Fpr%2F1/database/database.go
- Command line
-
hf download hf://spaces/Thaimozhi/Go-Filter-Bot@refs/pr/1/database/database.go
-
curl -L -o database.go https://huggingface.co/spaces/Thaimozhi/Go-Filter-Bot/resolve/refs%2Fpr%2F1/database/database.go
27.2 kB
| // (c) Jisin0 | |
| package database | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "regexp" | |
| "strings" | |
| "sync" | |
| "time" | |
| "github.com/Jisin0/Go-Filter-Bot/utils/config" | |
| "go.mongodb.org/mongo-driver/bson" | |
| "go.mongodb.org/mongo-driver/mongo" | |
| "go.mongodb.org/mongo-driver/mongo/options" | |
| ) | |
| var cachedSettings map[int64]*ChatSettings = make(map[int64]*ChatSettings) | |
| var connectionCache map[int64]int64 = make(map[int64]int64) // cache users' connections. | |
| var defaultChatSettings ChatSettings = ChatSettings{ | |
| Stopped: []string{}, | |
| } | |
| // ββ In-memory filter result cache βββββββββββββββββββββββββββββββββββββββββββββ | |
| type filterCacheEntry struct { | |
| results []*Filter | |
| expiry time.Time | |
| } | |
| var ( | |
| filterCache sync.Map | |
| FilterCacheTTL = 10 * time.Minute | |
| ) | |
| func FilterCacheKey(chatID int64, fields []string) string { | |
| return fmt.Sprintf("%d:%s", chatID, strings.Join(fields, ",")) | |
| } | |
| func GetCachedFilterResults(key string) ([]*Filter, bool) { | |
| v, ok := filterCache.Load(key) | |
| if !ok { | |
| return nil, false | |
| } | |
| e := v.(*filterCacheEntry) | |
| if time.Now().After(e.expiry) { | |
| filterCache.Delete(key) | |
| return nil, false | |
| } | |
| return e.results, true | |
| } | |
| func SetCachedFilterResults(key string, results []*Filter) { | |
| filterCache.Store(key, &filterCacheEntry{ | |
| results: results, | |
| expiry: time.Now().Add(FilterCacheTTL), | |
| }) | |
| } | |
| // Start cache cleanup daemon | |
| func init() { | |
| go func() { | |
| for { | |
| time.Sleep(15 * time.Minute) | |
| now := time.Now() | |
| filterCache.Range(func(key, value any) bool { | |
| if now.After(value.(*filterCacheEntry).expiry) { | |
| filterCache.Delete(key) | |
| } | |
| return true | |
| }) | |
| } | |
| }() | |
| } | |
| // InvalidateFilterCache removes all cached entries for a given chatID. | |
| func InvalidateFilterCache(chatID int64) { | |
| prefix := fmt.Sprintf("%d:", chatID) | |
| filterCache.Range(func(k, _ any) bool { | |
| if strings.HasPrefix(k.(string), prefix) { | |
| filterCache.Delete(k) | |
| } | |
| return true | |
| }) | |
| } | |
| // ββ Ban cache ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| var banCache sync.Map // map[int64]bool | |
| // ββ Data types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| type ChatSettings struct { | |
| // Stopped Global Filters | |
| Stopped []string | |
| } | |
| // A filter object stored in the database β VJ-style structure for high hit ratio. | |
| type Filter struct { | |
| // Unique file_id used as _id (for auto-indexed files) or random string (for manual filters) | |
| ID string `bson:"_id"` | |
| // Chat where the filter is in effect | |
| ChatID int64 `bson:"group_id"` | |
| // The key/text used for searching β for auto-index this is the cleaned file_name | |
| Text string `bson:"text"` | |
| // Full original file name (e.g. "@CinemasTrend Gullak S05E02.mkv") | |
| FileName string `bson:"file_name,omitempty"` | |
| // File size in bytes | |
| FileSize int64 `bson:"file_size,omitempty"` | |
| // file_ref for Pyrogram compat (optional, empty for Bot API bots) | |
| FileRef string `bson:"file_ref,omitempty"` | |
| // MIME type (e.g. "video/x-matroska") | |
| MimeType string `bson:"mime_type,omitempty"` | |
| // The text content/caption saved | |
| Content string `bson:"content,omitempty"` | |
| // The id of a media saved for the filter if any (Telegram file_id) | |
| FileID string `bson:"file,omitempty"` | |
| // Buttons/markup saved for a filter if any | |
| Markup [][]map[string]string `bson:"button,omitempty"` | |
| // Alerts saved for a filter if any | |
| Alerts []string `bson:"alert,omitempty"` | |
| // Length of the text according to which filters are sorted | |
| Length int `bson:"length"` | |
| // Type of media saved if any (video/document/audio/photo/animation/sticker) | |
| MediaType string `bson:"mediaType,omitempty"` | |
| } | |
| // A User Saved in the database. | |
| type User struct { | |
| // Unique telegram id of the user. | |
| ID int64 `bson:"_id"` | |
| // ID of the chat to which the user is connected. | |
| ConnectedChat int64 `bson:"connected,omitempty"` | |
| } | |
| // BannedUser is stored in the Banned collection. | |
| type BannedUser struct { | |
| ID int64 `bson:"_id"` | |
| Reason string `bson:"reason,omitempty"` | |
| } | |
| // BotSettings stores ALL bot-wide settings in the database. | |
| // These can be changed at runtime via /settings command. | |
| type BotSettings struct { | |
| ID string `bson:"_id"` | |
| ForceSubChats []int64 `bson:"force_sub_chats"` | |
| IndexChannels []int64 `bson:"index_channels"` | |
| MultiFilter bool `bson:"multi_filter"` | |
| SearchAsButton bool `bson:"search_as_button"` | |
| AutoDelete int64 `bson:"auto_delete"` | |
| ShortenerURL string `bson:"shortener_url"` | |
| ShortenerAPI string `bson:"shortener_api"` | |
| DynamicAdmins []int64 `bson:"dynamic_admins"` | |
| } | |
| const globalSettingsID = "global" | |
| // In-memory cache of bot settings. | |
| var ( | |
| cachedBotSettings *BotSettings | |
| botSettingsMu sync.RWMutex | |
| ) | |
| func NewDatabase() *Database { | |
| if config.MongodbURI == "" { | |
| log.Fatal("You must set your 'MONGODB_URI' environmental variable :(") | |
| } | |
| client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(config.MongodbURI)) | |
| if err != nil { | |
| panic(err) | |
| } | |
| db := client.Database("Adv_Auto_Filter") | |
| d := &Database{ | |
| Client: client, | |
| DB: db, | |
| Ucol: db.Collection("Users"), | |
| Col: db.Collection("Main"), | |
| Mcol: db.Collection("Manual_Filters"), | |
| BanCol: db.Collection("Banned"), | |
| GlobCol: db.Collection("GlobalSettings"), | |
| } | |
| // Seed defaults from env on first run | |
| d.SeedBotSettings() | |
| return d | |
| } | |
| // SeedBotSettings ensures there is a settings doc in the DB. | |
| // On first run it seeds from env vars; after that it loads from DB. | |
| func (db *Database) SeedBotSettings() { | |
| res := db.GlobCol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: globalSettingsID}}) | |
| if res.Err() == mongo.ErrNoDocuments { | |
| // First run β seed from env | |
| s := BotSettings{ | |
| ID: globalSettingsID, | |
| ForceSubChats: config.ForceSubChannels, | |
| IndexChannels: config.IndexChannels, | |
| MultiFilter: config.MultiFilter, | |
| SearchAsButton: config.SearchAsButton, | |
| AutoDelete: config.AutoDelete, | |
| ShortenerURL: config.ShortenerURL, | |
| ShortenerAPI: config.ShortenerAPI, | |
| } | |
| db.GlobCol.InsertOne(context.TODO(), s) | |
| botSettingsMu.Lock() | |
| cachedBotSettings = &s | |
| botSettingsMu.Unlock() | |
| fmt.Println("bot settings seeded from env") | |
| } else { | |
| // Already exists β load into cache | |
| var s BotSettings | |
| res.Decode(&s) | |
| botSettingsMu.Lock() | |
| cachedBotSettings = &s | |
| botSettingsMu.Unlock() | |
| fmt.Println("bot settings loaded from db") | |
| } | |
| } | |
| // GetBotSettings returns the cached bot settings. | |
| func GetBotSettings() BotSettings { | |
| botSettingsMu.RLock() | |
| defer botSettingsMu.RUnlock() | |
| if cachedBotSettings == nil { | |
| return BotSettings{} | |
| } | |
| return *cachedBotSettings | |
| } | |
| // SetBotSetting updates a single setting key in the DB and refreshes cache. | |
| func (db *Database) SetBotSetting(key string, value any) error { | |
| t := true | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: key, Value: value}}}}, | |
| &options.UpdateOptions{Upsert: &t}, | |
| ) | |
| if err != nil { | |
| fmt.Printf("db.setbotsetting: %v\n", err) | |
| return err | |
| } | |
| db.RefreshBotSettings() | |
| return nil | |
| } | |
| // RefreshBotSettings reloads settings from DB into cache. | |
| func (db *Database) RefreshBotSettings() { | |
| res := db.GlobCol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: globalSettingsID}}) | |
| if res.Err() == nil { | |
| var s BotSettings | |
| res.Decode(&s) | |
| botSettingsMu.Lock() | |
| cachedBotSettings = &s | |
| botSettingsMu.Unlock() | |
| } | |
| } | |
| type Database struct { | |
| // Mongo Client | |
| Client *mongo.Client | |
| // Database | |
| DB *mongo.Database | |
| // Users Collection | |
| Ucol *mongo.Collection | |
| // Main Collection | |
| Col *mongo.Collection | |
| // Manual Filters Collection | |
| Mcol *mongo.Collection | |
| // Banned Users Collection | |
| BanCol *mongo.Collection | |
| // Global Settings Collection | |
| GlobCol *mongo.Collection | |
| } | |
| // ββ User functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| func (db *Database) AddUser(userid int64) error { | |
| var ( | |
| filter = User{ID: userid} | |
| ) | |
| res := db.Ucol.FindOne(context.TODO(), filter) | |
| if res.Err() == mongo.ErrNoDocuments { | |
| _, err := db.Ucol.InsertOne(context.TODO(), filter) | |
| if err != nil { | |
| fmt.Printf("db.adduser: %v", err) | |
| } | |
| } | |
| return nil | |
| } | |
| var statText string = ` | |
| β βΈ <b>Users</b> : <code>%v</code> | |
| β βΈ <b>Filters</b> : <code>%v</code> | |
| β βΈ <b>Groups</b> : <code>%v</code> | |
| β° βΈ <b>Banned</b> : <code>%v</code> | |
| ` | |
| //nolint:errcheck // meh | |
| func (db *Database) Stats() string { | |
| users, _ := db.Ucol.CountDocuments(context.TODO(), bson.M{}) | |
| chats, _ := db.Col.CountDocuments(context.TODO(), bson.M{}) | |
| mfilters, _ := db.Mcol.CountDocuments(context.TODO(), bson.M{}) | |
| banned, _ := db.BanCol.CountDocuments(context.TODO(), bson.M{}) | |
| return fmt.Sprintf(statText, users, mfilters, chats, banned) | |
| } | |
| func (db *Database) GetConnection(userID int64) (int64, bool) { | |
| if c, ok := connectionCache[userID]; ok { | |
| if c == 0 { | |
| ok = false | |
| } | |
| return c, ok | |
| } | |
| res := db.Ucol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: userID}}) | |
| if res.Err() != nil { // this shouldn't happen. the user should be saved in the db. | |
| connectionCache[userID] = 0 | |
| return 0, false | |
| } | |
| var doc User | |
| res.Decode(&doc) | |
| connectionCache[userID] = doc.ConnectedChat | |
| if doc.ConnectedChat != 0 { | |
| return doc.ConnectedChat, true | |
| } | |
| return doc.ConnectedChat, false | |
| } | |
| func (db *Database) ConnectUser(userID, chatID int64) { | |
| var tf = true | |
| _, err := db.Ucol.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: userID}}, bson.D{{Key: "$set", Value: bson.D{{Key: "connected", Value: chatID}}}}, &options.UpdateOptions{Upsert: &tf}) | |
| if err != nil { | |
| fmt.Printf("db.connectuser: %v\n", err) | |
| } | |
| // clear any cache. | |
| delete(connectionCache, userID) | |
| } | |
| func (db *Database) DeleteConnection(userID int64) { | |
| _, err := db.Ucol.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: userID}}, bson.D{{Key: "$unset", Value: "connected"}}) | |
| if err != nil { | |
| fmt.Printf("db.connectuser: %v\n", err) | |
| } | |
| // clear any cache. | |
| delete(connectionCache, userID) | |
| } | |
| // ββ Ban functions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // BanUser bans a user, preventing the bot from responding to them. | |
| func (db *Database) BanUser(userID int64, reason string) error { | |
| banCache.Store(userID, true) | |
| t := true | |
| _, err := db.BanCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: userID}}, | |
| bson.D{{Key: "$set", Value: bson.D{ | |
| {Key: "_id", Value: userID}, | |
| {Key: "reason", Value: reason}, | |
| }}}, | |
| &options.UpdateOptions{Upsert: &t}, | |
| ) | |
| if err != nil { | |
| fmt.Printf("db.banuser: %v\n", err) | |
| } | |
| return err | |
| } | |
| // UnbanUser removes a ban from a user. | |
| func (db *Database) UnbanUser(userID int64) error { | |
| banCache.Delete(userID) | |
| _, err := db.BanCol.DeleteOne(context.TODO(), bson.D{{Key: "_id", Value: userID}}) | |
| if err != nil { | |
| fmt.Printf("db.unbanuser: %v\n", err) | |
| } | |
| return err | |
| } | |
| // IsBanned checks if a user is banned. Uses in-memory cache first. | |
| func (db *Database) IsBanned(userID int64) bool { | |
| if v, ok := banCache.Load(userID); ok { | |
| return v.(bool) | |
| } | |
| res := db.BanCol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: userID}}) | |
| banned := res.Err() == nil | |
| banCache.Store(userID, banned) | |
| return banned | |
| } | |
| // GetBannedUsers returns all banned users. | |
| func (db *Database) GetBannedUsers() ([]BannedUser, error) { | |
| cur, err := db.BanCol.Find(context.TODO(), bson.D{}) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var users []BannedUser | |
| if err := cur.All(context.TODO(), &users); err != nil { | |
| return nil, err | |
| } | |
| return users, nil | |
| } | |
| // ββ Force-sub functions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GetForceSubChannels reads from the cached bot settings. | |
| func (db *Database) GetForceSubChannels() []int64 { | |
| return GetBotSettings().ForceSubChats | |
| } | |
| // AddForceSubChannel adds a channel to the force-sub list and refreshes cache. | |
| func (db *Database) AddForceSubChannel(chanID int64) error { | |
| t := true | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$addToSet", Value: bson.D{{Key: "force_sub_chats", Value: chanID}}}}, | |
| &options.UpdateOptions{Upsert: &t}, | |
| ) | |
| if err != nil && strings.Contains(err.Error(), "non-array type null") { | |
| _, err = db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: "force_sub_chats", Value: []int64{chanID}}}}}, | |
| ) | |
| } | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // RemoveForceSubChannel removes a channel from the force-sub list and refreshes cache. | |
| func (db *Database) RemoveForceSubChannel(chanID int64) error { | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$pull", Value: bson.D{{Key: "force_sub_chats", Value: chanID}}}}, | |
| ) | |
| if err != nil && strings.Contains(err.Error(), "non-array type null") { | |
| _, err = db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: "force_sub_chats", Value: []int64{}}}}}, | |
| ) | |
| } | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // ββ Auto-Index functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // AddIndexChannel adds a channel to the auto-index list. | |
| func (db *Database) AddIndexChannel(chanID int64) error { | |
| t := true | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$addToSet", Value: bson.D{{Key: "index_channels", Value: chanID}}}}, | |
| &options.UpdateOptions{Upsert: &t}, | |
| ) | |
| if err != nil && strings.Contains(err.Error(), "non-array type null") { | |
| _, err = db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: "index_channels", Value: []int64{chanID}}}}}, | |
| ) | |
| } | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // RemoveIndexChannel removes a channel from the auto-index list. | |
| func (db *Database) RemoveIndexChannel(chanID int64) error { | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$pull", Value: bson.D{{Key: "index_channels", Value: chanID}}}}, | |
| ) | |
| if err != nil && strings.Contains(err.Error(), "non-array type null") { | |
| _, err = db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: "index_channels", Value: []int64{}}}}}, | |
| ) | |
| } | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // ββ Filter functions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| func (db *Database) SaveMfilter(data *Filter) { | |
| _, err := db.Mcol.InsertOne(context.TODO(), data) | |
| if err != nil { | |
| fmt.Printf("db.savemfilter: %v\n", err) | |
| } | |
| InvalidateFilterCache(data.ChatID) | |
| } | |
| // Returns all manual filters for a chat. | |
| func (db *Database) GetMfilters(chatID int64) (*mongo.Cursor, error) { | |
| return db.Mcol.Aggregate( | |
| context.TODO(), | |
| []bson.D{ | |
| {{Key: "$match", Value: bson.D{{Key: "group_id", Value: chatID}}}}, | |
| {{Key: "$sort", Value: bson.D{{Key: "length", Value: -1}}}}, | |
| }, | |
| ) | |
| } | |
| // SearchMfilterClassic uses the traditional way of fetching mfilters by fetching all mfilters of a chat and doing regex queries individually. | |
| // This method is more resource intensive but could be faster for large scale bots with several hundred/thousand groups. | |
| func (db *Database) SearchMfilterClassic(chatID int64, input string) (results []*Filter) { | |
| res, e := db.GetMfilters(chatID) | |
| if e != nil { | |
| fmt.Printf("db.searchmfiltersclassic: %v\n", e) | |
| return results | |
| } | |
| for res.Next(context.TODO()) { | |
| var f Filter | |
| err := res.Decode(&f) | |
| if err != nil { | |
| fmt.Printf("db.searchmfilterclassic: %v\n", err) | |
| continue | |
| } | |
| text := `(?i)( |^|[^\w])` + f.Text + `( |$|[^\w])` | |
| pattern := regexp.MustCompile(text) | |
| m := pattern.FindStringSubmatch(input) | |
| if len(m) > 0 { | |
| results = append(results, &f) | |
| } | |
| } | |
| return results | |
| } | |
| // SearchMfilterNew does a regex query on the database β searches both text and file_name | |
| // fields for a much higher hit ratio (VJ-style approach). | |
| func (db *Database) SearchMfilterNew(chatID int64, fields []string, multiFilter bool) (results []*Filter) { | |
| // Build a pattern that matches any word from the query | |
| pattern := "(?i)(" + strings.Join(fields, "|") + ")" | |
| // Search in BOTH text and file_name fields for maximum coverage | |
| filter := bson.D{ | |
| {Key: "group_id", Value: chatID}, | |
| {Key: "$or", Value: bson.A{ | |
| bson.D{{Key: "text", Value: bson.M{"$regex": pattern}}}, | |
| bson.D{{Key: "file_name", Value: bson.M{"$regex": pattern}}}, | |
| }}, | |
| } | |
| if !multiFilter { | |
| res := db.Mcol.FindOne(context.Background(), filter) | |
| switch res.Err() { | |
| case mongo.ErrNoDocuments: | |
| return results | |
| case nil: | |
| var f Filter | |
| if err := res.Decode(&f); err != nil { | |
| fmt.Printf("db.searchmfilternew: %v\n", err) | |
| return results | |
| } | |
| return append(results, &f) | |
| } | |
| } | |
| res, err := db.Mcol.Find(context.Background(), filter, options.Find().SetSort(bson.D{{Key: "length", Value: -1}}).SetLimit(50)) | |
| if err != nil { | |
| fmt.Printf("db.searchmfilternew: %v\n", err) | |
| return results | |
| } | |
| for res.Next(context.Background()) { | |
| var r Filter | |
| if err := res.Decode(&r); err != nil { | |
| fmt.Printf("db.searchmfilternew: %v\n", err) | |
| continue | |
| } | |
| results = append(results, &r) | |
| } | |
| return results | |
| } | |
| func (db *Database) GetMfilter(chatID int64, key string) (bson.M, bool) { | |
| res := db.Mcol.FindOne(context.TODO(), bson.D{{Key: "group_id", Value: chatID}, {Key: "text", Value: key}}) | |
| var b bson.M | |
| if res.Err() != nil { | |
| return b, false | |
| } else { | |
| err := res.Decode(&b) | |
| if err != nil { | |
| fmt.Printf("db.getmfilter: %v\n", err) | |
| return b, false | |
| } | |
| return b, true | |
| } | |
| } | |
| func (db *Database) StringMfilter(chatID int64) string { | |
| r, err := db.GetMfilters(chatID) | |
| if err != nil { | |
| return fmt.Sprintf("failed to get filters: %v", err) | |
| } | |
| var text string | |
| for r.Next(context.TODO()) { | |
| var d bson.M | |
| r.Decode(&d) | |
| text += fmt.Sprintf("\nβ’ <code>%v</code>", d["text"]) | |
| } | |
| return text | |
| } | |
| func (db *Database) StopGfilter(chatID int64, key string) { | |
| var t = true | |
| _, err := db.Col.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: chatID}}, bson.D{{Key: "$append", Value: bson.D{{Key: "stopped", Value: key}}}}, &options.UpdateOptions{Upsert: &t}) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| go db.RecacheSettings(chatID) | |
| } | |
| func (db *Database) DeleteMfilter(chatID int64, key string) { | |
| _, err := db.Mcol.DeleteOne(context.TODO(), bson.D{{Key: "group_id", Value: chatID}, {Key: "text", Value: key}}) | |
| if err != nil { | |
| fmt.Printf("db.deletemfilter: %v\n", err) | |
| } | |
| InvalidateFilterCache(chatID) | |
| } | |
| func (db *Database) SetChatSetting(chatID int64, key string, value any) { | |
| go db.SetDefaultSettings(chatID) | |
| _, err := db.Col.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: chatID}}, bson.D{{Key: "$set", Value: bson.D{{Key: key, Value: value}}}}) | |
| if err != nil { | |
| fmt.Printf("db.connectuser: %v\n", err) | |
| return | |
| } | |
| go db.RecacheSettings(chatID) | |
| } | |
| func (db *Database) SetDefaultSettings(chatID int64) { | |
| r := db.Col.FindOne(context.TODO(), bson.D{{Key: "_id", Value: chatID}}) | |
| if r.Err() == mongo.ErrNoDocuments { | |
| db.Col.InsertOne(context.TODO(), defaultSettings(chatID)) | |
| } | |
| } | |
| func defaultSettings(chatID int64) bson.D { | |
| return bson.D{ | |
| {Key: "_id", Value: chatID}, | |
| } | |
| } | |
| // A Function To Update Cached Settings With Latest From DB | |
| func (db *Database) RecacheSettings(chatID int64) { | |
| res := db.Col.FindOne(context.TODO(), bson.D{{Key: "_id", Value: chatID}}) | |
| if res.Err() == mongo.ErrNoDocuments { | |
| cachedSettings[chatID] = &defaultChatSettings | |
| } else { | |
| var r ChatSettings | |
| err := res.Decode(&r) | |
| if err != nil { | |
| fmt.Printf("db.recachesettings: %v\n", err) | |
| return | |
| } | |
| cachedSettings[chatID] = &r | |
| } | |
| } | |
| func (db *Database) GetCachedSetting(chatID int64) *ChatSettings { | |
| s, e := cachedSettings[chatID] | |
| if !e { | |
| go db.RecacheSettings(chatID) | |
| return &defaultChatSettings | |
| } else { | |
| return s | |
| } | |
| } | |
| func (db *Database) StartGfilter(chatID int64, key string) { | |
| keys := db.GetCachedSetting(chatID).Stopped | |
| for i, k := range keys { | |
| if k == key { | |
| keys[i] = keys[len(keys)-1] | |
| keys = keys[:len(keys)-1] | |
| } | |
| } | |
| _, err := db.Col.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: chatID}}, bson.D{{Key: "$set", Value: bson.D{{Key: "stopped", Value: keys}}}}) | |
| if err != nil { | |
| fmt.Printf("db.startgfilter: %v\n", err) | |
| } | |
| go db.RecacheSettings(chatID) | |
| } | |
| func (db *Database) GetAlert(uniqueID string, index int) string { | |
| defaultString := "404: Content Not Found !" | |
| res := db.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: uniqueID}}) | |
| var f Filter | |
| res.Decode(&f) | |
| if len(f.Alerts) < index+1 { | |
| return defaultString | |
| } else { | |
| return f.Alerts[index] | |
| } | |
| } | |
| // ββ Dynamic Admin functions ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GetDynamicAdmins returns the list of dynamically added admins from DB. | |
| func (db *Database) GetDynamicAdmins() []int64 { | |
| s := GetBotSettings() | |
| return s.DynamicAdmins | |
| } | |
| // AddDynamicAdmin adds a user to the dynamic admins list. | |
| func (db *Database) AddDynamicAdmin(userID int64) error { | |
| t := true | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$addToSet", Value: bson.D{{Key: "dynamic_admins", Value: userID}}}}, | |
| &options.UpdateOptions{Upsert: &t}, | |
| ) | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // RemoveDynamicAdmin removes a user from the dynamic admins list. | |
| func (db *Database) RemoveDynamicAdmin(userID int64) error { | |
| _, err := db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$pull", Value: bson.D{{Key: "dynamic_admins", Value: userID}}}}, | |
| ) | |
| if err != nil && strings.Contains(err.Error(), "non-array type null") { | |
| _, err = db.GlobCol.UpdateOne( | |
| context.TODO(), | |
| bson.D{{Key: "_id", Value: globalSettingsID}}, | |
| bson.D{{Key: "$set", Value: bson.D{{Key: "dynamic_admins", Value: []int64{}}}}}, | |
| ) | |
| } | |
| if err == nil { | |
| db.RefreshBotSettings() | |
| } | |
| return err | |
| } | |
| // GetAllUserIDs returns all user IDs stored in the DB. | |
| func (db *Database) GetAllUserIDs() ([]int64, error) { | |
| cur, err := db.Ucol.Find(context.TODO(), bson.M{}) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer cur.Close(context.TODO()) | |
| var ids []int64 | |
| for cur.Next(context.TODO()) { | |
| var u User | |
| if err := cur.Decode(&u); err == nil { | |
| ids = append(ids, u.ID) | |
| } | |
| } | |
| return ids, nil | |
| } | |
| // GetBotSettingsAdminsEnv returns the env-seeded admins (stored in config). | |
| // This is just a passthrough so plugins don't need to import config directly. | |
| func (db *Database) GetBotSettingsAdminsEnv() []int64 { | |
| return config.Admins | |
| } | |
| // AddUserAndCheck adds a user and returns true if they were new (not in DB before). | |
| func (db *Database) AddUserAndCheck(userid int64) bool { | |
| filter := User{ID: userid} | |
| res := db.Ucol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: userid}}) | |
| if res.Err() == mongo.ErrNoDocuments { | |
| db.Ucol.InsertOne(context.TODO(), filter) | |
| return true // new user | |
| } | |
| return false | |
| } | |
| // CleanGhostFilters removes bad "reference" type docs and channel-msg-style text docs. | |
| // Returns the number of documents deleted. | |
| func (db *Database) CleanGhostFilters() int64 { | |
| res1, _ := db.Mcol.DeleteMany(context.TODO(), bson.D{{Key: "mediaType", Value: "reference"}}) | |
| // Also clean docs where text looks like "channel_-100xxx_msg_xx" | |
| res2, _ := db.Mcol.DeleteMany(context.TODO(), bson.D{ | |
| {Key: "text", Value: bson.D{{Key: "$regex", Value: `^channel_-\d+_msg_\d+$`}}}, | |
| }) | |
| count := int64(0) | |
| if res1 != nil { | |
| count += res1.DeletedCount | |
| } | |
| if res2 != nil { | |
| count += res2.DeletedCount | |
| } | |
| return count | |
| } | |
| // randShortID generates a random alphanumeric string of given length. | |
| // Safe for Telegram deep link start parameters (max 64 chars). | |
| func randShortID(n int) string { | |
| const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" | |
| b := make([]byte, n) | |
| seed := time.Now().UnixNano() | |
| for i := range b { | |
| seed = seed*6364136223846793005 + 1442695040888963407 | |
| b[i] = chars[((seed>>33)&0x7fffffffffffffff)%int64(len(chars))] | |
| } | |
| return string(b) | |
| } | |
| // SaveMfilterByFileID saves a filter with a short random _id (safe for Telegram start params). | |
| // Deduplication is done by checking the 'file' (telegram file_id) field to avoid double-indexing. | |
| // Returns true if a new document was inserted, false if it already existed. | |
| func (db *Database) SaveMfilterByFileID(data *Filter) (bool, error) { | |
| // Check if this telegram file_id already exists | |
| existing := db.Mcol.FindOne(context.TODO(), bson.D{{Key: "file", Value: data.FileID}}) | |
| if existing.Err() == nil { | |
| return false, nil // already indexed | |
| } | |
| // Generate a short random _id safe for Telegram deep links (max 64 chars with "file_" prefix = 59 chars) | |
| data.ID = randShortID(15) | |
| _, err := db.Mcol.InsertOne(context.TODO(), data) | |
| if err != nil { | |
| fmt.Printf("db.SaveMfilterByFileID: %v\n", err) | |
| return false, err | |
| } | |
| return true, nil | |
| } | |
| // MigrateLongIDs finds documents whose _id is a long Telegram file_id (>20 chars) | |
| // and re-inserts them with a short random _id (safe for Telegram callback_data, | |
| // which has a 64-byte total limit including the "file()" wrapper). | |
| // Returns the number of documents migrated. | |
| func (db *Database) MigrateLongIDs() (int, error) { | |
| // Find all docs in the global filter namespace with a long _id | |
| cur, err := db.Mcol.Find(context.TODO(), bson.D{ | |
| {Key: "group_id", Value: 101}, | |
| }) | |
| if err != nil { | |
| return 0, err | |
| } | |
| defer cur.Close(context.TODO()) | |
| migrated := 0 | |
| for cur.Next(context.TODO()) { | |
| var f Filter | |
| if err := cur.Decode(&f); err != nil { | |
| continue | |
| } | |
| if len(f.ID) <= 20 { | |
| continue // already short, skip | |
| } | |
| oldID := f.ID | |
| newID := randShortID(15) | |
| // Insert with new short _id | |
| f.ID = newID | |
| if _, err := db.Mcol.InsertOne(context.TODO(), f); err != nil { | |
| continue | |
| } | |
| // Delete the old long-_id document | |
| if _, err := db.Mcol.DeleteOne(context.TODO(), bson.D{{Key: "_id", Value: oldID}}); err != nil { | |
| fmt.Printf("MigrateLongIDs: failed to delete old doc %s: %v\n", oldID, err) | |
| continue | |
| } | |
| migrated++ | |
| } | |
| return migrated, nil | |
| } | |