Go-Filter-Bot / plugins /filter.go
Thaimozhi's picture
Auto index concept update
1a53d1b
Raw History Blame
29.5 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_]+(?:\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
// Strip usernames from the caption
content = usernameRegex.ReplaceAllString(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)
}
}