Thaimozhi commited on
Commit
3fa0fdf
·
1 Parent(s): 1c9c497

Auto index concept update

Browse files
Files changed (6) hide show
  1. database/database.go +29 -11
  2. main.go +9 -0
  3. plugins/basics.go +28 -18
  4. plugins/bulkindex.go +2 -1
  5. plugins/filter.go +280 -605
  6. plugins/index.go +20 -20
database/database.go CHANGED
@@ -848,18 +848,36 @@ func (db *Database) CleanGhostFilters() int64 {
848
  return count
849
  }
850
 
851
- // SaveMfilterByFileID saves a filter using Telegram file_id as the document _id.
852
- // This prevents duplicate indexing of the same file across multiple runs.
853
- func (db *Database) SaveMfilterByFileID(data *Filter) error {
854
- t := true
855
- _, err := db.Mcol.UpdateOne(
856
- context.TODO(),
857
- bson.D{{Key: "_id", Value: data.ID}},
858
- bson.D{{Key: "$setOnInsert", Value: data}},
859
- &options.UpdateOptions{Upsert: &t},
860
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  if err != nil {
862
  fmt.Printf("db.SaveMfilterByFileID: %v\n", err)
 
863
  }
864
- return err
865
  }
 
848
  return count
849
  }
850
 
851
+ // randShortID generates a random alphanumeric string of given length.
852
+ // Safe for Telegram deep link start parameters (max 64 chars).
853
+ func randShortID(n int) string {
854
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
855
+ b := make([]byte, n)
856
+ seed := time.Now().UnixNano()
857
+ for i := range b {
858
+ seed = seed*6364136223846793005 + 1442695040888963407
859
+ b[i] = chars[((seed>>33)&0x7fffffffffffffff)%int64(len(chars))]
860
+ }
861
+ return string(b)
862
+ }
863
+
864
+ // SaveMfilterByFileID saves a filter with a short random _id (safe for Telegram start params).
865
+ // Deduplication is done by checking the 'file' (telegram file_id) field to avoid double-indexing.
866
+ // Returns true if a new document was inserted, false if it already existed.
867
+ func (db *Database) SaveMfilterByFileID(data *Filter) (bool, error) {
868
+ // Check if this telegram file_id already exists
869
+ existing := db.Mcol.FindOne(context.TODO(), bson.D{{Key: "file", Value: data.FileID}})
870
+ if existing.Err() == nil {
871
+ return false, nil // already indexed
872
+ }
873
+
874
+ // Generate a short random _id safe for Telegram deep links (max 64 chars with "file_" prefix = 59 chars)
875
+ data.ID = randShortID(15)
876
+
877
+ _, err := db.Mcol.InsertOne(context.TODO(), data)
878
  if err != nil {
879
  fmt.Printf("db.SaveMfilterByFileID: %v\n", err)
880
+ return false, err
881
  }
882
+ return true, nil
883
  }
main.go CHANGED
@@ -80,6 +80,15 @@ func main() {
80
  // Send restart alert to log channel
81
  go logger.SendRestart(b)
82
 
 
 
 
 
 
 
 
 
 
83
  // Always run the auto-delete daemon in the background to support dynamic settings
84
  go autodelete.RunAutodel(b)
85
 
 
80
  // Send restart alert to log channel
81
  go logger.SendRestart(b)
82
 
83
+ // Auto-cleanup ghost/reference docs on every startup
84
+ go func() {
85
+ removed := plugins.DB.CleanGhostFilters()
86
+ if removed > 0 {
87
+ fmt.Printf("startup: cleaned %d ghost docs from DB\n", removed)
88
+ logger.Send(b, fmt.Sprintf("🧹 <b>Startup cleanup:</b> removed <code>%d</code> ghost documents from DB.", removed))
89
+ }
90
+ }()
91
+
92
  // Always run the auto-delete daemon in the background to support dynamic settings
93
  go autodelete.RunAutodel(b)
94
 
plugins/basics.go CHANGED
@@ -172,14 +172,12 @@ func CbEdit(bot *gotgbot.Bot, update *ext.Context) error {
172
  }
173
 
174
  editMarkup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: markup}
175
- chatID := update.CallbackQuery.Message.GetChat().Id
176
- msgID := update.CallbackQuery.Message.GetMessageId()
177
 
178
- // Check if the original message has a photo (i.e. sent with START_IMAGE)
179
- // In that case we must edit caption, not text.
180
- origMsg, ok2 := update.CallbackQuery.Message.(*gotgbot.Message)
181
- if ok2 && origMsg.Photo != nil {
182
- // Edit caption on the photo message using the message helper
183
  _, _, err := origMsg.EditCaption(bot, &gotgbot.EditMessageCaptionOpts{
184
  Caption: text,
185
  ParseMode: gotgbot.ParseModeHTML,
@@ -191,20 +189,32 @@ func CbEdit(bot *gotgbot.Bot, update *ext.Context) error {
191
  return nil
192
  }
193
 
194
- // Regular text message — edit text
195
- _, _, err := update.CallbackQuery.Message.EditText(bot, text, &gotgbot.EditMessageTextOpts{
196
- ChatId: chatID,
197
- MessageId: msgID,
198
- ParseMode: gotgbot.ParseModeHTML,
199
- LinkPreviewOptions: &gotgbot.LinkPreviewOptions{
200
- IsDisabled: true,
201
- },
202
- ReplyMarkup: editMarkup,
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  })
204
  if err != nil {
205
- fmt.Printf("cbedit: %v\n", err)
206
  }
207
-
208
  return nil
209
  }
210
 
 
172
  }
173
 
174
  editMarkup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: markup}
 
 
175
 
