// (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
}