// (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 = ` ╭ ▸ Users : %v ├ ▸ Filters : %v ├ ▸ Groups : %v ╰ ▸ Banned : %v ` //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• %v", 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 }