176
+ // Try to get the full message object
177
+ origMsg, isMsg := update.CallbackQuery.Message.(*gotgbot.Message)
178
+
179
+ // If it's a photo message (sent with START_IMAGE), edit caption
180
+ if isMsg && len(origMsg.Photo) > 0 {
181
  _, _, err := origMsg.EditCaption(bot, &gotgbot.EditMessageCaptionOpts{
182
  Caption: text,
183
  ParseMode: gotgbot.ParseModeHTML,
 
189
  return nil
190
  }
191
 
192
+ // Standard text message edit
193
+ if isMsg {
194
+ _, _, err := origMsg.EditText(bot, text, &gotgbot.EditMessageTextOpts{
195
+ ParseMode: gotgbot.ParseModeHTML,
196
+ LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
197
+ ReplyMarkup: editMarkup,
198
+ })
199
+ if err != nil {
200
+ fmt.Printf("cbedit: %v\n", err)
201
+ }
202
+ return nil
203
+ }
204
+
205
+ // Fallback: use raw bot API with chat/message IDs
206
+ chatID := update.CallbackQuery.Message.GetChat().Id
207
+ msgID := update.CallbackQuery.Message.GetMessageId()
208
+ _, _, err := bot.EditMessageText(text, &gotgbot.EditMessageTextOpts{
209
+ ChatId: chatID,
210
+ MessageId: msgID,
211
+ ParseMode: gotgbot.ParseModeHTML,
212
+ LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
213
+ ReplyMarkup: editMarkup,
214
  })
215
  if err != nil {
216
+ fmt.Printf("cbedit(fallback): %v\n", err)
217
  }
 
218
  return nil
219
  }
220
 
plugins/bulkindex.go CHANGED
@@ -206,7 +206,8 @@ func runBulkIndex(bot *gotgbot.Bot, chanID, fromID, toID int64, statusMsg *gotgb
206
  Length: len(keyword),
207
  }
208
 
209
- if err := DB.SaveMfilterByFileID(f); err != nil {
 
210
  skipped++
211
  } else {
212
  indexed++
 
206
  Length: len(keyword),
207
  }
208
 
209
+ inserted, err := DB.SaveMfilterByFileID(f)
210
+ if err != nil || !inserted {
211
  skipped++
212
  } else {
213
  indexed++
plugins/filter.go CHANGED
@@ -1,4 +1,6 @@
1
- // (c) Jisin0
 
 
2
 
3
  package plugins
4
 
@@ -6,726 +8,399 @@ 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
- )
22
 
23
- // Regex expressions for callbacks and filtering
24
- var (
25
- buttonRegex *regexp.Regexp = regexp.MustCompile(`\[([^\[]+?)\]\((buttonurl|url|alert):(?:/{0,2})(.+?)\)`)
26
- parseRegex *regexp.Regexp = regexp.MustCompile(`^"([^"]+)"`)
27
- cbstopRegex *regexp.Regexp = regexp.MustCompile(`stopf\((.+)\)`)
28
- cbalertRegex *regexp.Regexp = regexp.MustCompile(`alert\((.+)\)`)
29
- cbfileRegex *regexp.Regexp = regexp.MustCompile(`file\((.+)\)`)
30
- usernameRegex *regexp.Regexp = regexp.MustCompile(`(?i)@[a-z0-9_]+(?:\s|-)*`)
31
  )
32
 
33
- var DB *database.Database = database.NewDatabase()
 
34
 
35
  const (
36
- lenUniqueID = 15 // length of unique id string for filters
37
- globalNumber int64 = 101 // Number used as chat id for global filters.
38
- maxKeyLength = 20 // maximum length for a keyword of a filter
39
-
40
- filterSplitCount = 3 // number of subtrings into which input of /filter command should be split
41
- minButtonParseParams = 4 // Parse of a button should yield atleast this many values
42
-
43
- alertCacheDuration = 3000 // number of seconds an alert should be cache by the client
44
  )
45
 
46
- const (
47
- cbStopParamCount = 3 // number of parameters required for cbstop
48
- maxButttons = 5 // maximum number of buttons to scan for
 
 
49
  )
50
 
51
- // ── Filter result cache ───────────────────────────────────────────────────────
52
-
53
- // filterCacheEntry holds cached search results with a TTL expiry.
54
- type filterCacheEntry struct {
55
- results []*database.Filter
56
- expiry time.Time
 
 
57
  }
58
 
59
- const filterCacheTTL = 10 * time.Minute
60
-
61
- // searchWithCache wraps DB search calls with in-memory caching.
62
- func searchWithCache(chatID int64, fields []string, message string, multiFilter bool) []*database.Filter {
63
- var cacheKey string
64
- useNew := len(fields) <= 15
65
-
66
- if useNew {
67
- cacheKey = database.FilterCacheKey(chatID, fields)
68
- } else {
69
- cacheKey = database.FilterCacheKey(chatID, []string{message})
70
  }
71
-
72
- if cached, ok := database.GetCachedFilterResults(cacheKey); ok {
73
- return cached
74
  }
75
-
76
- var results []*database.Filter
77
- if useNew {
78
- results = DB.SearchMfilterNew(chatID, fields, multiFilter)
79
- } else {
80
- results = DB.SearchMfilterClassic(chatID, message)
81
  }
82
-
83
- database.SetCachedFilterResults(cacheKey, results)
84
- return results
85
  }
86
 
87
- // ── Manual filter handler ─────────────────────────────────────────────────────
88
-
89
- // fileDisplayName returns the best display name for a filter result.
90
- // Prefers FileName (full file name), falls back to Content first line, then Text.
91
- func fileDisplayName(f *database.Filter) string {
92
- name := f.FileName
93
- if name == "" {
94
- name = strings.SplitN(f.Content, "\n", 2)[0]
95
- name = usernameRegex.ReplaceAllString(name, "")
96
- name = strings.TrimSpace(name)
97
- }
98
- if name == "" {
99
- name = f.Text
100
- }
101
- // Strip HTML tags for button display
102
- name = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(name, "")
103
- name = strings.TrimSpace(name)
104
- if len(name) > 58 {
105
- name = name[:55] + "..."
106
  }
107
- return name
108
- }
109
 
110
- func sendSearchResults(results []*database.Filter, bot *gotgbot.Bot, update *gotgbot.Message, chatID int64, messageID int64) {
111
- if len(results) == 0 {
112
- return
 
 
 
 
 
 
113
  }
114
 
115
- s := database.GetBotSettings()
 
 
116
 
117
- // Use VJ-style paginated results when there are more than 5 files
118
- if len(results) > 5 {
119
- query := strings.TrimSpace(update.Text + " " + update.Caption)
120
- if query == "" && len(results) > 0 {
121
- query = results[0].Text
122
- }
123
- sendVJResults(results, bot, update, query, chatID, messageID)
124
- return
125
  }
 
126
 
127
- if s.SearchAsButton {
128
- var rows [][]gotgbot.InlineKeyboardButton
129
- for _, f := range results {
130
- btnText := fileDisplayName(f)
131
- rows = append(rows, []gotgbot.InlineKeyboardButton{{
132
- Text: "📁 " + btnText,
133
- CallbackData: fmt.Sprintf("file(%s)", f.ID),
134
- }})
135
- }
136
-
137
- text := fmt.Sprintf("🔍 <b>Results found: %d</b>\n<i>Click a button to get the file sent to you.</i>", len(results))
138
- m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{
139
- ParseMode: gotgbot.ParseModeHTML,
140
- ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: rows},
141
- ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true},
142
- })
143
- if err == nil && m != nil && s.AutoDelete > 0 {
144
- autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
145
- }
146
- } else {
147
- botUser := bot.User.Username
148
- text := fmt.Sprintf("🔍 <b>Results found: %d</b>\n\n", len(results))
149
- for i, f := range results {
150
- btnText := fileDisplayName(f)
151
- link := fmt.Sprintf("https://t.me/%s?start=file_%s", botUser, f.ID)
152
- text += fmt.Sprintf("%d. <a href='%s'>%s</a>\n", i+1, link, btnText)
153
- }
154
- text += "\n⚠️ <i>Links open in PM — make sure you've started the bot!</i>"
155
-
156
- m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{
157
- ParseMode: gotgbot.ParseModeHTML,
158
- ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true},
159
- LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
160
- })
161
- if err == nil && m != nil && s.AutoDelete > 0 {
162
- autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
163
  }
164
  }
 
165
  }
