// Auto-index: listens to configured channels and saves file posts as searchable filters. package plugins import ( "fmt" "regexp" "strconv" "strings" "github.com/Jisin0/Go-Filter-Bot/database" "github.com/Jisin0/Go-Filter-Bot/utils" "github.com/PaulSonOfLars/gotgbot/v2" "github.com/PaulSonOfLars/gotgbot/v2/ext" ) // cleanNameRegex: minimal, non-destructive cleaning for the searchable 'text' keyword. // VJ-style: normalise separators only — don't strip quality/codec words, since // search (searchVJ) does fuzzy regex matching against file_name anyway. // Previous greedy regexes (`@[a-zA-Z0-9_]+` + `(?:...).* `) could consume the // ENTIRE filename when underscores were used as separators — fixed here. var ( extRegex = regexp.MustCompile(`\.\w{2,4}$`) leadingAtRx = regexp.MustCompile(`^@+`) separatorRx = regexp.MustCompile(`[\x{00A0}_.\-]+`) // underscore, dot, dash, NBSP → space bracketRx = regexp.MustCompile(`[\[\](){}]`) multiSpaceRx = regexp.MustCompile(`\s{2,}`) ) // cleanFileName turns a raw file name into a searchable keyword. // Minimal/non-destructive: normalises separators, strips a leading "@", keeps content. func cleanFileName(name string) string { name = extRegex.ReplaceAllString(name, "") // remove file extension name = separatorRx.ReplaceAllString(name, " ") // underscores/dots/dashes/NBSP → space name = bracketRx.ReplaceAllString(name, " ") // remove bracket characters name = leadingAtRx.ReplaceAllString(name, "") // strip leading @ name = multiSpaceRx.ReplaceAllString(name, " ") name = strings.TrimSpace(name) return strings.ToLower(name) } // isIndexChannel checks whether a channel ID is in the configured index list. func isIndexChannel(chanID int64) bool { s := database.GetBotSettings() for _, id := range s.IndexChannels { if id == chanID { return true } } return false } // AutoIndex handles channel posts and saves media as global filters. // Supports both ctx.ChannelPost (native channel updates) and ctx.Message (forwarded/other). // //nolint:errcheck func AutoIndex(bot *gotgbot.Bot, ctx *ext.Context) error { // Prefer ChannelPost, fall back to Message msg := ctx.ChannelPost if msg == nil { msg = ctx.Message } if msg == nil { return nil } chanID := msg.Chat.Id // Only index from configured channels if !isIndexChannel(chanID) { fmt.Printf("auto-index: channel %d not in index list, skipping\n", chanID) return nil } fmt.Printf("auto-index: received post from channel %d\n", chanID) var ( fileID string mediaType string rawName string fileSize int64 mimeType string ) switch { case msg.Document != nil: fileID = msg.Document.FileId mediaType = "document" rawName = msg.Document.FileName fileSize = msg.Document.FileSize mimeType = msg.Document.MimeType case msg.Video != nil: fileID = msg.Video.FileId mediaType = "video" rawName = msg.Video.FileName fileSize = msg.Video.FileSize mimeType = msg.Video.MimeType if rawName == "" { rawName = msg.Caption } case msg.Audio != nil: fileID = msg.Audio.FileId mediaType = "audio" rawName = msg.Audio.Title fileSize = msg.Audio.FileSize mimeType = msg.Audio.MimeType if rawName == "" { rawName = msg.Audio.FileName } default: fmt.Printf("auto-index: non-media post in channel %d, skipping\n", chanID) return nil } if rawName == "" && msg.Caption != "" { rawName = msg.Caption } if fileID == "" { fmt.Printf("auto-index: no fileID in channel %d, skipping\n", chanID) return nil } if rawName == "" { // Use file_id as fallback name (rare case) rawName = fileID[:min(20, len(fileID))] } keyword := cleanFileName(rawName) if len(keyword) < 2 { fmt.Printf("auto-index: keyword too short for %q, skipping\n", rawName) return nil } caption := "" if msg.Caption != "" { caption = msg.OriginalCaptionHTML() } f := &database.Filter{ ChatID: globalNumber, Text: keyword, FileName: rawName, Content: caption, FileID: fileID, MediaType: mediaType, FileSize: fileSize, MimeType: mimeType, Length: len(keyword), } inserted, err := DB.SaveMfilterByFileID(f) if err != nil { fmt.Printf("auto-index: ❌ save error for %q: %v\n", rawName, err) } else if inserted { fmt.Printf("auto-index: ✅ indexed %q (type=%s) from channel %d\n", rawName, mediaType, chanID) } else { fmt.Printf("auto-index: ⏭ duplicate skipped %q from channel %d\n", rawName, chanID) } return nil } // min helper for int func min(a, b int) int { if a < b { return a } return b } // IndexNow handles the /index command – manually triggers indexing of a replied message. // //nolint:errcheck func IndexNow(bot *gotgbot.Bot, ctx *ext.Context) error { if !utils.IsAdmin(ctx.EffectiveUser.Id) { ctx.EffectiveMessage.Reply(bot, "⛔ Only bot admins can use this command!", &gotgbot.SendMessageOpts{}) return nil } msg := ctx.Message if msg.ReplyToMessage == nil { msg.Reply(bot, "Reply to a media message to index it.", &gotgbot.SendMessageOpts{}) return nil } target := msg.ReplyToMessage var ( fileID string mediaType string rawName string ) switch { case target.Document != nil: fileID = target.Document.FileId mediaType = "document" rawName = target.Document.FileName case target.Video != nil: fileID = target.Video.FileId mediaType = "video" rawName = target.Video.FileName case target.Audio != nil: fileID = target.Audio.FileId mediaType = "audio" rawName = target.Audio.Title default: msg.Reply(bot, "❌ No supported media found in the replied message.", &gotgbot.SendMessageOpts{}) return nil } if rawName == "" { rawName = target.Caption } // Allow custom keyword as argument args := strings.SplitN(msg.Text, " ", 2) if len(args) >= 2 && strings.TrimSpace(args[1]) != "" { rawName = strings.TrimSpace(args[1]) } keyword := cleanFileName(rawName) if keyword == "" { msg.Reply(bot, "❌ Could not determine a keyword. Provide one: /index keyword", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil } caption := "" if target.Caption != "" { caption = target.OriginalCaptionHTML() } var fileSize int64 var mimeType string var origName string switch { case target.Document != nil: fileSize = target.Document.FileSize mimeType = target.Document.MimeType origName = target.Document.FileName case target.Video != nil: fileSize = target.Video.FileSize mimeType = target.Video.MimeType origName = target.Video.FileName case target.Audio != nil: fileSize = target.Audio.FileSize mimeType = target.Audio.MimeType origName = target.Audio.Title } if origName == "" { origName = rawName } f := &database.Filter{ ChatID: globalNumber, Text: keyword, FileName: origName, Content: caption, FileID: fileID, MediaType: mediaType, FileSize: fileSize, MimeType: mimeType, Length: len(keyword), } inserted, err := DB.SaveMfilterByFileID(f) if err != nil { msg.Reply(bot, fmt.Sprintf("❌ Save error: %v", err), &gotgbot.SendMessageOpts{}) return nil } if !inserted { msg.Reply(bot, fmt.Sprintf("⚠️ %s already indexed (duplicate file).", origName), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil } msg.Reply(bot, fmt.Sprintf("✅ Indexed: %s\nKeyword: %s", origName, keyword), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}, ) return nil } // AddIndexCmd handles the /addindex command to add a channel to auto-index // //nolint:errcheck func AddIndexCmd(bot *gotgbot.Bot, ctx *ext.Context) error { if !utils.IsAdmin(ctx.EffectiveUser.Id) { ctx.EffectiveMessage.Reply(bot, "⛔ Only bot admins can use this command!", &gotgbot.SendMessageOpts{}) return nil } args := strings.Split(ctx.Message.Text, " ") if len(args) < 2 { ctx.Message.Reply(bot, "Usage: /addindex <channel_id>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil } chanID, err := strconv.ParseInt(args[1], 10, 64) if err != nil { ctx.Message.Reply(bot, "❌ Invalid channel ID.", &gotgbot.SendMessageOpts{}) return nil } if err := DB.AddIndexChannel(chanID); err != nil { ctx.Message.Reply(bot, fmt.Sprintf("❌ Failed to add: %v", err), &gotgbot.SendMessageOpts{}) return nil } ctx.Message.Reply(bot, fmt.Sprintf("✅ Channel %d added to Auto-Index list.", chanID), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil } // DelIndexCmd handles the /delindex command to remove a channel from auto-index // //nolint:errcheck func DelIndexCmd(bot *gotgbot.Bot, ctx *ext.Context) error { if !utils.IsAdmin(ctx.EffectiveUser.Id) { ctx.EffectiveMessage.Reply(bot, "⛔ Only bot admins can use this command!", &gotgbot.SendMessageOpts{}) return nil } args := strings.Split(ctx.Message.Text, " ") if len(args) < 2 { ctx.Message.Reply(bot, "Usage: /delindex <channel_id>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil } chanID, err := strconv.ParseInt(args[1], 10, 64) if err != nil { ctx.Message.Reply(bot, "❌ Invalid channel ID.", &gotgbot.SendMessageOpts{}) return nil } if err := DB.RemoveIndexChannel(chanID); err != nil { ctx.Message.Reply(bot, fmt.Sprintf("❌ Failed to remove: %v", err), &gotgbot.SendMessageOpts{}) return nil } ctx.Message.Reply(bot, fmt.Sprintf("✅ Channel %d removed from Auto-Index list.", chanID), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) return nil }