Go-Filter-Bot / plugins /index.go
Thaimozhi's picture
Auto index concept update
71db04d
Raw History Blame Contribute Delete
9.63 kB
// 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: <code>/index keyword</code>", &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("⚠️ <code>%s</code> already indexed (duplicate file).", origName), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
return nil
}
msg.Reply(bot,
fmt.Sprintf("βœ… Indexed: <code>%s</code>\nKeyword: <code>%s</code>", 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: <code>/addindex &lt;channel_id&gt;</code>", &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 <code>%d</code> 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: <code>/delindex &lt;channel_id&gt;</code>", &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 <code>%d</code> removed from Auto-Index list.", chanID), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
return nil
}