166
- func MFilter(bot *gotgbot.Bot, ctx *ext.Context) error {
167
- var (
168
- chatID int64
169
- update = ctx.Message
170
- messageID = update.MessageId
171
- chatType = update.Chat.Type
172
- message = update.Text
173
- )
174
-
175
- if update.Caption != "" {
176
- message = update.Caption
177
- }
178
-
179
- switch chatType {
180
- case gotgbot.ChatTypePrivate:
181
- var ok bool
182
-
183
- chatID, ok = DB.GetConnection(update.Chat.Id)
184
- if !ok {
185
- return nil
186
- }
187
- case gotgbot.ChatTypeSupergroup, gotgbot.ChatTypeGroup:
188
- chatID = update.Chat.Id
189
- default:
190
- return nil
191
- }
192
-
193
- fields := strings.Fields(message)
194
- s := database.GetBotSettings()
195
- results := searchWithCache(chatID, fields, message, s.MultiFilter)
196
 
197
- if len(results) > 0 {
198
- sendSearchResults(results, bot, update, chatID, messageID)
199
- } else if update.Chat.Type != gotgbot.ChatTypePrivate {
200
- // No results – suggest spelling check and Google search
201
- sendNoResultsSuggestion(bot, update, message, messageID)
 
 
 
 
 
 
 
 
 
202
  }
203
-
204
- return nil
205
  }
206
 
207
- // Function to handle filter and gfilter commands
208
- //
209
- //nolint:errcheck // too many
210
- func NewFilter(bot *gotgbot.Bot, ctx *ext.Context) error {
211
- var (
212
- c int64
213
- update = ctx.Message
214
- )
215
-
216
- // I didnt wanna create a whole new function for gfilter so ...
217
- if strings.HasPrefix(update.Text, "/gfilter") {
218
- if !utils.IsAdmin(ctx.EffectiveUser.Id) {
219
- update.Reply(bot, "Thats an Admin-only Command :(", &gotgbot.SendMessageOpts{})
220
- return nil
221
- }
222
-
223
- c = globalNumber
224
- } else {
225
- // Verifying and getting connections for private chats
226
- var v bool
227
-
228
- c, v = customfilters.Verify(bot, ctx)
229
- if !v {
230
- return nil
231
- } else if c == 0 {
232
- c = ctx.Message.Chat.Id
233
- }
234
- }
235
-
236
- args := strings.SplitN(update.Text, " ", filterSplitCount)
237
- if len(args) < 2 && (update.ReplyToMessage == nil && len(args) < filterSplitCount) {
238
- update.Reply(
239
- bot,
240
- "Not Enough Parameters :(\n\nExample:- <code>/filter hi hello</code>",
241
- &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
242
- )
243
 
 
 
 
 
 
 
244
  return nil
245
  }
246
 
247
- parse := ParseQuotes(strings.SplitN(update.Text, " ", 2)[1])
248
-
249
- key := strings.ToLower(parse[0])
250
 
251
- if len(key) > maxKeyLength {
252
- 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})
 
253
  }
254
-
255
- e := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "group_id", Value: c}, {Key: "text", Value: key}})
256
- if e.Err() == nil {
257
- update.Reply(
258
- bot,
259
- 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),
260
- &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
261
- {{Text: "Stop Filter", CallbackData: fmt.Sprintf("stopf(%v|%v|y)", key, "local")}},
262
- }}},
263
- )
264
-
265
  return nil
266
  }
267
 
268
- var (
269
- text = parse[1]
270
- button [][]map[string]string
271
- )
272
-
273
- if update.ReplyToMessage != nil {
274
- text += update.ReplyToMessage.OriginalHTML()
275
- text += update.ReplyToMessage.OriginalCaptionHTML()
276
 
277
- if update.ReplyToMessage.ReplyMarkup != nil {
278
- // Buttons are converted to maps to save to the database
279
- button = buttonToMap(ctx.Message.ReplyToMessage.ReplyMarkup.InlineKeyboard)
 
280
  }
281
  }
282
 
283
- uniqueID := utils.RandString(lenUniqueID)
284
-
285
- text, button, alert := parseButtons(text, uniqueID, button)
 
 
 
 
286
 
287
- // Finding media if any
288
- var (
289
- fileID string
290
- mediaType string
291
- )
292
 
293
- if msg := update.ReplyToMessage; msg != nil {
294
- switch {
295
- case msg.Document != nil:
296
- fileID = msg.Document.FileId
297
- mediaType = "document"
298
- case msg.Video != nil:
299
- fileID = msg.Video.FileId
300
- mediaType = "video"
301
- case msg.Audio != nil:
302
- fileID = msg.Audio.FileId
303
- mediaType = "audio"
304
- case msg.Sticker != nil:
305
- fileID = msg.Sticker.FileId
306
- mediaType = "sticker"
307
- case msg.Animation != nil:
308
- fileID = msg.Animation.FileId
309
- mediaType = "animation"
310
- case msg.Photo != nil:
311
- fileID = msg.Photo[len(msg.Photo)-1].FileId
312
- mediaType = "photo"
313
  }
314
- }
315
-
316
- f := &database.Filter{
317
- ID: uniqueID,
318
- ChatID: c,
319
- Text: key,
320
- Content: text,
321
- FileID: fileID,
322
- Markup: button,
323
- Alerts: alert,
324
- Length: len(key),
325
- MediaType: mediaType,
326
- }
327
-
328
- DB.SaveMfilter(f)
329
-
330
- _, err := update.Reply(
331
- bot,
332
- fmt.Sprintf("<i>Successfully Saved A Manual Filter For <code>%v</code> !</i>", key),
333
- &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
334
- )
335
- if err != nil {
336
- fmt.Println(err)
337
- }
338
-
339
- return nil
340
- }
341
-
342
- func CbStop(bot *gotgbot.Bot, ctx *ext.Context) error {
343
- // Function to handle callbacks from confirm buttons when stopping a filter
344
- update := ctx.CallbackQuery
345
-
346
- c, v := customfilters.Verify(bot, ctx)
347
- if !v {
348
  return nil
349
- } else if c == 0 {
350
- c = update.Message.GetChat().Id
351
  }
352
 
353
- // Making sure the callback data is valid
354
- args := strings.Split(cbstopRegex.FindStringSubmatch(update.Data)[1], "|")
355
- if len(args) < cbStopParamCount {
356
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Button :(", ShowAlert: true})
357
- return nil
358
- }
359
 
360
- var (
361
- key = args[0]
362
- ftype = args[1]
363
- erase = args[2]
364
- )
365
-
366
- if ftype == "local" {
367
- if erase == "y" {
368
- DB.DeleteMfilter(c, key)
369
- update.Message.EditText(bot, fmt.Sprintf("Manual Filter For <code>%v</code> Was Deleted Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML})
370
- } else if erase == "n" {
371
- 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})
372
- }
373
- } else if ftype == "global" {
374
- DB.StopGfilter(c, key)
375
- update.Message.EditText(bot, fmt.Sprintf("Global Filter For <code>%v</code> Has Been Stopped Successfully !", key), &gotgbot.EditMessageTextOpts{ParseMode: gotgbot.ParseModeHTML})
376
  }
377
-
378
  return nil
379
  }
380
 
381
- // Function to handle the stop command
382
- //
383
- //nolint:errcheck // too many
384
- func StopMfilter(bot *gotgbot.Bot, ctx *ext.Context) error {
385
- c, v := customfilters.Verify(bot, ctx)
386
- if !v {
387
- return nil
388
- } else if c == 0 {
389
- c = ctx.Message.Chat.Id
390
- }
391
-
392
- var (
393
- update = ctx.Message
394
- split = strings.SplitN(update.Text, " ", 2)
395
- key string
396
- )
397
 
398
- switch {
399
- case len(split) < 2 && update.Chat.Type == gotgbot.ChatTypePrivate:
400
- m := utils.Ask(bot, "Ok Now Send Me The Name OF The Filter You Would Like To Stop ...", ctx.EffectiveChat, ctx.EffectiveUser)
401
- if m == nil {
402
- return nil
403
- }
404
-
405
- key = m.Text
406
- case len(split) < 2:
407
- update.Reply(bot, "Whoops looks like you forgot to mention a filter to stop !", &gotgbot.SendMessageOpts{})
408
- default:
409
- key = split[1]
410
  }
411
 
412
- // Checking if theres a local/global filter for the key
413
- _, k := DB.GetMfilter(c, key)
414
- _, ok := DB.GetMfilter(globalNumber, key)
415
-
416
- // If there isnt local or global
417
- if !k && !ok {
418
- update.Reply(bot, fmt.Sprintf("I Couldnt Find Any Filter For <code>%v</code> To Stop !", key), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
419
- return nil
420
  }
421
 
422
- // Both local and global
423
- switch {
424
- case k && ok:
425
- 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)}}}}
426
-
427
- _, 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})
428
- if err != nil {
429
- fmt.Println(err)
430
- }
431
- case k:
432
- // Only local
433
- markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|local|y)", key)}}}}
434
 
