Files changed (1) hide show
  1. filter.go +896 -0
filter.go ADDED
@@ -0,0 +1,896 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // (c) Jisin0
2
+
3
+ package plugins
4
+
5
+ import (
6
+ "context"
7
+ "fmt"
8
+ "regexp"
9
+ "strconv"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/Jisin0/Go-Filter-Bot/database"
14
+ "github.com/Jisin0/Go-Filter-Bot/utils"
15
+ "github.com/Jisin0/Go-Filter-Bot/utils/autodelete"
16
+ "github.com/Jisin0/Go-Filter-Bot/utils/customfilters"
17
+ "github.com/Jisin0/Go-Filter-Bot/utils/shortener"
18
+ "github.com/PaulSonOfLars/gotgbot/v2"
19
+ "github.com/PaulSonOfLars/gotgbot/v2/ext"
20
+ "go.mongodb.org/mongo-driver/bson"
21
+ "go.mongodb.org/mongo-driver/mongo/options"
22
+ )
23
+
24
+ // Regex expressions for callbacks and filtering
25
+ var (
26
+ buttonRegex *regexp.Regexp = regexp.MustCompile(`\[([^\[]+?)\]\((buttonurl|url|alert):(?:/{0,2})(.+?)\)`)
27
+ parseRegex *regexp.Regexp = regexp.MustCompile(`^"([^"]+)"`)
28
+ cbstopRegex *regexp.Regexp = regexp.MustCompile(`stopf\((.+)\)`)
29
+ cbalertRegex *regexp.Regexp = regexp.MustCompile(`alert\((.+)\)`)
30
+ cbfileRegex *regexp.Regexp = regexp.MustCompile(`file\((.+)\)`)
31
+ usernameRegex *regexp.Regexp = regexp.MustCompile(`(?i)@[a-z0-9_]+(?:\s|-)*`)
32
+ )
33
+
34
+ var DB *database.Database = database.NewDatabase()
35
+
36
+ const (
37
+ lenUniqueID = 15 // length of unique id string for filters
38
+ globalNumber int64 = 101 // Number used as chat id for global filters.
39
+ maxKeyLength = 20 // maximum length for a keyword of a filter
40
+
41
+ filterSplitCount = 3 // number of subtrings into which input of /filter command should be split
42
+ minButtonParseParams = 4 // Parse of a button should yield atleast this many values
43
+
44
+ alertCacheDuration = 3000 // number of seconds an alert should be cache by the client
45
+ )
46
+
47
+ const (
48
+ cbStopParamCount = 3 // number of parameters required for cbstop
49
+ maxButttons = 5 // maximum number of buttons to scan for
50
+ )
51
+
52
+ // ── Filter result cache ───────────────────────────────────────────────────────
53
+
54
+ // filterCacheEntry holds cached search results with a TTL expiry.
55
+ type filterCacheEntry struct {
56
+ results []*database.Filter
57
+ expiry time.Time
58
+ }
59
+
60
+ const filterCacheTTL = 10 * time.Minute
61
+
62
+ // searchWithCache wraps DB search calls with in-memory caching.
63
+ func searchWithCache(chatID int64, fields []string, message string, multiFilter bool) []*database.Filter {
64
+ var cacheKey string
65
+ useNew := len(fields) <= 15
66
+
67
+ if useNew {
68
+ cacheKey = database.FilterCacheKey(chatID, fields)
69
+ } else {
70
+ cacheKey = database.FilterCacheKey(chatID, []string{message})
71
+ }
72
+
73
+ if cached, ok := database.GetCachedFilterResults(cacheKey); ok {
74
+ return cached
75
+ }
76
+
77
+ var results []*database.Filter
78
+ if useNew {
79
+ results = DB.SearchMfilterNew(chatID, fields, multiFilter)
80
+ } else {
81
+ results = DB.SearchMfilterClassic(chatID, message)
82
+ }
83
+
84
+ database.SetCachedFilterResults(cacheKey, results)
85
+ return results
86
+ }
87
+
88
+ // ── Manual filter handler ─────────────────────────────────────────────────────
89
+
90
+ func sendSearchResults(results []*database.Filter, bot *gotgbot.Bot, update *gotgbot.Message, chatID int64, messageID int64) {
91
+ if len(results) == 0 {
92
+ return
93
+ }
94
+
95
+ s := database.GetBotSettings()
96
+
97
+ // Just limit to 10 for now
98
+ if len(results) > 10 {
99
+ results = results[:10]
100
+ }
101
+
102
+ if s.SearchAsButton {
103
+ var rows [][]gotgbot.InlineKeyboardButton
104
+ for _, f := range results {
105
+ btnText := strings.SplitN(f.Content, "\n", 2)[0]
106
+ btnText = usernameRegex.ReplaceAllString(btnText, "")
107
+ btnText = strings.TrimSpace(btnText)
108
+ if btnText == "" {
109
+ btnText = f.Text
110
+ }
111
+ if len(btnText) > 60 {
112
+ btnText = btnText[:57] + "..."
113
+ }
114
+ rows = append(rows, []gotgbot.InlineKeyboardButton{{
115
+ Text: "📁 " + btnText,
116
+ CallbackData: fmt.Sprintf("file(%s)", f.ID),
117
+ }})
118
+ }
119
+
120
+ text := fmt.Sprintf("<b>Here are the results found (%d):</b>\n<i>Click the buttons below to get the file.</i>", len(results))
121
+ m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{
122
+ ParseMode: gotgbot.ParseModeHTML,
123
+ ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: rows},
124
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID},
125
+ })
126
+ if err == nil && m != nil && s.AutoDelete > 0 {
127
+ autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
128
+ }
129
+ } else {
130
+ botUser := bot.User.Username
131
+ text := fmt.Sprintf("<b>Here are the results found (%d):</b>\n\n", len(results))
132
+ for i, f := range results {
133
+ btnText := strings.SplitN(f.Content, "\n", 2)[0]
134
+ btnText = usernameRegex.ReplaceAllString(btnText, "")
135
+ btnText = strings.TrimSpace(btnText)
136
+ if btnText == "" {
137
+ btnText = f.Text
138
+ }
139
+ link := fmt.Sprintf("https://t.me/%s?start=file_%s", botUser, f.ID)
140
+ text += fmt.Sprintf("%d. <a href='%s'>%s</a>\n", i+1, link, btnText)
141
+ }
142
+ text += "\n<i>Click the link above to get the file in PM.</i>"
143
+
144
+ m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{
145
+ ParseMode: gotgbot.ParseModeHTML,
146
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID},
147
+ LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
148
+ })
149
+ if err == nil && m != nil && s.AutoDelete > 0 {
150
+ autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
151
+ }
152
+ }
153
+ }
154
+
155
+ // MFilter handles automatic replies for manual filters.
156
+ func MFilter(bot *gotgbot.Bot, ctx *ext.Context) error {
157
+ update := ctx.Message
158
+ chatType := update.Chat.Type
159
+
160
+ // Group/supergroup search is now handled by Filter() (VJ-style with "Searching..." indicator)
161
+ // MFilter only handles PM search via connected group
162
+ if chatType != gotgbot.ChatTypePrivate {
163
+ return nil
164
+ }
165
+
166
+ chatID, ok := DB.GetConnection(update.Chat.Id)
167
+ if !ok {
168
+ return nil
169
+ }
170
+
171
+ message := update.Text
172
+ if update.Caption != "" {
173
+ message = update.Caption
174
+ }
175
+
176
+ fields := strings.Fields(message)
177
+ s := database.GetBotSettings()
178
+ results := searchWithCache(chatID, fields, message, s.MultiFilter)
179
+
180
+ if len(results) > 0 {
181
+ sendSearchResults(results, bot, update, chatID, update.MessageId)
182
+ }
183
+
184
+ return nil
185
+ }
186
+
187
+ // Function to handle filter and gfilter commands
188
+ //
189
+ //nolint:errcheck // too many
190
+ func NewFilter(bot *gotgbot.Bot, ctx *ext.Context) error {
191
+ var (
192
+ c int64
193
+ update = ctx.Message
194
+ )
195
+
196
+ // I didnt wanna create a whole new function for gfilter so ...
197
+ if strings.HasPrefix(update.Text, "/gfilter") {
198
+ if !utils.IsAdmin(ctx.EffectiveUser.Id) {
199
+ update.Reply(bot, "Thats an Admin-only Command :(", &gotgbot.SendMessageOpts{})
200
+ return nil
201
+ }
202
+
203
+ c = globalNumber
204
+ } else {
205
+ // Verifying and getting connections for private chats
206
+ var v bool
207
+
208
+ c, v = customfilters.Verify(bot, ctx)
209
+ if !v {
210
+ return nil
211
+ } else if c == 0 {
212
+ c = ctx.Message.Chat.Id
213
+ }
214
+ }
215
+
216
+ args := strings.SplitN(update.Text, " ", filterSplitCount)
217
+ if len(args) < 2 && (update.ReplyToMessage == nil && len(args) < filterSplitCount) {
218
+ update.Reply(
219
+ bot,
220
+ "Not Enough Parameters :(\n\nExample:- <code>/filter hi hello</code>",
221
+ &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
222
+ )
223
+
224
+ return nil
225
+ }
226
+
227
+ parse := ParseQuotes(strings.SplitN(update.Text, " ", 2)[1])
228
+
229
+ key := strings.ToLower(parse[0])
230
+
231
+ if len(key) > maxKeyLength {
232
+ 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})
233
+ }
234
+
235
+ e := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "group_id", Value: c}, {Key: "text", Value: key}})
236
+ if e.Err() == nil {
237
+ update.Reply(
238
+ bot,
239
+ 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),
240
+ &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
241
+ {{Text: "Stop Filter", CallbackData: fmt.Sprintf("stopf(%v|%v|y)", key, "local")}},
242
+ }}},
243
+ )
244
+
245
+ return nil
246
+ }
247
+
248
+ var (
249
+ text = parse[1]
250
+ button [][]map[string]string
251
+ )
252
+
253
+ if update.ReplyToMessage != nil {
254
+ text += update.ReplyToMessage.OriginalHTML()
255
+ text += update.ReplyToMessage.OriginalCaptionHTML()
256
+
257
+ if update.ReplyToMessage.ReplyMarkup != nil {
258
+ // Buttons are converted to maps to save to the database
259
+ button = buttonToMap(ctx.Message.ReplyToMessage.ReplyMarkup.InlineKeyboard)
260
+ }
261
+ }
262
+
263
+ uniqueID := utils.RandString(lenUniqueID)
264
+
265
+ text, button, alert := parseButtons(text, uniqueID, button)
266
+
267
+ // Finding media if any
268
+ var (
269
+ fileID string
270
+ mediaType string
271
+ )
272
+
273
+ if msg := update.ReplyToMessage; msg != nil {
274
+ switch {
275
+ case msg.Document != nil:
276
+ fileID = msg.Document.FileId
277
+ mediaType = "document"
278
+ case msg.Video != nil:
279
+ fileID = msg.Video.FileId
280
+ mediaType = "video"
281
+ case msg.Audio != nil:
282
+ fileID = msg.Audio.FileId
283
+ mediaType = "audio"
284
+ case msg.Sticker != nil:
285
+ fileID = msg.Sticker.FileId
286
+ mediaType = "sticker"
287
+ case msg.Animation != nil:
288
+ fileID = msg.Animation.FileId
289
+ mediaType = "animation"
290
+ case msg.Photo != nil:
291
+ fileID = msg.Photo[len(msg.Photo)-1].FileId
292
+ mediaType = "photo"
293
+ }
294
+ }
295
+
296
+ f := &database.Filter{
297
+ ID: uniqueID,
298
+ ChatID: c,
299
+ Text: key,
300
+ Content: text,
301
+ FileID: fileID,
302
+ Markup: button,
303
+ Alerts: alert,
304
+ Length: len(key),
305
+ MediaType: mediaType,
306
+ }
307
+
308
+ DB.SaveMfilter(f)
309
+
310
+ _, err := update.Reply(
311
+ bot,
312
+ fmt.Sprintf("<i>Successfully Saved A Manual Filter For <code>%v</code> !</i>", key),
313
+ &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
314
+ )
315
+ if err != nil {
316
+ fmt.Println(err)
317
+ }
318
+
319
+ return nil
320
+ }
321
+
322
+ func CbStop(bot *gotgbot.Bot, ctx *ext.Context) error {
323
+ // Function to handle callbacks from confirm buttons when stopping a filter
324
+ update := ctx.CallbackQuery
325
+
326
+ c, v := customfilters.Verify(bot, ctx)
327
+ if !v {
328
+ return nil
329
+ } else if c == 0 {
330
+ c = update.Message.GetChat().Id
331
+ }
332
+
333
+ // Making sure the callback data is valid
334
+ args := strings.Split(cbstopRegex.FindStringSubmatch(update.Data)[1], "|")
335
+ if len(args) < cbStopParamCount {
336
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Button :(", ShowAlert: true})
337
+ return nil
338
+ }
339
+
340
+ var (
341
+ key = args[0]
342
+ ftype = args[1]
343
+ erase = args[2]
344
+ )
345
+
346
+ if ftype == "local" {
347
+ if erase == "y" {
348
+ DB.DeleteMfilter(c, key)
349
+ update.Message.EditText(bot, fmt.Sprintf("Manual Filter For <code>%v</code> Was Deleted Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML})
350
+ } else if erase == "n" {
351
+ 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})
352
+ }
353
+ } else if ftype == "global" {
354
+ DB.StopGfilter(c, key)
355
+ update.Message.EditText(bot, fmt.Sprintf("Global Filter For <code>%v</code> Has Been Stopped Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML})
356
+ }
357
+
358
+ return nil
359
+ }
360
+
361
+ // Function to handle the stop command
362
+ //
363
+ //nolint:errcheck // too many
364
+ func StopMfilter(bot *gotgbot.Bot, ctx *ext.Context) error {
365
+ c, v := customfilters.Verify(bot, ctx)
366
+ if !v {
367
+ return nil
368
+ } else if c == 0 {
369
+ c = ctx.Message.Chat.Id
370
+ }
371
+
372
+ var (
373
+ update = ctx.Message
374
+ split = strings.SplitN(update.Text, " ", 2)
375
+ key string
376
+ )
377
+
378
+ switch {
379
+ case len(split) < 2 && update.Chat.Type == gotgbot.ChatTypePrivate:
380
+ m := utils.Ask(bot, "Ok Now Send Me The Name OF The Filter You Would Like To Stop ...", ctx.EffectiveChat, ctx.EffectiveUser)
381
+ if m == nil {
382
+ return nil
383
+ }
384
+
385
+ key = m.Text
386
+ case len(split) < 2:
387
+ update.Reply(bot, "Whoops looks like you forgot to mention a filter to stop !", &gotgbot.SendMessageOpts{})
388
+ default:
389
+ key = split[1]
390
+ }
391
+
392
+ // Checking if theres a local/global filter for the key
393
+ _, k := DB.GetMfilter(c, key)
394
+ _, ok := DB.GetMfilter(globalNumber, key)
395
+
396
+ // If there isnt local or global
397
+ if !k && !ok {
398
+ update.Reply(bot, fmt.Sprintf("I Couldnt Find Any Filter For <code>%v</code> To Stop !", key), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
399
+ return nil
400
+ }
401
+
402
+ // Both local and global
403
+ switch {
404
+ case k && ok:
405
+ 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)}}}}
406
+
407
+ _, 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})
408
+ if err != nil {
409
+ fmt.Println(err)
410
+ }
411
+ case k:
412
+ // Only local
413
+ markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|local|y)", key)}}}}
414
+
415
+ _, 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})
416
+ if err != nil {
417
+ fmt.Println(err)
418
+ }
419
+ case ok:
420
+ // Only global
421
+ markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|global|y)", key)}}}}
422
+
423
+ _, err := update.Reply(bot, "Please Press The Button Below To Stop Global Filter For <code>"+key+"</code>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: markup})
424
+ if err != nil {
425
+ fmt.Println(err)
426
+ }
427
+ }
428
+
429
+ return nil
430
+ }
431
+
432
+ func AllMfilters(bot *gotgbot.Bot, ctx *ext.Context) error {
433
+ // Function to handle the /filters command
434
+ update := ctx.Message
435
+ c := update.Chat.Id
436
+
437
+ if update.Chat.Type == gotgbot.ChatTypePrivate {
438
+ if i, k := DB.GetConnection(update.From.Id); k {
439
+ c = i
440
+ }
441
+ }
442
+
443
+ text := DB.StringMfilter(c)
444
+
445
+ _, err := update.Reply(bot, "Lɪsᴛ ᴏғ ғɪʟᴛᴇʀs ғᴏʀ ᴛʜɪs ᴄʜᴀᴛ :\n"+text, &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
446
+ if err != nil {
447
+ fmt.Printf("allmfilters: %v\n", err)
448
+ }
449
+
450
+ return nil
451
+ }
452
+
453
+ // Function to handle callbacks from alert button in saved filters
454
+ //
455
+ //nolint:errcheck // unnecessary
456
+ func CbAlert(bot *gotgbot.Bot, ctx *ext.Context) error {
457
+ update := ctx.CallbackQuery
458
+
459
+ args := strings.Split(cbalertRegex.FindStringSubmatch(update.Data)[1], "|")
460
+ if len(args) < 2 {
461
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true})
462
+ } else {
463
+ uniqueID := args[0]
464
+
465
+ index, err := strconv.Atoi(args[1])
466
+ if err != nil {
467
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true})
468
+ } else {
469
+ text := DB.GetAlert(uniqueID, index)
470
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: text, ShowAlert: true, CacheTime: alertCacheDuration})
471
+ }
472
+ }
473
+
474
+ return nil
475
+ }
476
+
477
+ // SendFileByID finds a filter by ID and sends it.
478
+ func SendFileByID(fileID string, bot *gotgbot.Bot, update *gotgbot.Message) {
479
+ res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileID}})
480
+ if res.Err() != nil {
481
+ fmt.Printf("SendFileByID: file %q not found in DB: %v\n", fileID, res.Err())
482
+ update.Reply(bot, "❌ File not found. It may have been deleted or the link is invalid.", &gotgbot.SendMessageOpts{ //nolint:errcheck
483
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: update.MessageId, AllowSendingWithoutReply: true},
484
+ })
485
+ return
486
+ }
487
+ var f database.Filter
488
+ if err := res.Decode(&f); err != nil {
489
+ fmt.Printf("SendFileByID: decode error for %q: %v\n", fileID, err)
490
+ return
491
+ }
492
+ if err := sendFilter(&f, bot, update.Chat.Id, update.MessageId, update.Chat.Type); err != nil {
493
+ update.Reply(bot, "❌ Failed to send file. Please try again or contact admin.", &gotgbot.SendMessageOpts{ //nolint:errcheck
494
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: update.MessageId, AllowSendingWithoutReply: true},
495
+ })
496
+ }
497
+ }
498
+
499
+ // CbSendFile handles inline button clicks to send a specific file.
500
+ //nolint:errcheck
501
+ func CbSendFile(bot *gotgbot.Bot, ctx *ext.Context) error {
502
+ update := ctx.CallbackQuery
503
+ if !CheckForceSub(bot, ctx) {
504
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "⚠️ Please join the required channel(s) first!", ShowAlert: true})
505
+ return nil
506
+ }
507
+
508
+ match := cbfileRegex.FindStringSubmatch(update.Data)
509
+ if len(match) < 2 {
510
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Invalid file button!", ShowAlert: true})
511
+ return nil
512
+ }
513
+
514
+ fileDocID := match[1]
515
+ fmt.Printf("CbSendFile: lookup _id=%q\n", fileDocID)
516
+
517
+ res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileDocID}})
518
+ if res.Err() != nil {
519
+ fmt.Printf("CbSendFile: file %q not found: %v\n", fileDocID, res.Err())
520
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "❌ File not found. It may have been removed.", ShowAlert: true})
521
+ return nil
522
+ }
523
+
524
+ var f database.Filter
525
+ if err := res.Decode(&f); err != nil {
526
+ fmt.Printf("CbSendFile: decode error: %v\n", err)
527
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "❌ Error loading file.", ShowAlert: true})
528
+ return nil
529
+ }
530
+
531
+ chatID := update.Message.GetChat().Id
532
+ msgID := update.Message.GetMessageId()
533
+ chatType := update.Message.GetChat().Type
534
+
535
+ if err := sendFilter(&f, bot, chatID, msgID, chatType); err != nil {
536
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "❌ Failed to send file: " + err.Error(), ShowAlert: true})
537
+ return nil
538
+ }
539
+
540
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "✅ Sent! Check below or your PM."})
541
+ return nil
542
+ }
543
+
544
+ func buttonToMap(btn [][]gotgbot.InlineKeyboardButton) [][]map[string]string {
545
+ // Convert a button into a map
546
+ var (
547
+ totalButtons [][]map[string]string
548
+ rowButtons []map[string]string
549
+ )
550
+
551
+ for _, i := range btn {
552
+ rowButtons = []map[string]string{}
553
+
554
+ for _, j := range i {
555
+ b := map[string]string{"Text": j.Text}
556
+
557
+ switch {
558
+ case j.CallbackData != "":
559
+ b["CallbackData"] = j.CallbackData
560
+ case j.Url != "":
561
+ b["Url"] = j.Url
562
+ default:
563
+ continue
564
+ }
565
+
566
+ rowButtons = append(rowButtons, b)
567
+ }
568
+
569
+ totalButtons = append(totalButtons, rowButtons)
570
+ }
571
+
572
+ return totalButtons
573
+ }
574
+
575
+ func mapToButton(data [][]map[string]string) [][]gotgbot.InlineKeyboardButton {
576
+ // Convert a map back into button
577
+ var totalButtons [][]gotgbot.InlineKeyboardButton
578
+
579
+ for _, i := range data {
580
+ var rowButtons []gotgbot.InlineKeyboardButton
581
+
582
+ for _, b := range i {
583
+ text := b["Text"]
584
+
585
+ if u, k := b["Url"]; k {
586
+ rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, Url: u})
587
+ } else if c, k := b["CallbackData"]; k {
588
+ rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, CallbackData: c})
589
+ }
590
+ }
591
+
592
+ totalButtons = append(totalButtons, rowButtons)
593
+ }
594
+
595
+ return totalButtons
596
+ }
597
+
598
+ func ParseQuotes(text string) []string {
599
+ res := parseRegex.FindStringSubmatch(text)
600
+ if len(res) > 0 {
601
+ return []string{res[1], strings.Replace(text, "\""+res[1]+"\"", "", 1)} // very hacky workaround by adding quotes //TODO: update regex to include quotes
602
+ } else {
603
+ split := strings.SplitN(text, " ", 2)
604
+ return []string{split[0], strings.Replace(text, split[0], "", 1)}
605
+ }
606
+ }
607
+
608
+ func parseButtons(text, uniqueID string, totalButtons [][]map[string]string) (messageText string, buttons [][]map[string]string, alertText []string) {
609
+ var (
610
+ returnText = text
611
+ rowButtons []map[string]string
612
+ alert []string
613
+ )
614
+
615
+ for _, rows := range strings.Split(text, "\n") {
616
+ for _, m := range buttonRegex.FindAllStringSubmatch(rows, maxButttons) {
617
+ if len(m) < minButtonParseParams {
618
+ continue
619
+ }
620
+
621
+ if m[2] == "url" || m[2] == "buttonurl" {
622
+ rowButtons = append(rowButtons, map[string]string{"Text": m[1], "Url": m[3]})
623
+ } else if m[2] == "alert" || m[2] == "buttonalert" {
624
+ alert = append(alert, m[3])
625
+ rowButtons = append(rowButtons, map[string]string{"Text": m[1], "CallbackData": fmt.Sprintf("alert(%v|%v)", uniqueID, len(alert)-1)})
626
+ }
627
+
628
+ returnText = strings.Replace(returnText, m[0], "", 1)
629
+ }
630
+
631
+ if len(rowButtons) > 0 {
632
+ totalButtons = append(totalButtons, rowButtons)
633
+ }
634
+
635
+ rowButtons = []map[string]string{}
636
+ }
637
+
638
+ return strings.Trim(returnText, " "), totalButtons, alert
639
+ }
640
+
641
+ // sendFilter sends the data from filter f to the chatID.
642
+ // URL buttons are shortened via the configured shortener service (if any).
643
+ // Returns an error if sending failed (so callers can show user feedback).
644
+ func sendFilter(f *database.Filter, bot *gotgbot.Bot, chatID, messageID int64, chatType string) error {
645
+ // Shorten URL buttons if a shortener is configured (read from dynamic settings)
646
+ markup := f.Markup
647
+ s := database.GetBotSettings()
648
+ if s.ShortenerAPI != "" && s.ShortenerURL != "" {
649
+ markup = shortener.ShortenAll(markup)
650
+ }
651
+
652
+ content := f.Content
653
+ // If it's a media filter (indexed file), use the clean filename as caption
654
+ if f.MediaType != "" {
655
+ if f.FileName != "" {
656
+ content = f.FileName
657
+ } else if content != "" {
658
+ // Extract first non-username line or fallback
659
+ content = usernameRegex.ReplaceAllString(content, "")
660
+ content = strings.TrimSpace(content)
661
+ }
662
+ } else {
663
+ // Strip usernames from text filter caption
664
+ content = usernameRegex.ReplaceAllString(content, "")
665
+ }
666
+
667
+ var (
668
+ buttons = mapToButton(markup)
669
+ ikm = gotgbot.InlineKeyboardMarkup{InlineKeyboard: buttons}
670
+ err error
671
+ m *gotgbot.Message
672
+ )
673
+
674
+ mediaType := f.MediaType
675
+ if mediaType != "" {
676
+ fileID := gotgbot.InputFileByID(f.FileID)
677
+
678
+ switch mediaType {
679
+ case "document":
680
+ m, err = bot.SendDocument(chatID, fileID, &gotgbot.SendDocumentOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
681
+ case "sticker":
682
+ m, err = bot.SendSticker(chatID, fileID, &gotgbot.SendStickerOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm})
683
+ case "video":
684
+ m, err = bot.SendVideo(chatID, fileID, &gotgbot.SendVideoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
685
+ case "photo":
686
+ m, err = bot.SendPhoto(chatID, fileID, &gotgbot.SendPhotoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
687
+ case "audio":
688
+ m, err = bot.SendAudio(chatID, fileID, &gotgbot.SendAudioOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
689
+ case "animation":
690
+ m, err = bot.SendAnimation(chatID, fileID, &gotgbot.SendAnimationOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
691
+ default:
692
+ fmt.Println("Unknown media type " + mediaType)
693
+ }
694
+ } else {
695
+ m, err = bot.SendMessage(chatID, content, &gotgbot.SendMessageOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
696
+ }
697
+
698
+ if err != nil {
699
+ fmt.Printf("sendFilter: failed to send %q (type=%s, fileID=%s...): %v\n",
700
+ f.FileName, f.MediaType, safePrefix(f.FileID, 20), err)
701
+ return err
702
+ }
703
+
704
+ if m != nil && s.AutoDelete > 0 {
705
+ // Only auto-delete in groups — PM file sends should not auto-delete
706
+ if chatType != gotgbot.ChatTypePrivate {
707
+ autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60) //nolint:errcheck
708
+ }
709
+ }
710
+ return nil
711
+ }
712
+
713
+ // safePrefix returns the first n chars of s, or s itself if shorter.
714
+ func safePrefix(s string, n int) string {
715
+ if len(s) <= n {
716
+ return s
717
+ }
718
+ return s[:n]
719
+ }
720
+
721
+ // ══════════════════════════════════════════════════════════════════════════════
722
+ // VJ-FILTER-BOT style search — ported from Python to Go
723
+ // Credit: Bot code made by Claude
724
+ // ══════════════════════════════════════════════════════════════════════════════
725
+
726
+
727
+ // vjCleanQuery replicates VJ bot's query cleaning before search.
728
+ // Removes noise words that hurt search accuracy.
729
+ func vjCleanQuery(raw string) string {
730
+ 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), " ")
731
+ q = regexp.MustCompile(`[-:.]`).ReplaceAllString(q, " ")
732
+ q = regexp.MustCompile(`\s{2,}`).ReplaceAllString(q, " ")
733
+ return strings.TrimSpace(q)
734
+ }
735
+
736
+ // buildVJRegex builds MongoDB regex exactly matching VJ-FILTER-BOT's get_search_results.
737
+ // Single word: boundary match. Multi-word: words joined by wildcard separator.
738
+ func buildVJRegex(query string) string {
739
+ query = strings.TrimSpace(query)
740
+ if query == "" {
741
+ return "."
742
+ }
743
+ if !strings.Contains(query, " ") {
744
+ return `(\b|[.\+\-_])` + regexp.QuoteMeta(query) + `(\b|[.\+\-_])`
745
+ }
746
+ parts := strings.Fields(query)
747
+ escaped := make([]string, len(parts))
748
+ for i, p := range parts {
749
+ escaped[i] = regexp.QuoteMeta(p)
750
+ }
751
+ return strings.Join(escaped, `.*[\s\.\+\-_]`)
752
+ }
753
+
754
+ // searchVJ searches file_name AND text fields using VJ-style regex.
755
+ // This is the core of VJ's high hit ratio.
756
+ func searchVJ(chatID int64, rawQuery string, limit int64) []*database.Filter {
757
+ query := vjCleanQuery(rawQuery)
758
+ if query == "" {
759
+ return nil
760
+ }
761
+ pattern := buildVJRegex(query)
762
+ filter := bson.D{
763
+ {Key: "group_id", Value: chatID},
764
+ {Key: "$or", Value: bson.A{
765
+ bson.D{{Key: "file_name", Value: bson.M{"$regex": pattern, "$options": "i"}}},
766
+ bson.D{{Key: "text", Value: bson.M{"$regex": pattern, "$options": "i"}}},
767
+ }},
768
+ {Key: "file", Value: bson.M{"$exists": true, "$ne": ""}},
769
+ }
770
+
771
+ cur, err := DB.Mcol.Find(
772
+ context.Background(),
773
+ filter,
774
+ options.Find().
775
+ SetSort(bson.D{{Key: "$natural", Value: -1}}).
776
+ SetLimit(limit),
777
+ )
778
+ if err != nil {
779
+ return nil
780
+ }
781
+ defer cur.Close(context.Background())
782
+
783
+ var results []*database.Filter
784
+ for cur.Next(context.Background()) {
785
+ var f database.Filter
786
+ if err := cur.Decode(&f); err == nil {
787
+ results = append(results, &f)
788
+ }
789
+ }
790
+ return results
791
+ }
792
+
793
+ // Filter is the main group message handler using VJ-style search.
794
+ // Replaces the old MFilter+searchWithCache approach for group messages.
795
+ //
796
+ //nolint:errcheck
797
+ func Filter(bot *gotgbot.Bot, ctx *ext.Context) error {
798
+ msg := ctx.Message
799
+ if msg == nil {
800
+ return nil
801
+ }
802
+
803
+ query := strings.TrimSpace(msg.Text + msg.Caption)
804
+ if strings.HasPrefix(query, "/") || strings.HasPrefix(query, "#") || len(query) < 2 || len(query) > 100 {
805
+ return nil
806
+ }
807
+
808
+ chatID := msg.Chat.Id
809
+ messageID := msg.MessageId
810
+
811
+ s := database.GetBotSettings()
812
+
813
+ // Force sub check
814
+ if len(s.ForceSubChats) > 0 && !CheckForceSub(bot, ctx) {
815
+ return nil
816
+ }
817
+
818
+ // "Searching For..." indicator — VJ style
819
+ searching, _ := msg.Reply(bot,
820
+ fmt.Sprintf("<b><i>Searching For %s 🔍</i></b>", query),
821
+ &gotgbot.SendMessageOpts{
822
+ ParseMode: gotgbot.ParseModeHTML,
823
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true},
824
+ },
825
+ )
826
+
827
+ t0 := time.Now()
828
+ results := searchVJ(globalNumber, query, 100) // fetch all, paginate 10/page like VJ
829
+ searchTime := time.Since(t0).Seconds()
830
+
831
+ if len(results) == 0 {
832
+ if searching != nil {
833
+ googleQuery := strings.ReplaceAll(vjCleanQuery(query), " ", "+") + "+movie"
834
+ searching.EditText(bot,
835
+ fmt.Sprintf("❌ <b>No results found for:</b> <code>%s</code>\n\n💡 <i>Check spelling and try again.</i>", query),
836
+ &gotgbot.EditMessageTextOpts{
837
+ ParseMode: gotgbot.ParseModeHTML,
838
+ ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
839
+ {{Text: "🔍 Search on Google", Url: "https://www.google.com/search?q=" + googleQuery}},
840
+ }},
841
+ },
842
+ )
843
+ }
844
+ return nil
845
+ }
846
+
847
+ // Delete searching indicator and show paginated results
848
+ if searching != nil {
849
+ searching.Delete(bot, nil)
850
+ }
851
+ sendVJResults(results, bot, msg, query, chatID, messageID, searchTime)
852
+ return nil
853
+ }
854
+
855
+ // CbPages handles the no-op page-counter button.
856
+ // CbPages is defined in vjsearch.go
857
+
858
+ // fileDisplayName returns VJ-style "[size] name" display string.
859
+ func fileDisplayName(f *database.Filter) string {
860
+ name := f.FileName
861
+ if name == "" {
862
+ name = strings.SplitN(f.Content, "\n", 2)[0]
863
+ name = usernameRegex.ReplaceAllString(name, "")
864
+ name = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(name, "")
865
+ }
866
+ if name == "" {
867
+ name = f.Text
868
+ }
869
+ name = usernameRegex.ReplaceAllString(name, "")
870
+ name = regexp.MustCompile(`(?i)https?://\S+|t\.me/\S+|www\.\S+`).ReplaceAllString(name, "")
871
+ name = strings.TrimSpace(name)
872
+
873
+ prefix := ""
874
+ if f.FileSize > 0 {
875
+ prefix = "[" + getFileSize(f.FileSize) + "] "
876
+ }
877
+ full := prefix + name
878
+ if len(full) > 58 {
879
+ full = full[:55] + "..."
880
+ }
881
+ return full
882
+ }
883
+
884
+ // getFileSize returns human-readable file size.
885
+ func getFileSize(size int64) string {
886
+ switch {
887
+ case size >= 1024*1024*1024:
888
+ return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024))
889
+ case size >= 1024*1024:
890
+ return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024))
891
+ case size >= 1024:
892
+ return fmt.Sprintf("%.1f KB", float64(size)/1024)
893
+ default:
894
+ return fmt.Sprintf("%d B", size)
895
+ }
896
+ }