Spaces:
Sleeping
Sleeping
Upload filter.go
#2
by Thaimozhi001 - opened
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 |
+
}
|