435
- _, 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})
436
- if err != nil {
437
- fmt.Println(err)
438
- }
439
- case ok:
440
- // Only global
441
- markup := gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{{Text: "CONFIRM", CallbackData: fmt.Sprintf("stopf(%v|global|y)", key)}}}}
442
 
443
- _, err := update.Reply(bot, "Please Press The Button Below To Stop Global Filter For <code>"+key+"</code>", &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML, ReplyMarkup: markup})
444
- if err != nil {
445
- fmt.Println(err)
 
 
446
  }
447
  }
448
 
449
- return nil
450
- }
 
 
 
 
451
 
452
- func AllMfilters(bot *gotgbot.Bot, ctx *ext.Context) error {
453
- // Function to handle the /filters command
454
- update := ctx.Message
455
- c := update.Chat.Id
456
 
457
- if update.Chat.Type == gotgbot.ChatTypePrivate {
458
- if i, k := DB.GetConnection(update.From.Id); k {
459
- c = i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  }
 
 
 
 
 
 
461
  }
462
 
463
- text := DB.StringMfilter(c)
464
-
465
- _, err := update.Reply(bot, "Lɪsᴛ ᴏғ ғɪʟᴛᴇʀs ғᴏʀ ᴛʜɪs ᴄʜᴀᴛ :\n"+text, &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
466
  if err != nil {
467
- fmt.Printf("allmfilters: %v\n", err)
 
468
  }
469
 
470
- return nil
471
- }
472
-
473
- // Function to handle callbacks from alert button in saved filters
474
- //
475
- //nolint:errcheck // unnecessary
476
- func CbAlert(bot *gotgbot.Bot, ctx *ext.Context) error {
477
- update := ctx.CallbackQuery
478
-
479
- args := strings.Split(cbalertRegex.FindStringSubmatch(update.Data)[1], "|")
480
- if len(args) < 2 {
481
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true})
482
- } else {
483
- uniqueID := args[0]
484
-
485
- index, err := strconv.Atoi(args[1])
486
- if err != nil {
487
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Bad Request !", ShowAlert: true})
488
- } else {
489
- text := DB.GetAlert(uniqueID, index)
490
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: text, ShowAlert: true, CacheTime: alertCacheDuration})
491
- }
492
  }
493
-
494
- return nil
495
  }
496
 
497
- // SendFileByID finds a filter by ID and sends it.
498
- func SendFileByID(fileID string, bot *gotgbot.Bot, update *gotgbot.Message) {
499
- res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileID}})
500
- if res.Err() == nil {
501
- var f database.Filter
502
- res.Decode(&f)
503
- sendFilter(&f, bot, update, update.Chat.Id, update.MessageId)
 
 
 
 
504
  }
505
  }
506
 
 
 
507
  // CbSendFile handles inline button clicks to send a specific file.
 
508
  //nolint:errcheck
509
  func CbSendFile(bot *gotgbot.Bot, ctx *ext.Context) error {
510
  update := ctx.CallbackQuery
511
- if !CheckForceSub(bot, ctx) {
 
 
 
 
 
 
512
  return nil
513
  }
514
-
515
- match := cbfileRegex.FindStringSubmatch(update.Data)
516
- if len(match) < 2 {
517
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Invalid file button!", ShowAlert: true})
 
518
  return nil
519
  }
520
-
521
- fileID := match[1]
522
-
523
- update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{})
524
-
525
- msg, ok := update.Message.(*gotgbot.Message)
526
- if !ok {
527
  return nil
528
  }
529
-
530
- SendFileByID(fileID, bot, msg)
531
-
532
- return nil
533
- }
534
 
535
- func buttonToMap(btn [][]gotgbot.InlineKeyboardButton) [][]map[string]string {
536
- // Convert a button into a map
537
- var (
538
- totalButtons [][]map[string]string
539
- rowButtons []map[string]string
540
- )
541
-
542
- for _, i := range btn {
543
- rowButtons = []map[string]string{}
544
-
545
- for _, j := range i {
546
- b := map[string]string{"Text": j.Text}
547
-
548
- switch {
549
- case j.CallbackData != "":
550
- b["CallbackData"] = j.CallbackData
551
- case j.Url != "":
552
- b["Url"] = j.Url
553
- default:
554
- continue
555
- }
556
-
557
- rowButtons = append(rowButtons, b)
558
- }
559
-
560
- totalButtons = append(totalButtons, rowButtons)
561
  }
562
 
563
- return totalButtons
 
 
564
  }
565
 
566
- func mapToButton(data [][]map[string]string) [][]gotgbot.InlineKeyboardButton {
567
- // Convert a map back into button
568
- var totalButtons [][]gotgbot.InlineKeyboardButton
569
-
570
- for _, i := range data {
571
- var rowButtons []gotgbot.InlineKeyboardButton
572
 
573
- for _, b := range i {
574
- text := b["Text"]
575
-
576
- if u, k := b["Url"]; k {
577
- rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, Url: u})
578
- } else if c, k := b["CallbackData"]; k {
579
- rowButtons = append(rowButtons, gotgbot.InlineKeyboardButton{Text: text, CallbackData: c})
580
- }
581
- }
582
-
583
- totalButtons = append(totalButtons, rowButtons)
584
  }
585
-
586
- return totalButtons
587
- }
588
-
589
- func ParseQuotes(text string) []string {
590
- res := parseRegex.FindStringSubmatch(text)
591
- if len(res) > 0 {
592
- return []string{res[1], strings.Replace(text, "\""+res[1]+"\"", "", 1)} // very hacky workaround by adding quotes //TODO: update regex to include quotes
593
- } else {
594
- split := strings.SplitN(text, " ", 2)
595
- return []string{split[0], strings.Replace(text, split[0], "", 1)}
596
  }
 
597
  }
598
 
