Spaces:
Sleeping
Sleeping
Download plugins/filter.go from Thaimozhi/Go-Filter-Bot: direct link, hf CLI and curl.
- Browser
- Download file 30.3 kB
-
https://huggingface.co/spaces/Thaimozhi/Go-Filter-Bot/resolve/main/plugins/filter.go
- Command line
-
hf download hf://spaces/Thaimozhi/Go-Filter-Bot/plugins/filter.go
-
curl -L -o filter.go https://huggingface.co/spaces/Thaimozhi/Go-Filter-Bot/resolve/main/plugins/filter.go
30.3 kB
| // (c) Jisin0 | |
| package plugins | |
| import ( | |
| "context" | |
| "fmt" | |
| "regexp" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "github.com/Jisin0/Go-Filter-Bot/database" | |
| "github.com/Jisin0/Go-Filter-Bot/utils" | |
| "github.com/Jisin0/Go-Filter-Bot/utils/autodelete" | |
| "github.com/Jisin0/Go-Filter-Bot/utils/customfilters" | |
| "github.com/Jisin0/Go-Filter-Bot/utils/shortener" | |
| "github.com/PaulSonOfLars/gotgbot/v2" | |
| "github.com/PaulSonOfLars/gotgbot/v2/ext" | |
| "go.mongodb.org/mongo-driver/bson" | |
| "go.mongodb.org/mongo-driver/mongo/options" | |
| ) | |
| // Regex expressions for callbacks and filtering | |
| var ( | |
| buttonRegex *regexp.Regexp = regexp.MustCompile(`\[([^\[]+?)\]\((buttonurl|url|alert):(?:/{0,2})(.+?)\)`) | |
| parseRegex *regexp.Regexp = regexp.MustCompile(`^"([^"]+)"`) | |
| cbstopRegex *regexp.Regexp = regexp.MustCompile(`stopf\((.+)\)`) | |
| cbalertRegex *regexp.Regexp = regexp.MustCompile(`alert\((.+)\)`) | |
| cbfileRegex *regexp.Regexp = regexp.MustCompile(`file\((.+)\)`) | |
| usernameRegex *regexp.Regexp = regexp.MustCompile(`(?i)@[a-z0-9_]+`) | |
| urlRegex *regexp.Regexp = regexp.MustCompile(`(?i)https?://\S+|t\.me/\S+|www\.\S+`) | |
| ) | |
| // cleanCaption removes usernames, URLs/links, and cleans up extra spaces/symbols left behind | |
| func cleanCaption(raw string) string { | |
| if raw == "" { | |
| return "" | |
| } | |
| s := usernameRegex.ReplaceAllString(raw, "") | |
| s = urlRegex.ReplaceAllString(s, "") | |
| // Clean up orphaned separators like "& -", " -", " - ", etc. | |
| s = regexp.MustCompile(`[\s\-\&\|]+\s*-\s*`).ReplaceAllString(s, " - ") | |
| s = regexp.MustCompile(`^\s*[\-\&\|\:]+\s*`).ReplaceAllString(s, "") | |
| s = regexp.MustCompile(`\s{2,}`).ReplaceAllString(s, " ") | |
| return strings.TrimSpace(s) | |
| } | |
| var DB *database.Database = database.NewDatabase() | |
| const ( | |
| lenUniqueID = 15 // length of unique id string for filters | |
| globalNumber int64 = 101 // Number used as chat id for global filters. | |
| maxKeyLength = 20 // maximum length for a keyword of a filter | |
| filterSplitCount = 3 // number of subtrings into which input of /filter command should be split | |
| minButtonParseParams = 4 // Parse of a button should yield atleast this many values | |
| alertCacheDuration = 3000 // number of seconds an alert should be cache by the client | |
| ) | |
| const ( | |
| cbStopParamCount = 3 // number of parameters required for cbstop | |
| maxButttons = 5 // maximum number of buttons to scan for | |
| ) | |
| // โโ Filter result cache โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // filterCacheEntry holds cached search results with a TTL expiry. | |
| type filterCacheEntry struct { | |
| results []*database.Filter | |
| expiry time.Time | |
| } | |
| const filterCacheTTL = 10 * time.Minute | |
| // searchWithCache wraps DB search calls with in-memory caching. | |
| func searchWithCache(chatID int64, fields []string, message string, multiFilter bool) []*database.Filter { | |
| var cacheKey string | |
| useNew := len(fields) <= 15 | |
| if useNew { | |
| cacheKey = database.FilterCacheKey(chatID, fields) | |
| } else { | |
| cacheKey = database.FilterCacheKey(chatID, []string{message}) | |
| } | |
| if cached, ok := database.GetCachedFilterResults(cacheKey); ok { | |
| return cached | |
| } | |
| var results []*database.Filter | |
| if useNew { | |
| results = DB.SearchMfilterNew(chatID, fields, multiFilter) | |
| } else { | |
| results = DB.SearchMfilterClassic(chatID, message) | |
| } | |
| database.SetCachedFilterResults(cacheKey, results) | |
| return results | |
| } | |
| // โโ Manual filter handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| func sendSearchResults(results []*database.Filter, bot *gotgbot.Bot, update *gotgbot.Message, chatID int64, messageID int64) { | |
| if len(results) == 0 { | |
| return | |
| } | |
| s := database.GetBotSettings() | |
| // Just limit to 10 for now | |
| if len(results) > 10 { | |
| results = results[:10] | |
| } | |
| if s.SearchAsButton { | |
| var rows [][]gotgbot.InlineKeyboardButton | |
| for _, f := range results { | |
| btnText := strings.SplitN(f.Content, "\n", 2)[0] | |
| btnText = usernameRegex.ReplaceAllString(btnText, "") | |
| btnText = strings.TrimSpace(btnText) | |
| if btnText == "" { | |
| btnText = f.Text | |
| } | |
| if len(btnText) > 60 { | |
| btnText = btnText[:57] + "..." | |
| } | |
| rows = append(rows, []gotgbot.InlineKeyboardButton{{ | |
| Text: "๐ " + btnText, | |
| CallbackData: fmt.Sprintf("file(%s)", f.ID), | |
| }}) | |
| } | |
| text := fmt.Sprintf("<b>Here are the results found (%d):</b>\n<i>Click the buttons below to get the file.</i>", len(results)) | |
| m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{ | |
| ParseMode: gotgbot.ParseModeHTML, | |
| ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: rows}, | |
| ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, | |
| }) | |
| if err == nil && m != nil && s.AutoDelete > 0 { | |
| autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60) | |
| } | |
| } else { | |
| botUser := bot.User.Username | |
| text := fmt.Sprintf("<b>Here are the results found (%d):</b>\n\n", len(results)) | |
| for i, f := range results { | |
| btnText := strings.SplitN(f.Content, "\n", 2)[0] | |
| btnText = usernameRegex.ReplaceAllString(btnText, "") | |
| btnText = strings.TrimSpace(btnText) | |
| if btnText == "" { | |
| btnText = f.Text | |
| } | |
| link := fmt.Sprintf("https://t.me/%s?start=file_%s", botUser, f.ID) | |
| text += fmt.Sprintf("%d. <a href='%s'>%s</a>\n", i+1, link, btnText) | |
| } | |
| text += "\n<i>Click the link above to get the file in PM.</i>" | |
| m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{ | |
| ParseMode: gotgbot.ParseModeHTML, | |
| ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, | |
| LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true}, | |
| }) | |
| if err == nil && m != nil && s.AutoDelete > 0 { | |
| autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60) | |
| } | |
| } | |
| } | |
| // MFilter handles automatic replies for manual filters. | |
| func MFilter(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| update := ctx.Message | |
| chatType := update.Chat.Type | |
| // Group/supergroup search is now handled by Filter() (VJ-style with "Searching..." indicator) | |
| // MFilter only handles PM search via connected group | |
| if chatType != gotgbot.ChatTypePrivate { | |
| return nil | |
| } | |
| chatID, ok := DB.GetConnection(update.Chat.Id) | |
| if !ok { | |
| return nil | |
| } | |
| message := update.Text | |
| if update.Caption != "" { | |
| message = update.Caption | |
| } | |
| fields := strings.Fields(message) | |
| s := database.GetBotSettings() | |
| results := searchWithCache(chatID, fields, message, s.MultiFilter) | |
| if len(results) > 0 { | |
| sendSearchResults(results, bot, update, chatID, update.MessageId) | |
| } | |
| return nil | |
| } | |
| // Function to handle filter and gfilter commands | |
| // | |
| //nolint:errcheck // too many | |
| func NewFilter(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| var ( | |
| c int64 | |
| update = ctx.Message | |
| ) | |
| // I didnt wanna create a whole new function for gfilter so ... | |
| if strings.HasPrefix(update.Text, "/gfilter") { | |
| if !utils.IsAdmin(ctx.EffectiveUser.Id) { | |
| update.Reply(bot, "Thats an Admin-only Command :(", &gotgbot.SendMessageOpts{}) | |
| return nil | |
| } | |
| c = globalNumber | |
| } else { | |
| // Verifying and getting connections for private chats | |
| var v bool | |
| c, v = customfilters.Verify(bot, ctx) | |
| if !v { | |
| return nil | |
| } else if c == 0 { | |
| c = ctx.Message.Chat.Id | |
| } | |
| } | |
| args := strings.SplitN(update.Text, " ", filterSplitCount) | |
| if len(args) < 2 && (update.ReplyToMessage == nil && len(args) < filterSplitCount) { | |
| update.Reply( | |
| bot, | |
| "Not Enough Parameters :(\n\nExample:- <code>/filter hi hello</code>", | |
| &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}, | |
| ) | |
| return nil | |
| } | |
| parse := ParseQuotes(strings.SplitN(update.Text, " ", 2)[1]) | |
| key := strings.ToLower(parse[0]) | |
| if len(key) > maxKeyLength { | |
| update.Reply(bot, fmt.Sprintf("Sorry The Length of the Key Can't be More than %d Characters !\nInput Key: <code>%s</code>", maxKeyLength, key), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) | |
| } | |
| e := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "group_id", Value: c}, {Key: "text", Value: key}}) | |
| if e.Err() == nil { | |
| update.Reply( | |
| bot, | |
| fmt.Sprintf("It Looks Like Another Filter For <code>%v</code> Has Already Been Saved In This Chat, If You Want To Stop It First Use The Button Below", key), | |
| &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{ | |
| {{Text: "Stop Filter", CallbackData: fmt.Sprintf("stopf(%v|%v|y)", key, "local")}}, | |
| }}}, | |
| ) | |
| return nil | |
| } | |
| var ( | |
| text = parse[1] | |
| button [][]map[string]string | |
| ) | |
| if update.ReplyToMessage != nil { | |
| text += update.ReplyToMessage.OriginalHTML() | |
| text += update.ReplyToMessage.OriginalCaptionHTML() | |
| if update.ReplyToMessage.ReplyMarkup != nil { | |
| // Buttons are converted to maps to save to the database | |
| button = buttonToMap(ctx.Message.ReplyToMessage.ReplyMarkup.InlineKeyboard) | |
| } | |
| } | |
| uniqueID := utils.RandString(lenUniqueID) | |
| text, button, alert := parseButtons(text, uniqueID, button) | |
| // Finding media if any | |
| var ( | |
| fileID string | |
| mediaType string | |
| ) | |
| if msg := update.ReplyToMessage; msg != nil { | |
| switch { | |
| case msg.Document != nil: | |
| fileID = msg.Document.FileId | |
| mediaType = "document" | |
| case msg.Video != nil: | |
| fileID = msg.Video.FileId | |
| mediaType = "video" | |
| case msg.Audio != nil: | |
| fileID = msg.Audio.FileId | |
| mediaType = "audio" | |
| case msg.Sticker != nil: | |
| fileID = msg.Sticker.FileId | |
| mediaType = "sticker" | |
| case msg.Animation != nil: | |
| fileID = msg.Animation.FileId | |
| mediaType = "animation" | |
| case msg.Photo != nil: | |
| fileID = msg.Photo[len(msg.Photo)-1].FileId | |
| mediaType = "photo" | |
| } | |
| } | |
| f := &database.Filter{ | |
| ID: uniqueID, | |
| ChatID: c, | |
| Text: key, | |
| Content: text, | |
| FileID: fileID, | |
| Markup: button, | |
| Alerts: alert, | |
| Length: len(key), | |
| MediaType: mediaType, | |
| } | |
| DB.SaveMfilter(f) | |
| _, err := update.Reply( | |
| bot, | |
| fmt.Sprintf("<i>Successfully Saved A Manual Filter For <code>%v</code> !</i>", key), | |
| &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}, | |
| ) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| return nil | |
| } | |
| func CbStop(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| // Function to handle callbacks from confirm buttons when stopping a filter | |
| update := ctx.CallbackQuery | |
| c, v := customfilters.Verify(bot, ctx) | |
| if !v { | |
| return nil | |
| } else if c == 0 { | |
| c = update.Message.GetChat().Id | |
| } | |
| // Making sure the callback data is valid | |
| args := strings.Split(cbstopRegex.FindStringSubmatch(update.Data)[1], "|") | |
| if len(args) < cbStopParamCount { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Button :(", ShowAlert: true}) | |
| return nil | |
| } | |
| var ( | |
| key = args[0] | |
| ftype = args[1] | |
| erase = args[2] | |
| ) | |
| if ftype == "local" { | |
| if erase == "y" { | |
| DB.DeleteMfilter(c, key) | |
| update.Message.EditText(bot, fmt.Sprintf("Manual Filter For <code>%v</code> Was Deleted Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML}) | |
| } else if erase == "n" { | |
| update.Message.EditText(bot, fmt.Sprintf(`Are You Sure You Want To Permanently Delete The Manual Filter For %v ?\nClick The "Yes I'm Sure" Button To Confirm `, key), &gotgbot.EditMessageTextOpts{ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "Ignore", CallbackData: "close"}, {Text: "Yes I'm Sure", CallbackData: fmt.Sprintf("stopf(%v|local|y)", key)}}}}, ParseMode: gotgbot.ParseModeHTML}) | |
| } | |
| } else if ftype == "global" { | |
| DB.StopGfilter(c, key) | |
| update.Message.EditText(bot, fmt.Sprintf("Global Filter For <code>%v</code> Has Been Stopped Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML}) | |
| } | |
| return nil | |
| } | |
| // Function to handle the stop command | |
| // | |
| //nolint:errcheck // too many | |
| func StopMfilter(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| c, v := customfilters.Verify(bot, ctx) | |
| if !v { | |
| return nil | |
| } else if c == 0 { | |
| c = ctx.Message.Chat.Id | |
| } | |
| var ( | |
| update = ctx.Message | |
| split = strings.SplitN(update.Text, " ", 2) | |
| key string | |
| ) | |
| switch { | |
| case len(split) < 2 && update.Chat.Type == gotgbot.ChatTypePrivate: | |
| m := utils.Ask(bot, "Ok Now Send Me The Name OF The Filter You Would Like To Stop ...", ctx.EffectiveChat, ctx.EffectiveUser) | |
| if m == nil { | |
| return nil | |
| } | |
| key = m.Text | |
| case len(split) < 2: | |
| update.Reply(bot, "Whoops looks like you forgot to mention a filter to stop !", &gotgbot.SendMessageOpts{}) | |
| default: | |
| key = split[1] | |
| } | |
| // Checking if theres a local/global filter for the key | |
| _, k := DB.GetMfilter(c, key) | |
| _, ok := DB.GetMfilter(globalNumber, key) | |
| // If there isnt local or global | |
| if !k && !ok { | |
| update.Reply(bot, fmt.Sprintf("I Couldnt Find Any Filter For <code>%v</code> To Stop !", key), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) | |
| return nil | |
| } | |
| // Both local and global | |
| switch { | |
| case k && ok: | |
| markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "Local", CallbackData: fmt.Sprintf("stopf(%v|local|y)", key)}, {Text: "Global", CallbackData: fmt.Sprintf("stopf(%v|global|y)", key)}}}} | |
| _, err := update.Reply(bot, "Please Select If You Would Like To Stop The Manual Filter (which you saved) Or Global Filter (saved by owners) For <code>"+key+"</code>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: markup}) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| case k: | |
| // Only local | |
| markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|local|y)", key)}}}} | |
| _, err := update.Reply(bot, "Please Press The Button Below To Confirm Deletion Of Manual Filter For <code>"+key+"</code>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: markup}) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| case ok: | |
| // Only global | |
| markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|global|y)", key)}}}} | |
| _, err := update.Reply(bot, "Please Press The Button Below To Stop Global Filter For <code>"+key+"</code>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: markup}) | |
| if err != nil { | |
| fmt.Println(err) | |
| } | |
| } | |
| return nil | |
| } | |
| func AllMfilters(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| // Function to handle the /filters command | |
| update := ctx.Message | |
| c := update.Chat.Id | |
| if update.Chat.Type == gotgbot.ChatTypePrivate { | |
| if i, k := DB.GetConnection(update.From.Id); k { | |
| c = i | |
| } | |
| } | |
| text := DB.StringMfilter(c) | |
| _, err := update.Reply(bot, "Lษชsแด แดา าษชสแดแดสs าแดส แดสษชs แดสแดแด :\n"+text, &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML}) | |
| if err != nil { | |
| fmt.Printf("allmfilters: %v\n", err) | |
| } | |
| return nil | |
| } | |
| // Function to handle callbacks from alert button in saved filters | |
| // | |
| //nolint:errcheck // unnecessary | |
| func CbAlert(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| update := ctx.CallbackQuery | |
| args := strings.Split(cbalertRegex.FindStringSubmatch(update.Data)[1], "|") | |
| if len(args) < 2 { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true}) | |
| } else { | |
| uniqueID := args[0] | |
| index, err := strconv.Atoi(args[1]) | |
| if err != nil { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true}) | |
| } else { | |
| text := DB.GetAlert(uniqueID, index) | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: text, ShowAlert: true, CacheTime: alertCacheDuration}) | |
| } | |
| } | |
| return nil | |
| } | |
| // SendFileByID finds a filter by ID and sends it. | |
| func SendFileByID(fileID string, bot *gotgbot.Bot, update *gotgbot.Message) { | |
| res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileID}}) | |
| if res.Err() != nil { | |
| fmt.Printf("SendFileByID: file %q not found in DB: %v\n", fileID, res.Err()) | |
| update.Reply(bot, "โ File not found. It may have been deleted or the link is invalid.", &gotgbot.SendMessageOpts{ //nolint:errcheck | |
| ReplyParameters: &gotgbot.ReplyParameters{MessageId: update.MessageId, AllowSendingWithoutReply: true}, | |
| }) | |
| return | |
| } | |
| var f database.Filter | |
| if err := res.Decode(&f); err != nil { | |
| fmt.Printf("SendFileByID: decode error for %q: %v\n", fileID, err) | |
| return | |
| } | |
| if err := sendFilter(&f, bot, update.Chat.Id, update.MessageId, update.Chat.Type); err != nil { | |
| update.Reply(bot, "โ Failed to send file. Please try again or contact admin.", &gotgbot.SendMessageOpts{ //nolint:errcheck | |
| ReplyParameters: &gotgbot.ReplyParameters{MessageId: update.MessageId, AllowSendingWithoutReply: true}, | |
| }) | |
| } | |
| } | |
| // CbSendFile handles inline button clicks to send a specific file. | |
| //nolint:errcheck | |
| func CbSendFile(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| update := ctx.CallbackQuery | |
| if !CheckForceSub(bot, ctx) { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "โ ๏ธ Please join the required channel(s) first!", ShowAlert: true}) | |
| return nil | |
| } | |
| match := cbfileRegex.FindStringSubmatch(update.Data) | |
| if len(match) < 2 { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Invalid file button!", ShowAlert: true}) | |
| return nil | |
| } | |
| fileDocID := match[1] | |
| fmt.Printf("CbSendFile: lookup _id=%q\n", fileDocID) | |
| res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileDocID}}) | |
| if res.Err() != nil { | |
| fmt.Printf("CbSendFile: file %q not found: %v\n", fileDocID, res.Err()) | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "โ File not found. It may have been removed.", ShowAlert: true}) | |
| return nil | |
| } | |
| var f database.Filter | |
| if err := res.Decode(&f); err != nil { | |
| fmt.Printf("CbSendFile: decode error: %v\n", err) | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "โ Error loading file.", ShowAlert: true}) | |
| return nil | |
| } | |
| chatID := update.Message.GetChat().Id | |
| msgID := update.Message.GetMessageId() | |
| chatType := update.Message.GetChat().Type | |
| if err := sendFilter(&f, bot, chatID, msgID, chatType); err != nil { | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "โ Failed to send file: " + err.Error(), ShowAlert: true}) | |
| return nil | |
| } | |
| update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "โ Sent! Check below or your PM."}) | |
| return nil | |
| } | |
| func buttonToMap(btn [][]gotgbot.InlineKeyboardButton) [][]map[string]string { | |
| // Convert a button into a map | |
| var ( | |
| totalButtons [][]map[string]string | |
| rowButtons []map[string]string | |
| ) | |
| for _, i := range btn { | |
| rowButtons = []map[string]string{} | |
| for _, j := range i { | |
| b := map[string]string{"Text": j.Text} | |
| switch { | |
| case j.CallbackData != "": | |
| b["CallbackData"] = j.CallbackData | |
| case j.Url != "": | |
| b["Url"] = j.Url | |
| default: | |
| continue | |
| } | |
| rowButtons = append(rowButtons, b) | |
| } | |
| totalButtons = append(totalButtons, rowButtons) | |
| } | |
| return totalButtons | |
| } | |
| func mapToButton(data [][]map[string]string) [][]gotgbot.InlineKeyboardButton { | |
| // Convert a map back into button | |
| var totalButtons [][]gotgbot.InlineKeyboardButton | |
| for _, i := range data { | |
| var rowButtons []gotgbot.InlineKeyboardButton | |
| for _, b := range i { | |
| text := b["Text"] | |
| if u, k := b["Url"]; k { | |
| rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, Url: u}) | |
| } else if c, k := b["CallbackData"]; k { | |
| rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, CallbackData: c}) | |
| } | |
| } | |
| totalButtons = append(totalButtons, rowButtons) | |
| } | |
| return totalButtons | |
| } | |
| func ParseQuotes(text string) []string { | |
| res := parseRegex.FindStringSubmatch(text) | |
| if len(res) > 0 { | |
| return []string{res[1], strings.Replace(text, "\""+res[1]+"\"", "", 1)} // very hacky workaround by adding quotes //TODO: update regex to include quotes | |
| } else { | |
| split := strings.SplitN(text, " ", 2) | |
| return []string{split[0], strings.Replace(text, split[0], "", 1)} | |
| } | |
| } | |
| func parseButtons(text, uniqueID string, totalButtons [][]map[string]string) (messageText string, buttons [][]map[string]string, alertText []string) { | |
| var ( | |
| returnText = text | |
| rowButtons []map[string]string | |
| alert []string | |
| ) | |
| for _, rows := range strings.Split(text, "\n") { | |
| for _, m := range buttonRegex.FindAllStringSubmatch(rows, maxButttons) { | |
| if len(m) < minButtonParseParams { | |
| continue | |
| } | |
| if m[2] == "url" || m[2] == "buttonurl" { | |
| rowButtons = append(rowButtons, map[string]string{"Text": m[1], "Url": m[3]}) | |
| } else if m[2] == "alert" || m[2] == "buttonalert" { | |
| alert = append(alert, m[3]) | |
| rowButtons = append(rowButtons, map[string]string{"Text": m[1], "CallbackData": fmt.Sprintf("alert(%v|%v)", uniqueID, len(alert)-1)}) | |
| } | |
| returnText = strings.Replace(returnText, m[0], "", 1) | |
| } | |
| if len(rowButtons) > 0 { | |
| totalButtons = append(totalButtons, rowButtons) | |
| } | |
| rowButtons = []map[string]string{} | |
| } | |
| return strings.Trim(returnText, " "), totalButtons, alert | |
| } | |
| // sendFilter sends the data from filter f to the chatID. | |
| // URL buttons are shortened via the configured shortener service (if any). | |
| // Returns an error if sending failed (so callers can show user feedback). | |
| func sendFilter(f *database.Filter, bot *gotgbot.Bot, chatID, messageID int64, chatType string) error { | |
| // Shorten URL buttons if a shortener is configured (read from dynamic settings) | |
| markup := f.Markup | |
| s := database.GetBotSettings() | |
| if s.ShortenerAPI != "" && s.ShortenerURL != "" { | |
| markup = shortener.ShortenAll(markup) | |
| } | |
| content := f.Content | |
| // If it's a media filter (indexed file), use the clean filename as caption | |
| if f.MediaType != "" { | |
| if f.FileName != "" { | |
| content = cleanCaption(f.FileName) | |
| } else if content != "" { | |
| content = cleanCaption(content) | |
| } | |
| } else { | |
| content = cleanCaption(content) | |
| } | |
| var ( | |
| buttons = mapToButton(markup) | |
| ikm = gotgbot.InlineKeyboardMarkup{InlineKeyboard: buttons} | |
| err error | |
| m *gotgbot.Message | |
| ) | |
| mediaType := f.MediaType | |
| if mediaType != "" { | |
| fileID := gotgbot.InputFileByID(f.FileID) | |
| switch mediaType { | |
| case "document": | |
| m, err = bot.SendDocument(chatID, fileID, &gotgbot.SendDocumentOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| case "sticker": | |
| m, err = bot.SendSticker(chatID, fileID, &gotgbot.SendStickerOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm}) | |
| case "video": | |
| m, err = bot.SendVideo(chatID, fileID, &gotgbot.SendVideoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| case "photo": | |
| m, err = bot.SendPhoto(chatID, fileID, &gotgbot.SendPhotoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| case "audio": | |
| m, err = bot.SendAudio(chatID, fileID, &gotgbot.SendAudioOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| case "animation": | |
| m, err = bot.SendAnimation(chatID, fileID, &gotgbot.SendAnimationOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| default: | |
| fmt.Println("Unknown media type " + mediaType) | |
| } | |
| } else { | |
| m, err = bot.SendMessage(chatID, content, &gotgbot.SendMessageOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML}) | |
| } | |
| if err != nil { | |
| fmt.Printf("sendFilter: failed to send %q (type=%s, fileID=%s...): %v\n", | |
| f.FileName, f.MediaType, safePrefix(f.FileID, 20), err) | |
| return err | |
| } | |
| if m != nil && s.AutoDelete > 0 { | |
| // Only auto-delete in groups โ PM file sends should not auto-delete | |
| if chatType != gotgbot.ChatTypePrivate { | |
| autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60) //nolint:errcheck | |
| } | |
| } | |
| return nil | |
| } | |
| // safePrefix returns the first n chars of s, or s itself if shorter. | |
| func safePrefix(s string, n int) string { | |
| if len(s) <= n { | |
| return s | |
| } | |
| return s[:n] | |
| } | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // VJ-FILTER-BOT style search โ ported from Python to Go | |
| // Credit: Bot code made by Claude | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // vjCleanQuery replicates VJ bot's query cleaning before search. | |
| // Removes noise words that hurt search accuracy. | |
| func vjCleanQuery(raw string) string { | |
| q := regexp.MustCompile(`(?i)\b(please|pls|plz|send|movie|new|latest|bro|bruh|broh|find|link|file|dubbed|full|helo|that|undo|kitti|thar|kittum|anyone)\b`).ReplaceAllString(strings.ToLower(raw), " ") | |
| q = regexp.MustCompile(`[-:.]`).ReplaceAllString(q, " ") | |
| q = regexp.MustCompile(`\s{2,}`).ReplaceAllString(q, " ") | |
| return strings.TrimSpace(q) | |
| } | |
| // buildVJRegex builds MongoDB regex exactly matching VJ-FILTER-BOT's get_search_results. | |
| // Single word: boundary match. Multi-word: words joined by wildcard separator. | |
| func buildVJRegex(query string) string { | |
| query = strings.TrimSpace(query) | |
| if query == "" { | |
| return "." | |
| } | |
| if !strings.Contains(query, " ") { | |
| return `(\b|[.\+\-_])` + regexp.QuoteMeta(query) + `(\b|[.\+\-_])` | |
| } | |
| parts := strings.Fields(query) | |
| escaped := make([]string, len(parts)) | |
| for i, p := range parts { | |
| escaped[i] = regexp.QuoteMeta(p) | |
| } | |
| return strings.Join(escaped, `.*[\s\.\+\-_]`) | |
| } | |
| // searchVJ searches file_name AND text fields using VJ-style regex. | |
| // This is the core of VJ's high hit ratio. | |
| func searchVJ(chatID int64, rawQuery string, limit int64) []*database.Filter { | |
| query := vjCleanQuery(rawQuery) | |
| if query == "" { | |
| return nil | |
| } | |
| pattern := buildVJRegex(query) | |
| filter := bson.D{ | |
| {Key: "group_id", Value: chatID}, | |
| {Key: "$or", Value: bson.A{ | |
| bson.D{{Key: "file_name", Value: bson.M{"$regex": pattern, "$options": "i"}}}, | |
| bson.D{{Key: "text", Value: bson.M{"$regex": pattern, "$options": "i"}}}, | |
| }}, | |
| {Key: "file", Value: bson.M{"$exists": true, "$ne": ""}}, | |
| } | |
| cur, err := DB.Mcol.Find( | |
| context.Background(), | |
| filter, | |
| options.Find(). | |
| SetSort(bson.D{{Key: "$natural", Value: -1}}). | |
| SetLimit(limit), | |
| ) | |
| if err != nil { | |
| return nil | |
| } | |
| defer cur.Close(context.Background()) | |
| var results []*database.Filter | |
| for cur.Next(context.Background()) { | |
| var f database.Filter | |
| if err := cur.Decode(&f); err == nil { | |
| results = append(results, &f) | |
| } | |
| } | |
| return results | |
| } | |
| // Filter is the main group message handler using VJ-style search. | |
| // Replaces the old MFilter+searchWithCache approach for group messages. | |
| // | |
| //nolint:errcheck | |
| func Filter(bot *gotgbot.Bot, ctx *ext.Context) error { | |
| msg := ctx.Message | |
| if msg == nil { | |
| return nil | |
| } | |
| query := strings.TrimSpace(msg.Text + msg.Caption) | |
| if strings.HasPrefix(query, "/") || strings.HasPrefix(query, "#") || len(query) < 2 || len(query) > 100 { | |
| return nil | |
| } | |
| chatID := msg.Chat.Id | |
| messageID := msg.MessageId | |
| s := database.GetBotSettings() | |
| // Force sub check | |
| if len(s.ForceSubChats) > 0 && !CheckForceSub(bot, ctx) { | |
| return nil | |
| } | |
| // "Searching For..." indicator โ VJ style | |
| searching, _ := msg.Reply(bot, | |
| fmt.Sprintf("<b><i>Searching For %s ๐</i></b>", query), | |
| &gotgbot.SendMessageOpts{ | |
| ParseMode: gotgbot.ParseModeHTML, | |
| ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, | |
| }, | |
| ) | |
| t0 := time.Now() | |
| results := searchVJ(globalNumber, query, 100) // fetch all, paginate 10/page like VJ | |
| searchTime := time.Since(t0).Seconds() | |
| if len(results) == 0 { | |
| if searching != nil { | |
| googleQuery := strings.ReplaceAll(vjCleanQuery(query), " ", "+") + "+movie" | |
| searching.EditText(bot, | |
| fmt.Sprintf("โ <b>No results found for:</b> <code>%s</code>\n\n๐ก <i>Check spelling and try again.</i>", query), | |
| &gotgbot.EditMessageTextOpts{ | |
| ParseMode: gotgbot.ParseModeHTML, | |
| ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{ | |
| {{Text: "๐ Search on Google", Url: "https://www.google.com/search?q=" + googleQuery}}, | |
| }}, | |
| }, | |
| ) | |
| } | |
| return nil | |
| } | |
| // Delete searching indicator and show paginated results | |
| if searching != nil { | |
| searching.Delete(bot, nil) | |
| } | |
| sendVJResults(results, bot, msg, query, chatID, messageID, searchTime) | |
| return nil | |
| } | |
| // CbPages handles the no-op page-counter button. | |
| // CbPages is defined in vjsearch.go | |
| // fileDisplayName returns VJ-style "[size] name" display string. | |
| func fileDisplayName(f *database.Filter) string { | |
| name := f.FileName | |
| if name == "" { | |
| name = strings.SplitN(f.Content, "\n", 2)[0] | |
| name = usernameRegex.ReplaceAllString(name, "") | |
| name = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(name, "") | |
| } | |
| if name == "" { | |
| name = f.Text | |
| } | |
| name = usernameRegex.ReplaceAllString(name, "") | |
| name = regexp.MustCompile(`(?i)https?://\S+|t\.me/\S+|www\.\S+`).ReplaceAllString(name, "") | |
| name = strings.TrimSpace(name) | |
| prefix := "" | |
| if f.FileSize > 0 { | |
| prefix = "[" + getFileSize(f.FileSize) + "] " | |
| } | |
| full := prefix + name | |
| if len(full) > 58 { | |
| full = full[:55] + "..." | |
| } | |
| return full | |
| } | |
| // getFileSize returns human-readable file size. | |
| func getFileSize(size int64) string { | |
| switch { | |
| case size >= 1024*1024*1024: | |
| return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024)) | |
| case size >= 1024*1024: | |
| return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024)) | |
| case size >= 1024: | |
| return fmt.Sprintf("%.1f KB", float64(size)/1024) | |
| default: | |
| return fmt.Sprintf("%d B", size) | |
| } | |
| } | |