599
- func parseButtons(text, uniqueID string, totalButtons [][]map[string]string) (messageText string, buttons [][]map[string]string, alertText []string) {
600
- var (
601
- returnText = text
602
- rowButtons []map[string]string
603
- alert []string
604
- )
605
 
606
- for _, rows := range strings.Split(text, "\n") {
607
- for _, m := range buttonRegex.FindAllStringSubmatch(rows, maxButttons) {
608
- if len(m) < minButtonParseParams {
609
- continue
610
- }
611
 
612
- if m[2] == "url" || m[2] == "buttonurl" {
613
- rowButtons = append(rowButtons, map[string]string{"Text": m[1], "Url": m[3]})
614
- } else if m[2] == "alert" || m[2] == "buttonalert" {
615
- alert = append(alert, m[3])
616
- rowButtons = append(rowButtons, map[string]string{"Text": m[1], "CallbackData": fmt.Sprintf("alert(%v|%v)", uniqueID, len(alert)-1)})
 
 
 
 
 
 
 
 
617
  }
618
-
619
- returnText = strings.Replace(returnText, m[0], "", 1)
620
  }
621
-
622
- if len(rowButtons) > 0 {
623
- totalButtons = append(totalButtons, rowButtons)
624
  }
625
-
626
- rowButtons = []map[string]string{}
627
  }
628
-
629
- return strings.Trim(returnText, " "), totalButtons, alert
630
  }
631
 
632
- // sendFilter sends the data from filter f to the chatID.
633
- // URL buttons are shortened via the configured shortener service (if any).
634
- func sendFilter(f *database.Filter, bot *gotgbot.Bot, update *gotgbot.Message, chatID, messageID int64) {
635
- // Shorten URL buttons if a shortener is configured (read from dynamic settings)
636
- markup := f.Markup
637
- s := database.GetBotSettings()
638
- if s.ShortenerAPI != "" && s.ShortenerURL != "" {
639
- markup = shortener.ShortenAll(markup)
640
- }
641
-
642
- content := f.Content
643
- // Strip usernames from the caption
644
- content = usernameRegex.ReplaceAllString(content, "")
645
-
646
- var (
647
- buttons = mapToButton(markup)
648
- ikm = gotgbot.InlineKeyboardMarkup{InlineKeyboard: buttons}
649
- err error
650
- m *gotgbot.Message
651
- )
652
-
653
- mediaType := f.MediaType
654
- if mediaType != "" {
655
- fileID := gotgbot.InputFileByID(f.FileID)
656
-
657
- switch mediaType {
658
- case "document":
659
- m, err = bot.SendDocument(chatID, fileID, &gotgbot.SendDocumentOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
660
- case "sticker":
661
- m, err = bot.SendSticker(chatID, fileID, &gotgbot.SendStickerOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm})
662
- case "video":
663
- m, err = bot.SendVideo(chatID, fileID, &gotgbot.SendVideoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
664
- case "photo":
665
- m, err = bot.SendPhoto(chatID, fileID, &gotgbot.SendPhotoOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
666
- case "audio":
667
- m, err = bot.SendAudio(chatID, fileID, &gotgbot.SendAudioOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
668
- case "animation":
669
- m, err = bot.SendAnimation(chatID, fileID, &gotgbot.SendAnimationOpts{Caption: content, ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
670
- default:
671
- fmt.Println("Unknown media type " + mediaType)
672
- }
673
- } else {
674
- m, err = update.Reply(bot, content, &gotgbot.SendMessageOpts{ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID}, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
675
- }
676
-
677
- if err != nil {
678
- fmt.Println(err)
679
- fmt.Println(f)
680
- return
681
  }
682
-
683
- if m != nil && s.AutoDelete > 0 {
684
- err := autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
685
- if err != nil {
686
- fmt.Printf("sendfilter: %v\n", err)
687
- }
688
  }
689
- }
 
 
 
690
 
691
- // sendNoResultsSuggestion sends a "not found" message with a Google Search button.
692
- // Only triggered when a message is long enough to be a search query.
693
- //
694
- //nolint:errcheck
695
- func sendNoResultsSuggestion(bot *gotgbot.Bot, update *gotgbot.Message, query string, messageID int64) {
696
- // Only trigger for messages that look like search queries (3+ chars, not commands)
697
- query = strings.TrimSpace(query)
698
- if len(query) < 3 || strings.HasPrefix(query, "/") {
699
- return
700
  }
701
- // Limit to reasonable length
702
- if len(strings.Fields(query)) > 8 {
703
- return
704
- }
705
-
706
- googleQuery := "https://www.google.com/search?q=" + strings.ReplaceAll(query, " ", "+") + "+telegram+movie+bot"
707
-
708
- text := fmt.Sprintf(
709
- "❌ <b>No results found for:</b> <code>%s</code>\n\n"+
710
- "💡 <i>Check your spelling and try again, or search on Google.</i>",
711
- query,
712
- )
713
 
714
- s := database.GetBotSettings()
715
- m, err := update.Reply(bot, text, &gotgbot.SendMessageOpts{
716
- ParseMode: gotgbot.ParseModeHTML,
717
- ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
718
- {{Text: "🔍 Search on Google", Url: googleQuery}},
719
- }},
720
- ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID},
721
- })
722
- if err == nil && m != nil && s.AutoDelete > 0 {
723
- autodelete.InsertAutodel(autodelete.AutodelData{ChatID: update.Chat.Id, MessageID: m.MessageId}, s.AutoDelete*60)
724
  }
725
- }
726
-
727
- // CbPages handles the no-op "pages" callback (page number display button).
728
- func CbPages(bot *gotgbot.Bot, ctx *ext.Context) error {
729
- ctx.CallbackQuery.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{}) //nolint:errcheck
730
- return nil
731
  }
 
1
+ // Core auto-filter and search result logic.
2
+ // Search algorithm ported from VJ-FILTER-BOT (Python/Pyrogram) to Go.
3
+ // Credit: Bot code made by Claude
4
 
5
  package plugins
6
 
 
8
  "context"
9
  "fmt"
10
  "regexp"
 
11
  "strings"
 
12
 
13
  "github.com/Jisin0/Go-Filter-Bot/database"
 
14
  "github.com/Jisin0/Go-Filter-Bot/utils/autodelete"
15
  "github.com/Jisin0/Go-Filter-Bot/utils/customfilters"
 
 
 
16
  "go.mongodb.org/mongo-driver/bson"
17
+ "go.mongodb.org/mongo-driver/mongo/options"
18
 
19
+ "github.com/PaulSonOfLars/gotgbot/v2"
20
+ "github.com/PaulSonOfLars/gotgbot/v2/ext"
 
 
 
 
 
 
21
  )
22
 
23
+ // DB is the shared database instance from customfilters package.
24
+ var DB = customfilters.DB
25
 
26
  const (
27
+ globalNumber = 101
28
+ lenUniqueID = 15
 
 
 
 
 
 
29
  )
30
 
31
+ // VJ-style search: removes noise words from query before searching
32
+ // Ported from auto_filter() in VJ-FILTER-BOT/plugins/pm_filter.py
33
+ var (
34
+ usernameRegex = regexp.MustCompile(`(?i)@\w+`)
35
+ noiseWords = regexp.MustCompile(`(?i)\b(please|pls|plz|send|movie|new|latest|bro|bruh|find|link|file|dubbed|full|helo|that|undo|kitti|thar|kittum|anyone|with subtitle[s]?)\b`)
36
  )
37
 
38
+ // vjCleanQuery replicates VJ bot's search query cleaning logic.
39
+ // Removes noise words, normalises spacing — improves search hit ratio.
40
+ func vjCleanQuery(raw string) string {
41
+ q := strings.ToLower(raw)
42
+ q = noiseWords.ReplaceAllString(q, " ")
43
+ q = regexp.MustCompile(`[-:.]`).ReplaceAllString(q, " ")
44
+ q = regexp.MustCompile(`\s{2,}`).ReplaceAllString(q, " ")
45
+ return strings.TrimSpace(q)
46
  }
47
 
48
+ // buildVJRegex builds a MongoDB regex pattern matching VJ-FILTER-BOT logic:
49
+ // - single word → word-boundary match
50
+ // - multi word → words connected by wildcard separators (space/dot/dash/+)
51
+ //
52
+ // Source: get_search_results() in VJ-FILTER-BOT/database/ia_filterdb.py
53
+ func buildVJRegex(query string) string {
54
+ query = strings.TrimSpace(query)
55
+ if query == "" {
56
+ return "."
 
 
57
  }
58
+ if !strings.Contains(query, " ") {
59
+ // Single word: match at word boundary or file-name separator
60
+ return `(\b|[.\+\-_])` + regexp.QuoteMeta(query) + `(\b|[.\+\-_])`
61
  }
62
+ // Multi-word: words connected by any separator (VJ exact logic)
63
+ parts := strings.Fields(query)
64
+ escaped := make([]string, len(parts))
65
+ for i, p := range parts {
66
+ escaped[i] = regexp.QuoteMeta(p)
 
67
  }
68
+ return strings.Join(escaped, `.*[\s\.\+\-_]`)
 
 
69
  }
70
 
71
+ // SearchVJ searches the file_name field using VJ-style regex — the primary hit driver.
72
+ // Falls back to text field for older manually-indexed docs.
73
+ func SearchVJ(chatID int64, query string, limit int64) []*database.Filter {
74
+ query = vjCleanQuery(query)
75
+ if query == "" {
76
+ return nil
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
 
 
78
 
79
+ pattern := buildVJRegex(query)
80
+ mongoFilter := bson.D{
81
+ {Key: "group_id", Value: chatID},
82
+ {Key: "$or", Value: bson.A{
83
+ bson.D{{Key: "file_name", Value: bson.M{"$regex": pattern, "$options": "i"}}},
84
+ bson.D{{Key: "text", Value: bson.M{"$regex": pattern, "$options": "i"}}},
85
+ }},
86
+ // Exclude ghost reference docs
87
+ {Key: "file", Value: bson.M{"$exists": true, "$ne": ""}},
88
  }
89
 
90
+ opts := options.Find().
91
+ SetSort(bson.D{{Key: "$natural", Value: -1}}). // newest first — matches VJ sort
92
+ SetLimit(limit)
93
 
94
+ cur, err := DB.Mcol.Find(context.Background(), mongoFilter, opts)
95
+ if err != nil {
96
+ fmt.Printf("SearchVJ: %v\n", err)
97
+ return nil
 
 
 
 
98
  }
99
+ defer cur.Close(context.Background())
100
 
101
+ var results []*database.Filter
102
+ for cur.Next(context.Background()) {
103
+ var f database.Filter
104
+ if err := cur.Decode(&f); err == nil {
105
+ results = append(results, &f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
  }
108
+ return results
109
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ // CountVJ returns total matching documents for pagination.
112
+ func CountVJ(chatID int64, query string) int64 {
113
+ query = vjCleanQuery(query)
114
+ if query == "" {
115
+ return 0
116
+ }
117
+ pattern := buildVJRegex(query)
118
+ mongoFilter := bson.D{
119
+ {Key: "group_id", Value: chatID},
120
+ {Key: "$or", Value: bson.A{
121
+ bson.D{{Key: "file_name", Value: bson.M{"$regex": pattern, "$options": "i"}}},
122
+ bson.D{{Key: "text", Value: bson.M{"$regex": pattern, "$options": "i"}}},
123
+ }},
124
+ {Key: "file", Value: bson.M{"$exists": true, "$ne": ""}},
125
  }
126
+ count, _ := DB.Mcol.CountDocuments(context.Background(), mongoFilter)
127
+ return count
128
  }
129
 
130
+ // ── Auto filter (group message handler) ──────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ // Filter is the main group message handler — the "give_filter" equivalent.
133
+ //
134
+ //nolint:errcheck
135
+ func Filter(bot *gotgbot.Bot, ctx *ext.Context) error {
136
+ msg := ctx.Message
137
+ if msg == nil {
138
  return nil
139
  }
140
 
141
+ chatID := msg.Chat.Id
142
+ messageID := msg.MessageId
143
+ query := strings.TrimSpace(msg.Text + msg.Caption)
144
 
145
+ // Ignore commands and very short queries
146
+ if strings.HasPrefix(query, "/") || strings.HasPrefix(query, "#") || len(query) < 2 {
147
+ return nil
148
  }
149
+ // Ignore very long messages (not a movie search)
150
+ if len(query) > 100 {
 
 
 
 
 
 
 
 
 
151
  return nil
152
  }
153
 
154
+ s := database.GetBotSettings()
 
 
 
 
 
 
 
155
 
156
+ // Check force sub
157
+ if len(s.ForceSubChats) > 0 {
158
+ if !CheckForceSub(bot, ctx) {
159
+ return nil
160
  }
161
  }
162
 
163
+ // Send "Searching For..." indicator — VJ style
164
+ searching, _ := msg.Reply(bot,
165
+ fmt.Sprintf("<b><i>Searching For %s 🔍</i></b>", query),
166
+ &gotgbot.SendMessageOpts{
167
+ ParseMode: gotgbot.ParseModeHTML,
168
+ ReplyParameters: &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true},
169
+ })
170
 
171
+ maxResults := int64(10)
172
+ results := SearchVJ(globalNumber, query, maxResults)
 
 
 
173
 
174
+ if len(results) == 0 {
175
+ // No results — show Google search suggestion
176
+ if searching != nil {
177
+ googleQuery := strings.ReplaceAll(query, " ", "+") + "+telegram+movie"
178
+ searching.EditText(bot,
179
+ fmt.Sprintf("❌ <b>No results found for:</b> <code>%s</code>\n\n💡 <i>Check spelling and try again.</i>", query),
180
+ &gotgbot.EditMessageTextOpts{
181
+ ParseMode: gotgbot.ParseModeHTML,
182
+ ReplyMarkup: gotgbot.InlineKeyboardMarkup{InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
183
+ {{Text: "🔍 Search on Google", Url: "https://www.google.com/search?q=" + googleQuery}},
184
+ }},
185
+ })
 
 
 
 
 
 
 
 
186
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return nil
 
 
188
  }
189
 
190
+ total := CountVJ(globalNumber, query)
 
 
 
 
 
191
 
192
+ // Dispatch to VJ-style paginated results
193
+ if searching != nil {
194
+ searching.Delete(bot, nil) //nolint:errcheck
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  }
196
+ sendVJResults(results, bot, msg, query, chatID, messageID)
197
  return nil
198
  }
199
 
200
+ // ── File sender ───────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ // sendFilter sends a single filter result to the chat.
203
+ //
204
+ //nolint:errcheck
205
+ func sendFilter(f *database.Filter, bot *gotgbot.Bot, update *gotgbot.Message, chatID, messageID int64) {
206
+ if f == nil {
207
+ return
 
 
 
 
 
 
208
  }
209
 
210
+ // Skip ghost reference docs
211
+ mediaType := f.MediaType
212
+ if mediaType == "reference" || (mediaType != "" && f.FileID == "") {
213
+ fmt.Printf("sendFilter: skipping ghost doc id=%s type=%s\n", f.ID, mediaType)
214
+ return
 
 
 
215
  }
216
 
217
+ markup := f.Markup
218
+ s := database.GetBotSettings()
 
 
 
 
 
 
 
 
 
 
219
 
220
+ content := f.Content
221
+ content = usernameRegex.ReplaceAllString(content, "")
 
 
 
 
 
222
 
223
+ // Build caption: use FileName if Content is empty
224
+ if content == "" && f.FileName != "" {
225
+ content = "<b>" + f.FileName + "</b>"
226
+ if f.FileSize > 0 {
227
+ content += fmt.Sprintf("\n📦 Size: %s", getFileSize(f.FileSize))
228
  }
229
  }
230
 
231
+ var (
232
+ buttons = mapToButton(markup)
233
+ ikm = gotgbot.InlineKeyboardMarkup{InlineKeyboard: buttons}
234
+ err error
235
+ m *gotgbot.Message
236
+ )
237
 
238
+ replyParams := &gotgbot.ReplyParameters{MessageId: messageID, AllowSendingWithoutReply: true}
 
 
 
239
 
240
+ if mediaType != "" {
241
+ fileID := gotgbot.InputFileByID(f.FileID)
242
+ switch mediaType {
243
+ case "document":
244
+ m, err = bot.SendDocument(chatID, fileID, &gotgbot.SendDocumentOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
245
+ case "video":
246
+ m, err = bot.SendVideo(chatID, fileID, &gotgbot.SendVideoOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
247
+ case "audio":
248
+ m, err = bot.SendAudio(chatID, fileID, &gotgbot.SendAudioOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
249
+ case "photo":
250
+ m, err = bot.SendPhoto(chatID, fileID, &gotgbot.SendPhotoOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
251
+ case "animation":
252
+ m, err = bot.SendAnimation(chatID, fileID, &gotgbot.SendAnimationOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
253
+ case "sticker":
254
+ m, err = bot.SendSticker(chatID, fileID, &gotgbot.SendStickerOpts{ReplyParameters: replyParams, ReplyMarkup: ikm})
255
+ default:
256
+ // Try as document for unknown media types
257
+ m, err = bot.SendDocument(chatID, fileID, &gotgbot.SendDocumentOpts{Caption: content, ReplyParameters: replyParams, ReplyMarkup: ikm, ParseMode: gotgbot.ParseModeHTML})
258
  }
259
+ } else {
260
+ m, err = update.Reply(bot, content, &gotgbot.SendMessageOpts{
261
+ ReplyParameters: replyParams,
262
+ ReplyMarkup: ikm,
263
+ ParseMode: gotgbot.ParseModeHTML,
264
+ })
265
  }
266
 
 
 
 
267
  if err != nil {
268
+ fmt.Printf("sendFilter err: %v\n", err)
269
+ return
270
  }
271
 
272
+ if m != nil && s.AutoDelete > 0 {
273
+ autodelete.InsertAutodel(autodelete.AutodelData{ChatID: chatID, MessageID: m.MessageId}, s.AutoDelete*60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
 
 
275
  }
276
 
277
+ // getFileSize returns human-readable file size.
278
+ func getFileSize(size int64) string {
279
+ switch {
280
+ case size >= 1024*1024*1024:
281
+ return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024))
282
+ case size >= 1024*1024:
283
+ return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024))
284
+ case size >= 1024:
285
+ return fmt.Sprintf("%.1f KB", float64(size)/1024)
286
+ default:
287
+ return fmt.Sprintf("%d B", size)
288
  }
289
  }
290
 
291
+ // ── CbSendFile — inline button file delivery ─────────────────────────────────
292
+
293
  // CbSendFile handles inline button clicks to send a specific file.
294
+ //
295
  //nolint:errcheck
296
  func CbSendFile(bot *gotgbot.Bot, ctx *ext.Context) error {
297
  update := ctx.CallbackQuery
298
+ data := update.Data
299
+
300
+ var fileDocID string
301
+ if strings.HasPrefix(data, "file(") && strings.HasSuffix(data, ")") {
302
+ fileDocID = data[5 : len(data)-1]
303
+ } else {
304
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "Invalid file data", ShowAlert: true})
305
  return nil
306
  }
307
+
308
+ // Lookup by _id
309
+ res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileDocID}})
310
+ if res.Err() != nil {
311
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "❌ File not found. It may have been deleted.", ShowAlert: true})
312
  return nil
313
  }
314
+
315
+ var f database.Filter
316
+ if err := res.Decode(&f); err != nil {
317
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{Text: "❌ Error loading file.", ShowAlert: true})
 
 
 
318
  return nil
319
  }
 
 
 
 
 
320
 
321
+ msg, ok := update.Message.(*gotgbot.Message)
322
+ if !ok {
323
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{})
324
+ return nil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
+ update.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{})
328
+ sendFilter(&f, bot, msg, msg.Chat.Id, msg.MessageId)
329
+ return nil
330
  }
331
 
332
+ // ── SendFileByID — start=file_xxx deep link handler ──────────────────────────
 
 
 
 
 
333
 
334
+ // SendFileByID finds a filter by its _id and sends it.
335
+ func SendFileByID(fileDocID string, bot *gotgbot.Bot, update *gotgbot.Message) {
336
+ res := DB.Mcol.FindOne(context.TODO(), bson.D{{Key: "_id", Value: fileDocID}})
337
+ if res.Err() != nil {
338
+ update.Reply(bot, "❌ File not found. It may have been deleted or the link is invalid.", &gotgbot.SendMessageOpts{})
339
+ return
 
 
 
 
 
340
  }
341
+ var f database.Filter
342
+ if err := res.Decode(&f); err != nil {
343
+ return
 
 
 
 
 
 
 
 
344
  }
345
+ sendFilter(&f, bot, update, update.Chat.Id, update.MessageId)
346
  }
347
 
348
+ // ── No-op callbacks ────────────────────────────────────────────────────────────
 
 
 
 
 
349
 
350
+ // CbPages handles the no-op "pages" callback (page number display button).
351
+ func CbPages(bot *gotgbot.Bot, ctx *ext.Context) error {
352
+ ctx.CallbackQuery.Answer(bot, &gotgbot.AnswerCallbackQueryOpts{}) //nolint:errcheck
353
+ return nil
354
+ }
355
 
356
+ // ── Helpers used by vjsearch.go ───────────────────────────────────────────────
357
+
358
+ // mapToButton converts [][]map[string]string markup to gotgbot keyboard rows.
359
+ func mapToButton(markup [][]map[string]string) [][]gotgbot.InlineKeyboardButton {
360
+ var rows [][]gotgbot.InlineKeyboardButton
361
+ for _, row := range markup {
362
+ var buttons []gotgbot.InlineKeyboardButton
363
+ for _, btn := range row {
364
+ text := btn["name"]
365
+ if url, ok := btn["url"]; ok && url != "" {
366
+ buttons = append(buttons, gotgbot.InlineKeyboardButton{Text: text, Url: url})
367
+ } else if cb, ok := btn["data"]; ok && cb != "" {
368
+ buttons = append(buttons, gotgbot.InlineKeyboardButton{Text: text, CallbackData: cb})
369
  }
 
 
370
  }
371
+ if len(buttons) > 0 {
372
+ rows = append(rows, buttons)
 
373
  }
 
 
374
  }
375
+ return rows
 
376
  }
377
 
378
+ // fileDisplayName returns the best display name for a filter, VJ-style:
379
+ // strips @username, URLs, shows [size] prefix like VJ does.
380
+ func fileDisplayName(f *database.Filter) string {
381
+ name := f.FileName
382
+ if name == "" {
383
+ name = strings.SplitN(f.Content, "\n", 2)[0]
384
+ name = usernameRegex.ReplaceAllString(name, "")
385
+ name = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(name, "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  }
387
+ if name == "" {
388
+ name = f.Text
 
 
 
 
389
  }
390
+ // Strip @usernames and URLs from display name — VJ filter
391
+ name = usernameRegex.ReplaceAllString(name, "")
392
+ name = regexp.MustCompile(`(?i)https?://\S+|t\.me/\S+|www\.\S+`).ReplaceAllString(name, "")
393
+ name = strings.TrimSpace(name)
394
 
395
+ // Add size prefix like VJ: "[800MB] Movie Name"
396
+ prefix := ""
397
+ if f.FileSize > 0 {
398
+ prefix = "[" + getFileSize(f.FileSize) + "] "
 
 
 
 
 
399
  }
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
+ full := prefix + name
402
+ if len(full) > 58 {
403
+ full = full[:55] + "..."
 
 
 
 
 
 
 
404
  }
405
+ return full
 
 
 
 
 
406
  }
plugins/index.go CHANGED
@@ -106,17 +106,11 @@ func AutoIndex(bot *gotgbot.Bot, ctx *ext.Context) error {
106
  return nil
107
  }
108
 
109
- // Skip if an identical global filter already exists
110
- if _, exists := DB.GetMfilter(globalNumber, keyword); exists {
111
- return nil
112
- }
113
-
114
  caption := ""
115
  if msg.Caption != "" {
116
  caption = msg.OriginalCaptionHTML()
117
  }
118
 
119
- // Extract file size and mime type
120
  var fileSize int64
121
  var mimeType string
122
  switch {
@@ -132,10 +126,10 @@ func AutoIndex(bot *gotgbot.Bot, ctx *ext.Context) error {
132
  }
133
 
134
  f := &database.Filter{
135
- ID: fileID, // Use Telegram file_id as document _id for deduplication
136
  ChatID: globalNumber,
137
  Text: keyword,
138
- FileName: rawName, // full original name
139
  Content: caption,
140
  FileID: fileID,
141
  MediaType: mediaType,
@@ -144,9 +138,14 @@ func AutoIndex(bot *gotgbot.Bot, ctx *ext.Context) error {
144
  Length: len(keyword),
145
  }
146
 
147
- DB.SaveMfilterByFileID(f)
148
-
149
- fmt.Printf("auto-index: saved %q from channel %d\n", rawName, msg.Chat.Id)
 
 
 
 
 
150
  return nil
151
  }
152
 
@@ -207,11 +206,6 @@ func IndexNow(bot *gotgbot.Bot, ctx *ext.Context) error {
207
  return nil
208
  }
209
 
210
- if _, exists := DB.GetMfilter(globalNumber, keyword); exists {
211
- msg.Reply(bot, fmt.Sprintf("⚠️ A global filter for <code>%s</code> already exists.", keyword), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
212
- return nil
213
- }
214
-
215
  caption := ""
216
  if target.Caption != "" {
217
  caption = target.OriginalCaptionHTML()
@@ -239,7 +233,6 @@ func IndexNow(bot *gotgbot.Bot, ctx *ext.Context) error {
239
  }
240
 
241
  f := &database.Filter{
242
- ID: fileID, // Use file_id as _id for deduplication
243
  ChatID: globalNumber,
244
  Text: keyword,
245
  FileName: origName,
@@ -251,10 +244,17 @@ func IndexNow(bot *gotgbot.Bot, ctx *ext.Context) error {
251
  Length: len(keyword),
252
  }
253
 
254
- DB.SaveMfilterByFileID(f)
255
-
 
 
 
 
 
 
 
256
  msg.Reply(bot,
257
- fmt.Sprintf("✅ Indexed as global filter: <code>%s</code>", keyword),
258
  &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
259
  )
260
  return nil
 
106
  return nil
107
  }
108
 
 
 
 
 
 
109
  caption := ""
110
  if msg.Caption != "" {
111
  caption = msg.OriginalCaptionHTML()
112
  }
113
 
 
114
  var fileSize int64
115
  var mimeType string
116
  switch {
 
126
  }
127
 
128
  f := &database.Filter{
129
+ // ID is generated by SaveMfilterByFileID (short, safe for Telegram deep links)
130
  ChatID: globalNumber,
131
  Text: keyword,
132
+ FileName: rawName,
133
  Content: caption,
134
  FileID: fileID,
135
  MediaType: mediaType,
 
138
  Length: len(keyword),
139
  }
140
 
141
+ inserted, err := DB.SaveMfilterByFileID(f)
142
+ if err != nil {
143
+ fmt.Printf("auto-index: save error: %v\n", err)
144
+ } else if inserted {
145
+ fmt.Printf("auto-index: ✅ indexed %q from channel %d\n", rawName, msg.Chat.Id)
146
+ } else {
147
+ fmt.Printf("auto-index: ⏭ skipped duplicate %q\n", rawName)
148
+ }
149
  return nil
150
  }
151
 
 
206
  return nil
207
  }
208
 
 
 
 
 
 
209
  caption := ""
210
  if target.Caption != "" {
211
  caption = target.OriginalCaptionHTML()
 
233
  }
234
 
235
  f := &database.Filter{
 
236
  ChatID: globalNumber,
237
  Text: keyword,
238
  FileName: origName,
 
244
  Length: len(keyword),
245
  }
246
 
247
+ inserted, err := DB.SaveMfilterByFileID(f)
248
+ if err != nil {
249
+ msg.Reply(bot, fmt.Sprintf("❌ Save error: %v", err), &gotgbot.SendMessageOpts{})
250
+ return nil
251
+ }
252
+ if !inserted {
253
+ msg.Reply(bot, fmt.Sprintf("⚠️ <code>%s</code> already indexed (duplicate file).", origName), &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML})
254
+ return nil
255
+ }
256
  msg.Reply(bot,
257
+ fmt.Sprintf("✅ Indexed: <code>%s</code>\nKeyword: <code>%s</code>", origName, keyword),
258
  &gotgbot.SendMessageOpts{ParseMode: gotgbot.ParseModeHTML},
259
  )
260
  return nil