diff --git a/README.md b/README.md index 2b2fc20..e7815ff 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,8 @@ ![image](https://github.com/user-attachments/assets/57017af9-7ec1-41c6-b287-a8b2decd60f8) -## 项目简介 +## 项目功能 -这个项目主要功能: - -1. TeleGuard:一个 Telegram 机器人,用于管理群组中的关键词并自动删除包含这些关键词的消息。 -2. 币安价格更新器:定期获取并发送指定加密货币的价格信息。 -3. 链接拦截:拦截并撤回非白名单域名链接的第二次发送, 这里不去掉查询参数, 但是去掉头部的http协议。 - - -这些功能被整合到一个 Docker 容器中,可以同时运行。 - -## 功能特点 ### TeleGuard - 自动删除包含指定关键词的消息 @@ -28,44 +18,43 @@ ### 币安价格更新器 - 定期获取指定加密货币的价格信息 - 发送详细的价格更新,包括当前价格、24小时变化、高低点等 -- 可自定义更新频率和货币对 +- 可自定义货币对, 更新频率可自行在代码里修改 ### 链接拦截 +- 新增: 当非管理员时, 才会进行链接拦截 - 非白名单域名链接, 在发送第二次会被拦截撤回 ### 白名单域名 +- 当用户发送链接, 属于白名单域名, 则不进行操作. 如果不属于白名单域名, 则会第一次允许发送, 第二次进行撤回操作. - 会匹配链接中的域名, 包括二级域名和三级域名 - 例如,如果白名单中有 "example.com",它将匹配 "example.com"、"sub.example.com" 和 "sub.sub.example.com"。 - 同时,如果白名单中有 "sub.example.com",它将匹配 "sub.example.com" 和 "subsub.sub.example.com",但不会匹配 "example.com" 或 "othersub.example.com"。 +### 提示词自动回复 +- 当用户发送包含特定关键词的消息时,机器人将自动回复提示词。 +- 管理员通过`/prompt`进行设置, 支持添加, 删除, 列出. + +### 群组快捷管理 +- 管理员可以对成员消息回复`/ban`, 会进行以下处理: + 1. 将成员消息撤回, 无限期封禁成员, 并发送封禁通知 + 2. 在3分钟后, 撤回管理员指令消息和机器人的封禁通知 + ## 安装与配置 -1. 克隆此仓库到本地 -2. 确保已安装 Docker 和 Docker Compose -3. 使用 `docker-compose.yml` 文件构建和启动容器 +1. 确保服务器已安装 Docker 和 Docker Compose +2. 使用 `docker-compose.yml` 文件构建和启动容器 ## 使用方法 -1. 构建并启动 Docker 容器: - ``` - docker-compose up -d - ``` - -2. 查看日志: - ``` - docker-compose logs -f - ``` - -3. TeleGuard 命令: - - `/add 关键词`:添加新的关键词 - - `/delete 关键词`:删除现有的关键词 - - `/list`:列出所有当前的关键词 +构建并启动 Docker 容器: +``` +docker-compose up -d +``` ## 注意事项 - 确保 Telegram 机器人已被添加到目标群组,并被赋予管理员权限 -- 币安 API 可能有请求限制,请注意控制请求频率 - 定期检查日志以确保服务正常运行 ## 贡献 diff --git a/core/bot_commands.go b/core/bot_commands.go index 3b3259c..6c2d9fe 100644 --- a/core/bot_commands.go +++ b/core/bot_commands.go @@ -1,5 +1,6 @@ package core +// 注册命令 import ( "fmt" "log" @@ -28,9 +29,9 @@ func RegisterCommands(bot *tgbotapi.BotAPI) error { _, err := bot.Request(config) if err != nil { - return fmt.Errorf("failed to register bot commands: %w", err) + return fmt.Errorf("注册机器人命令失败: %w", err) } - log.Println("Bot commands registered successfully.") + log.Println("机器人命令注册成功。") return nil } diff --git a/core/config.go b/core/config.go deleted file mode 100644 index c4cffa0..0000000 --- a/core/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package core - -import ( - "os" - "path/filepath" -) - -var ( - BOT_TOKEN string - ADMIN_ID int64 - DB_FILE string - DEBUG_MODE bool -) - -func InitGlobalVariables(botToken string, adminID int64) { - BOT_TOKEN = botToken - ADMIN_ID = adminID - - // 设置数据库文件路径 - DB_FILE = filepath.Join("/app/data", "q58.db") - - // 从环境变量中读取调试模式设置 - DEBUG_MODE = os.Getenv("DEBUG_MODE") == "true" -} - -func IsAdmin(userID int64) bool { - return userID == ADMIN_ID -} diff --git a/core/database.go b/core/database.go index 83d5249..37caf6f 100644 --- a/core/database.go +++ b/core/database.go @@ -1,5 +1,6 @@ package core +//数据库处理 import ( "database/sql" "os" diff --git a/core/functions.go b/core/functions.go deleted file mode 100644 index 133c603..0000000 --- a/core/functions.go +++ /dev/null @@ -1,90 +0,0 @@ -package core - -import ( - "fmt" - "strings" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -// SendLongMessage sends a long message by splitting it into multiple messages if necessary -// SendLongMessage sends a long message by splitting it into multiple messages if necessary -func SendLongMessage(bot *tgbotapi.BotAPI, chatID int64, prefix string, items []string) error { - const maxMessageLength = 4000 // Leave some room for Telegram's message limit - - message := prefix + "\n" - for i, item := range items { - newLine := fmt.Sprintf("%d. %s\n", i+1, item) - if len(message)+len(newLine) > maxMessageLength { - msg := tgbotapi.NewMessage(chatID, message) - _, err := bot.Send(msg) - if err != nil { - return err - } - message = "" - } - message += newLine - } - - if message != "" { - msg := tgbotapi.NewMessage(chatID, message) - _, err := bot.Send(msg) - if err != nil { - return err - } - } - - return nil -} - -// SendLongMessageWithoutNumbering sends a long message without numbering the items -func SendLongMessageWithoutNumbering(bot *tgbotapi.BotAPI, chatID int64, prefix string, items []string) error { - const maxMessageLength = 4000 // Leave some room for Telegram's message limit - - message := prefix + "\n" - for _, item := range items { - newLine := item + "\n" - if len(message)+len(newLine) > maxMessageLength { - msg := tgbotapi.NewMessage(chatID, message) - _, err := bot.Send(msg) - if err != nil { - return err - } - message = "" - } - message += newLine - } - - if message != "" { - msg := tgbotapi.NewMessage(chatID, message) - _, err := bot.Send(msg) - if err != nil { - return err - } - } - - return nil -} - -// JoinLongMessage joins items into a single long message, splitting it if necessary -func JoinLongMessage(prefix string, items []string) []string { - const maxMessageLength = 4000 // Leave some room for Telegram's message limit - - var messages []string - message := prefix + "\n" - - for i, item := range items { - newLine := fmt.Sprintf("%d. %s\n", i+1, item) - if len(message)+len(newLine) > maxMessageLength { - messages = append(messages, strings.TrimSpace(message)) - message = "" - } - message += newLine - } - - if message != "" { - messages = append(messages, strings.TrimSpace(message)) - } - - return messages -} diff --git a/core/init.go b/core/init.go new file mode 100644 index 0000000..fb30bd9 --- /dev/null +++ b/core/init.go @@ -0,0 +1,84 @@ +package core + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var ( + Bot *tgbotapi.BotAPI + BOT_TOKEN string + ChatID int64 + ADMIN_ID int64 + Symbols []string + SingaporeTZ *time.Location + DB_FILE string + DEBUG_MODE bool + err error +) + +func IsAdmin(userID int64) bool { + return userID == ADMIN_ID +} +func mustParseInt64(s string) (int64, error) { + if s == "" { + return 0, fmt.Errorf("空字符串") + } + + value, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, fmt.Errorf("未能将'%s'解析为 int64: %v", s, err) + } + + return value, nil +} +func Init(botToken string, adminID int64) error { + + // 设置数据库文件路径 + DB_FILE = filepath.Join("/app/data", "q58.db") + + // 从环境变量中读取调试模式设置 + DEBUG_MODE = os.Getenv("DEBUG_MODE") == "true" + + // 设置时区 + loc := time.FixedZone("Asia/Singapore", 8*60*60) + time.Local = loc + + // 初始化 Chat ID + chatIDStr := os.Getenv("CHAT_ID") + ChatID, err = mustParseInt64(chatIDStr) + if err != nil { + log.Fatalf("Invalid CHAT_ID: %v", err) + } + + // 初始化 Symbols + symbolsRaw := strings.Split(os.Getenv("SYMBOLS"), ",") + Symbols = make([]string, len(symbolsRaw)) + for i, s := range symbolsRaw { + Symbols[i] = strings.ReplaceAll(s, "/", "") + } + + // 初始化新加坡时区 + SingaporeTZ, err = time.LoadLocation("Asia/Singapore") + if err != nil { + log.Printf("加载新加坡时区时出错: %v", err) + log.Println("回落至 UTC+8") + SingaporeTZ = time.FixedZone("Asia/Singapore", 8*60*60) + } + + // 初始化 Bot API + Bot, err = tgbotapi.NewBotAPI(botToken) + if err != nil { + log.Fatal(err) + } + + log.Printf("账户已授权 %s", Bot.Self.UserName) + return nil +} diff --git a/core/ratelimiter.go b/core/ratelimiter.go new file mode 100644 index 0000000..535c498 --- /dev/null +++ b/core/ratelimiter.go @@ -0,0 +1,41 @@ +package core + +import ( + "sync" + "time" +) + +// 为了简单, 直接把速率限制写死在这里 +const ( + maxCalls = 20 + period = time.Second +) + +type RateLimiter struct { + mu sync.Mutex + calls []time.Time +} + +func NewRateLimiter() *RateLimiter { + return &RateLimiter{ + calls: make([]time.Time, 0, maxCalls), + } +} + +func (r *RateLimiter) Allow() bool { + r.mu.Lock() + defer r.mu.Unlock() + + now := time.Now() + if len(r.calls) < maxCalls { + r.calls = append(r.calls, now) + return true + } + + if now.Sub(r.calls[0]) >= period { + r.calls = append(r.calls[1:], now) + return true + } + + return false +} diff --git a/main.go b/main.go index cc69652..445d5a7 100644 --- a/main.go +++ b/main.go @@ -5,26 +5,33 @@ import ( "os" "strconv" + "github.com/woodchen-ink/Q58Bot/core" "github.com/woodchen-ink/Q58Bot/service" + "github.com/woodchen-ink/Q58Bot/service/binance" ) func main() { log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) botToken := os.Getenv("BOT_TOKEN") + adminIDStr := os.Getenv("ADMIN_ID") adminID, err := strconv.ParseInt(adminIDStr, 10, 64) if err != nil { - log.Fatalf("Invalid ADMIN_ID: %v", err) + log.Fatalf("Failed to get ADMIN_ID: %v", err) } - err = service.Init(botToken, adminID) + err = core.Init(botToken, adminID) if err != nil { log.Fatalf("Failed to initialize service: %v", err) } - go service.RunGuard() - go service.RunBinance() + err = service.RunMessageHandler() + if err != nil { + log.Fatalf("Error in RunMessageHandler: %v", err) + } + + go binance.RunBinance() select {} } diff --git a/service/binance.go b/service/binance/binance.go similarity index 78% rename from service/binance.go rename to service/binance/binance.go index 837bdf7..26a9e3e 100644 --- a/service/binance.go +++ b/service/binance/binance.go @@ -1,12 +1,11 @@ -package service +package binance +//币安价格推送 import ( "context" "fmt" "log" - "os" "strconv" - "strings" "time" "github.com/adshao/go-binance/v2" @@ -22,38 +21,6 @@ var ( singaporeTZ *time.Location ) -func init() { - var err error - botToken = os.Getenv("BOT_TOKEN") - chatID = mustParseInt64(os.Getenv("CHAT_ID")) - - symbolsRaw := strings.Split(os.Getenv("SYMBOLS"), ",") - symbols = make([]string, len(symbolsRaw)) - for i, s := range symbolsRaw { - symbols[i] = strings.ReplaceAll(s, "/", "") - } - - singaporeTZ, err = time.LoadLocation("Asia/Singapore") - if err != nil { - log.Printf("Error loading Singapore time zone: %v", err) - log.Println("Falling back to UTC+8") - singaporeTZ = time.FixedZone("Asia/Singapore", 8*60*60) - } - - bot, err = tgbotapi.NewBotAPI(botToken) - if err != nil { - log.Fatal(err) - } -} - -func mustParseInt64(s string) int64 { - i, err := strconv.ParseInt(s, 10, 64) - if err != nil { - log.Fatal(err) - } - return i -} - type tickerInfo struct { symbol string last float64 diff --git a/service/group_member_management/group_member_management.go b/service/group_member_management/group_member_management.go new file mode 100644 index 0000000..4facc07 --- /dev/null +++ b/service/group_member_management/group_member_management.go @@ -0,0 +1,70 @@ +package group_member_management + +import ( + "fmt" + "log" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/woodchen-ink/Q58Bot/core" +) + +func HandleBanCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message) { + // 检查是否是管理员 + if !core.IsAdmin(message.From.ID) { + return + } + + // 检查是否是回复消息 + if message.ReplyToMessage == nil { + return + } + + chatID := message.Chat.ID + userToBan := message.ReplyToMessage.From + + // 立即删除被回复的原消息 + deleteConfig := tgbotapi.NewDeleteMessage(chatID, message.ReplyToMessage.MessageID) + _, err := bot.Request(deleteConfig) + if err != nil { + log.Printf("删除原消息时出错: %v", err) + } + + // 踢出用户 + kickChatMemberConfig := tgbotapi.KickChatMemberConfig{ + ChatMemberConfig: tgbotapi.ChatMemberConfig{ + ChatID: chatID, + UserID: userToBan.ID, + }, + UntilDate: 0, // 0 means ban forever + } + + _, err = bot.Request(kickChatMemberConfig) + if err != nil { + log.Printf("禁止用户时出错: %v", err) + return + } + + // 发送提示消息 + banMessage := fmt.Sprintf("用户 %s 已被封禁并踢出群组。", userToBan.UserName) + msg := tgbotapi.NewMessage(chatID, banMessage) + sentMsg, err := bot.Send(msg) + if err != nil { + log.Printf("发送禁止消息时出错: %v", err) + return + } + + // 3分钟后删除机器人的消息和管理员的指令消息 + go deleteMessagesAfterDelay(bot, chatID, []int{sentMsg.MessageID, message.MessageID}, 3*time.Minute) +} + +func deleteMessagesAfterDelay(bot *tgbotapi.BotAPI, chatID int64, messageIDs []int, delay time.Duration) { + time.Sleep(delay) + for _, msgID := range messageIDs { + deleteConfig := tgbotapi.NewDeleteMessage(chatID, msgID) + _, err := bot.Request(deleteConfig) + if err != nil { + log.Printf("删除消息 %d 时出错: %v", msgID, err) + } + } +} diff --git a/service/guard.go b/service/guard.go deleted file mode 100644 index 223cc8f..0000000 --- a/service/guard.go +++ /dev/null @@ -1,185 +0,0 @@ -package service - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/woodchen-ink/Q58Bot/core" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -type RateLimiter struct { - mu sync.Mutex - maxCalls int - period time.Duration - calls []time.Time -} - -func NewRateLimiter(maxCalls int, period time.Duration) *RateLimiter { - return &RateLimiter{ - maxCalls: maxCalls, - period: period, - calls: make([]time.Time, 0, maxCalls), - } -} - -func (r *RateLimiter) Allow() bool { - r.mu.Lock() - defer r.mu.Unlock() - - now := time.Now() - if len(r.calls) < r.maxCalls { - r.calls = append(r.calls, now) - return true - } - - if now.Sub(r.calls[0]) >= r.period { - r.calls = append(r.calls[1:], now) - return true - } - - return false -} - -func deleteMessageAfterDelay(bot *tgbotapi.BotAPI, chatID int64, messageID int, delay time.Duration) { - time.Sleep(delay) - deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) - _, err := bot.Request(deleteMsg) - if err != nil { - log.Printf("Failed to delete message: %v", err) - } -} - -func RunGuard() { - baseDelay := time.Second - maxDelay := 5 * time.Minute - delay := baseDelay - - for { - err := startBot() - if err != nil { - log.Printf("Bot encountered an error: %v", err) - log.Printf("Attempting to restart in %v...", delay) - time.Sleep(delay) - - delay *= 2 - if delay > maxDelay { - delay = maxDelay - } - } else { - delay = baseDelay - log.Println("Bot disconnected. Attempting to restart immediately...") - } - } -} - -func startBot() error { - bot, err := tgbotapi.NewBotAPI(core.BOT_TOKEN) - if err != nil { - return fmt.Errorf("failed to create bot: %w", err) - } - - bot.Debug = core.DEBUG_MODE - - log.Printf("Authorized on account %s", bot.Self.UserName) - - err = core.RegisterCommands(bot) - if err != nil { - return fmt.Errorf("error registering commands: %w", err) - } - - linkFilter, err := NewLinkFilter() - if err != nil { - return fmt.Errorf("failed to create LinkFilter: %v", err) - } - - rateLimiter := NewRateLimiter(10, time.Second) - - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 - - updates := bot.GetUpdatesChan(u) - - for update := range updates { - go handleUpdate(bot, update, linkFilter, rateLimiter) - } - - return nil -} - -func handleUpdate(bot *tgbotapi.BotAPI, update tgbotapi.Update, linkFilter *LinkFilter, rateLimiter *RateLimiter) { - if update.Message == nil { - return - } - - if update.Message.Chat.Type == "private" && update.Message.From.ID == core.ADMIN_ID { - handleAdminCommand(bot, update.Message, linkFilter) - return - } - - if update.Message.Chat.Type != "private" && rateLimiter.Allow() { - processMessage(bot, update.Message, linkFilter) - } -} - -func handleAdminCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, linkFilter *LinkFilter) { - command := message.Command() - args := message.CommandArguments() - - switch command { - case "add", "delete", "list", "deletecontaining": - linkFilter.HandleKeywordCommand(bot, message, command, args) - case "addwhite", "delwhite", "listwhite": - linkFilter.HandleWhitelistCommand(bot, message, command, args) - case "prompt": - HandlePromptCommand(bot, message) - default: - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "未知命令")) - } -} - -func processMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message, linkFilter *LinkFilter) { - log.Printf("Processing message: %s", message.Text) - shouldFilter, newLinks := linkFilter.ShouldFilter(message.Text) - if shouldFilter { - log.Printf("Message should be filtered: %s", message.Text) - if message.From.ID != core.ADMIN_ID { - // 删除原始消息 - deleteMsg := tgbotapi.NewDeleteMessage(message.Chat.ID, message.MessageID) - _, err := bot.Request(deleteMsg) - if err != nil { - log.Printf("Failed to delete message: %v", err) - } - - // 发送提示消息 - notification := tgbotapi.NewMessage(message.Chat.ID, "已撤回该消息。注:一个链接不能发两次.") - sent, err := bot.Send(notification) - if err != nil { - log.Printf("Failed to send notification: %v", err) - } else { - // 3分钟后删除提示消息 - go deleteMessageAfterDelay(bot, message.Chat.ID, sent.MessageID, 3*time.Minute) - } - } - return - } - if len(newLinks) > 0 { - log.Printf("New non-whitelisted links found: %v", newLinks) - } - - // 检查并回复提示词 - if reply, found := GetPromptReply(message.Text); found { - replyMsg := tgbotapi.NewMessage(message.Chat.ID, reply) - replyMsg.ReplyToMessageID = message.MessageID - sent, err := bot.Send(replyMsg) - if err != nil { - log.Printf("Failed to send prompt reply: %v", err) - } else { - // 3分钟后删除提示词回复 - go deleteMessageAfterDelay(bot, message.Chat.ID, sent.MessageID, 3*time.Minute) - } - } -} diff --git a/service/init.go b/service/init.go deleted file mode 100644 index 4b646c4..0000000 --- a/service/init.go +++ /dev/null @@ -1,23 +0,0 @@ -package service - -import ( - "time" - - "github.com/woodchen-ink/Q58Bot/core" -) - -func Init(botToken string, adminID int64) error { - core.InitGlobalVariables(botToken, adminID) - - // 初始化提示词服务 - err := InitPromptService() - if err != nil { - return err - } - - // 设置时区 - loc := time.FixedZone("Asia/Singapore", 8*60*60) - time.Local = loc - - return nil -} diff --git a/service/link_filter.go b/service/link_filter.go deleted file mode 100644 index 52ce835..0000000 --- a/service/link_filter.go +++ /dev/null @@ -1,311 +0,0 @@ -package service - -import ( - "fmt" - "log" - "net/url" - "regexp" - "strings" - - "github.com/woodchen-ink/Q58Bot/core" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -var logger = log.New(log.Writer(), "LinkFilter: ", log.Ldate|log.Ltime|log.Lshortfile) - -type LinkFilter struct { - db *core.Database - keywords []string - whitelist []string - linkPattern *regexp.Regexp -} - -func NewLinkFilter() (*LinkFilter, error) { - db, err := core.NewDatabase() - if err != nil { - return nil, err - } - lf := &LinkFilter{ - db: db, - } - lf.linkPattern = regexp.MustCompile(`(?i)\b(?:(?:https?://)?(?:(?:www\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:t\.me|telegram\.me))(?:/[^\s]*)?)`) - err = lf.LoadDataFromFile() - if err != nil { - return nil, err - } - return lf, nil -} - -func (lf *LinkFilter) LoadDataFromFile() error { - var err error - lf.keywords, err = lf.db.GetAllKeywords() - if err != nil { - return err - } - lf.whitelist, err = lf.db.GetAllWhitelist() - if err != nil { - return err - } - logger.Printf("Loaded %d keywords and %d whitelist entries from database", len(lf.keywords), len(lf.whitelist)) - return nil -} - -func (lf *LinkFilter) NormalizeLink(link string) string { - link = regexp.MustCompile(`^https?://`).ReplaceAllString(link, "") - link = strings.TrimPrefix(link, "/") - parsedURL, err := url.Parse("http://" + link) - if err != nil { - logger.Printf("Error parsing URL: %v", err) - return link - } - normalized := fmt.Sprintf("%s%s", parsedURL.Hostname(), parsedURL.EscapedPath()) - if parsedURL.RawQuery != "" { - normalized += "?" + parsedURL.RawQuery - } - result := strings.TrimSuffix(normalized, "/") - logger.Printf("Normalized link: %s -> %s", link, result) - return result -} - -func (lf *LinkFilter) ExtractDomain(urlStr string) string { - parsedURL, err := url.Parse(urlStr) - if err != nil { - logger.Printf("Error parsing URL: %v", err) - return urlStr - } - return strings.ToLower(parsedURL.Hostname()) -} - -func (lf *LinkFilter) domainMatch(domain, whiteDomain string) bool { - domainParts := strings.Split(domain, ".") - whiteDomainParts := strings.Split(whiteDomain, ".") - - if len(domainParts) < len(whiteDomainParts) { - return false - } - - for i := 1; i <= len(whiteDomainParts); i++ { - if domainParts[len(domainParts)-i] != whiteDomainParts[len(whiteDomainParts)-i] { - return false - } - } - - return true -} -func (lf *LinkFilter) IsWhitelisted(link string) bool { - domain := lf.ExtractDomain(link) - for _, whiteDomain := range lf.whitelist { - if lf.domainMatch(domain, whiteDomain) { - logger.Printf("Whitelist check for %s: Passed (matched %s)", link, whiteDomain) - return true - } - } - logger.Printf("Whitelist check for %s: Failed", link) - return false -} - -func (lf *LinkFilter) AddKeyword(keyword string) error { - if lf.linkPattern.MatchString(keyword) { - keyword = lf.NormalizeLink(keyword) - } - keyword = strings.TrimPrefix(keyword, "/") - for _, k := range lf.keywords { - if k == keyword { - logger.Printf("Keyword already exists: %s", keyword) - return nil - } - } - err := lf.db.AddKeyword(keyword) - if err != nil { - return err - } - logger.Printf("New keyword added: %s", keyword) - return lf.LoadDataFromFile() -} - -func (lf *LinkFilter) RemoveKeyword(keyword string) bool { - for _, k := range lf.keywords { - if k == keyword { - lf.db.RemoveKeyword(keyword) - lf.LoadDataFromFile() - return true - } - } - return false -} - -func (lf *LinkFilter) RemoveKeywordsContaining(substring string) ([]string, error) { - removed, err := lf.db.RemoveKeywordsContaining(substring) - if err != nil { - return nil, err - } - err = lf.LoadDataFromFile() - if err != nil { - return nil, err - } - return removed, nil -} - -func (lf *LinkFilter) ShouldFilter(text string) (bool, []string) { - logger.Printf("Checking text: %s", text) - for _, keyword := range lf.keywords { - if strings.Contains(strings.ToLower(text), strings.ToLower(keyword)) { - logger.Printf("Text contains keyword: %s", text) - return true, nil - } - } - - links := lf.linkPattern.FindAllString(text, -1) - logger.Printf("Found links: %v", links) - - var newNonWhitelistedLinks []string - for _, link := range links { - normalizedLink := lf.NormalizeLink(link) - if !lf.IsWhitelisted(normalizedLink) { - logger.Printf("Link not whitelisted: %s", normalizedLink) - found := false - for _, keyword := range lf.keywords { - if keyword == normalizedLink { - logger.Printf("Existing keyword found: %s", normalizedLink) - return true, nil - } - } - if !found { - newNonWhitelistedLinks = append(newNonWhitelistedLinks, normalizedLink) - lf.AddKeyword(normalizedLink) - } - } - } - - if len(newNonWhitelistedLinks) > 0 { - logger.Printf("New non-whitelisted links found: %v", newNonWhitelistedLinks) - } - return false, newNonWhitelistedLinks -} - -func (lf *LinkFilter) HandleKeywordCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string) { - switch command { - case "list": - keywords, err := lf.db.GetAllKeywords() - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "获取关键词列表时发生错误。")) - return - } - if len(keywords) == 0 { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "关键词列表为空。")) - } else { - core.SendLongMessage(bot, message.Chat.ID, "当前关键词列表:", keywords) - } - case "add": - if args != "" { - keyword := args - exists, err := lf.db.KeywordExists(keyword) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "检查关键词时发生错误。")) - return - } - if !exists { - err = lf.AddKeyword(keyword) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "添加关键词时发生错误。")) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已添加。", keyword))) - } - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已存在。", keyword))) - } - } - - case "delete": - if args != "" { - keyword := args - if lf.RemoveKeyword(keyword) { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已删除。", keyword))) - } else { - similarKeywords, err := lf.db.SearchKeywords(keyword) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "搜索关键词时发生错误。")) - return - } - if len(similarKeywords) > 0 { - core.SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("未找到精确匹配的关键词 '%s'。\n\n以下是相似的关键词:", keyword), similarKeywords) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 不存在。", keyword))) - } - } - } - case "deletecontaining": - if args != "" { - substring := args - removedKeywords, err := lf.RemoveKeywordsContaining(substring) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "删除关键词时发生错误。")) - return - } - if len(removedKeywords) > 0 { - core.SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("已删除包含 '%s' 的以下关键词:", substring), removedKeywords) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("没有找到包含 '%s' 的关键词。", substring))) - } - } - default: - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。")) - } -} - -func (lf *LinkFilter) HandleWhitelistCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string) { - switch command { - case "listwhite": - whitelist, err := lf.db.GetAllWhitelist() - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "获取白名单时发生错误。")) - return - } - if len(whitelist) == 0 { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "白名单为空。")) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "白名单域名列表:\n"+strings.Join(whitelist, "\n"))) - } - case "addwhite": - if args != "" { - domain := strings.ToLower(args) - exists, err := lf.db.WhitelistExists(domain) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "检查白名单时发生错误。")) - return - } - if !exists { - err = lf.db.AddWhitelist(domain) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "添加到白名单时发生错误。")) - return - } - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已添加到白名单。", domain))) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已在白名单中。", domain))) - } - } - case "delwhite": - if args != "" { - domain := strings.ToLower(args) - exists, err := lf.db.WhitelistExists(domain) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "检查白名单时发生错误。")) - return - } - if exists { - err = lf.db.RemoveWhitelist(domain) - if err != nil { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "从白名单删除时发生错误。")) - return - } - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已从白名单中删除。", domain))) - } else { - bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 不在白名单中。", domain))) - } - } - default: - bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。")) - } -} diff --git a/service/link_filter/link_filter.go b/service/link_filter/link_filter.go new file mode 100644 index 0000000..433c33b --- /dev/null +++ b/service/link_filter/link_filter.go @@ -0,0 +1,240 @@ +package link_filter + +// 链接处理 +import ( + "fmt" + "log" + "net/url" + "regexp" + "strings" + + "github.com/woodchen-ink/Q58Bot/core" +) + +var logger = log.New(log.Writer(), "LinkFilter: ", log.Ldate|log.Ltime|log.Lshortfile) + +type LinkFilter struct { + db *core.Database + keywords []string + whitelist []string + linkPattern *regexp.Regexp +} + +// NewLinkFilter 创建一个新的LinkFilter实例。这个实例用于过滤链接,且在创建时会初始化数据库连接和链接过滤正则表达式。 +// 它首先尝试创建一个数据库连接,然后加载链接过滤所需的配置,最后返回一个包含所有初始化设置的LinkFilter实例。 +// 如果在任何步骤中发生错误,错误将被返回,LinkFilter实例将不会被创建。 +func NewLinkFilter() (*LinkFilter, error) { + // 初始化数据库连接 + db, err := core.NewDatabase() + if err != nil { + return nil, err + } + // 创建LinkFilter实例 + lf := &LinkFilter{ + db: db, + } + // 编译链接过滤正则表达式 + lf.linkPattern = regexp.MustCompile(`(?i)\b(?:(?:https?://)?(?:(?:www\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:t\.me|telegram\.me))(?:/[^\s]*)?)`) + // 从文件中加载额外的链接过滤数据 + err = lf.LoadDataFromFile() + if err != nil { + return nil, err + } + return lf, nil +} + +// LoadDataFromFile 从文件中加载数据到 LinkFilter 结构体的 keywords 和 whitelist 字段。 +// 它首先从数据库中获取所有的关键词和白名单条目,如果数据库操作出现错误,它会立即返回错误。 +// 一旦数据成功加载,它会通过日志记录加载的关键词和白名单条目的数量。 +// 参数: 无 +// 返回值: 如果加载过程中发生错误,返回该错误;否则返回 nil。 +func (lf *LinkFilter) LoadDataFromFile() error { + // 从数据库中加载所有关键词到 lf.keywords + var err error + lf.keywords, err = lf.db.GetAllKeywords() + if err != nil { + // 如果发生错误,立即返回 + return err + } + + // 从数据库中加载所有白名单条目到 lf.whitelist + lf.whitelist, err = lf.db.GetAllWhitelist() + if err != nil { + // 如果发生错误,立即返回 + return err + } + + // 记录成功加载的关键词和白名单条目的数量 + logger.Printf("Loaded %d keywords and %d whitelist entries from database", len(lf.keywords), len(lf.whitelist)) + + // 数据加载成功,返回 nil + return nil +} + +// NormalizeLink 标准化链接地址。 +// +// 该函数接受一个链接字符串,对其进行标准化处理,并返回处理后的链接。 +// 标准化过程包括移除协议头(http或https)、TrimPrefix去除链接中的斜杠、 +// 解析URL以获取主机名和路径、将查询参数附加到URL末尾。 +// +// 参数: +// +// link - 需要被标准化的链接字符串。 +// +// 返回值: +// +// 标准化后的链接字符串。 +func (lf *LinkFilter) NormalizeLink(link string) string { + // 移除链接中的协议头(http或https) + link = regexp.MustCompile(`^https?://`).ReplaceAllString(link, "") + // 去除链接中的斜杠 + link = strings.TrimPrefix(link, "/") + // 解析URL,此处默认使用http协议,因为协议头部已被移除 + parsedURL, err := url.Parse("http://" + link) + if err != nil { + // 如果URL解析失败,记录错误信息,并返回原始链接 + logger.Printf("Error parsing URL: %v", err) + return link + } + // 构建标准化的URL,包含主机名和转义后的路径 + normalized := fmt.Sprintf("%s%s", parsedURL.Hostname(), parsedURL.EscapedPath()) + // 如果URL有查询参数,将其附加到标准化的URL后面 + if parsedURL.RawQuery != "" { + normalized += "?" + parsedURL.RawQuery + } + // 移除标准化URL末尾的斜杠(如果有) + result := strings.TrimSuffix(normalized, "/") + // 记录标准化后的链接信息 + logger.Printf("Normalized link: %s -> %s", link, result) + return result +} + +// ExtractDomain 从给定的URL字符串中提取域名。 +// 该函数首先解析URL字符串,然后返回解析得到的主机名,同时将其转换为小写。 +// 如果URL解析失败,错误信息将被记录,并且函数会返回原始的URL字符串。 +// 参数: +// +// urlStr - 待处理的URL字符串。 +// +// 返回值: +// +// 解析后的主机名,如果解析失败则返回原始的URL字符串。 +func (lf *LinkFilter) ExtractDomain(urlStr string) string { + // 尝试解析给定的URL字符串。 + parsedURL, err := url.Parse(urlStr) + if err != nil { + // 如果解析过程中出现错误,记录错误信息并返回原始URL字符串。 + logger.Printf("Error parsing URL: %v", err) + return urlStr + } + // 返回解析得到的主机名,转换为小写。 + return strings.ToLower(parsedURL.Hostname()) +} + +func (lf *LinkFilter) domainMatch(domain, whiteDomain string) bool { + domainParts := strings.Split(domain, ".") + whiteDomainParts := strings.Split(whiteDomain, ".") + + if len(domainParts) < len(whiteDomainParts) { + return false + } + + for i := 1; i <= len(whiteDomainParts); i++ { + if domainParts[len(domainParts)-i] != whiteDomainParts[len(whiteDomainParts)-i] { + return false + } + } + + return true +} +func (lf *LinkFilter) IsWhitelisted(link string) bool { + domain := lf.ExtractDomain(link) + for _, whiteDomain := range lf.whitelist { + if lf.domainMatch(domain, whiteDomain) { + logger.Printf("Whitelist check for %s: Passed (matched %s)", link, whiteDomain) + return true + } + } + logger.Printf("Whitelist check for %s: Failed", link) + return false +} + +func (lf *LinkFilter) AddKeyword(keyword string) error { + if lf.linkPattern.MatchString(keyword) { + keyword = lf.NormalizeLink(keyword) + } + keyword = strings.TrimPrefix(keyword, "/") + for _, k := range lf.keywords { + if k == keyword { + logger.Printf("Keyword already exists: %s", keyword) + return nil + } + } + err := lf.db.AddKeyword(keyword) + if err != nil { + return err + } + logger.Printf("New keyword added: %s", keyword) + return lf.LoadDataFromFile() +} + +func (lf *LinkFilter) RemoveKeyword(keyword string) bool { + for _, k := range lf.keywords { + if k == keyword { + lf.db.RemoveKeyword(keyword) + lf.LoadDataFromFile() + return true + } + } + return false +} + +func (lf *LinkFilter) RemoveKeywordsContaining(substring string) ([]string, error) { + removed, err := lf.db.RemoveKeywordsContaining(substring) + if err != nil { + return nil, err + } + err = lf.LoadDataFromFile() + if err != nil { + return nil, err + } + return removed, nil +} + +// 检查消息是否包含关键词或者非白名单链接 +func (lf *LinkFilter) ShouldFilter(text string) (bool, []string) { + logger.Printf("Checking text: %s", text) + for _, keyword := range lf.keywords { + if strings.Contains(strings.ToLower(text), strings.ToLower(keyword)) { + logger.Printf("文字包含关键字: %s", text) + return true, nil + } + } + + links := lf.linkPattern.FindAllString(text, -1) + logger.Printf("找到链接: %v", links) + + var newNonWhitelistedLinks []string + for _, link := range links { + normalizedLink := lf.NormalizeLink(link) + if !lf.IsWhitelisted(normalizedLink) { + logger.Printf("链接未列入白名单: %s", normalizedLink) + found := false + for _, keyword := range lf.keywords { + if keyword == normalizedLink { + logger.Printf("找到现有关键字: %s", normalizedLink) + return true, nil + } + } + if !found { + newNonWhitelistedLinks = append(newNonWhitelistedLinks, normalizedLink) + lf.AddKeyword(normalizedLink) + } + } + } + + if len(newNonWhitelistedLinks) > 0 { + logger.Printf("发现新的非白名单链接: %v", newNonWhitelistedLinks) + } + return false, newNonWhitelistedLinks +} diff --git a/service/message_handler.go b/service/message_handler.go new file mode 100644 index 0000000..a9f135b --- /dev/null +++ b/service/message_handler.go @@ -0,0 +1,175 @@ +// 消息处理函数 +package service + +import ( + "fmt" + "log" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/woodchen-ink/Q58Bot/core" + "github.com/woodchen-ink/Q58Bot/service/group_member_management" + "github.com/woodchen-ink/Q58Bot/service/link_filter" + "github.com/woodchen-ink/Q58Bot/service/prompt_reply" +) + +// handleUpdate 处理所有传入的更新信息,包括消息和命令, 然后分开处理。 +func handleUpdate(bot *tgbotapi.BotAPI, update tgbotapi.Update, linkFilter *link_filter.LinkFilter, rateLimiter *core.RateLimiter, db *core.Database) { + // 检查更新是否包含消息,如果不包含则直接返回。 + if update.Message == nil { + return + } + + // 如果消息来自私聊且发送者是预定义的管理员,调用处理管理员命令的函数。 + if update.Message.Chat.Type == "private" && update.Message.From.ID == core.ADMIN_ID { + handleAdminCommand(bot, update.Message, db) + return + } + + // 如果消息来自群聊且通过了速率限制器的检查,调用处理普通消息的函数。 + if update.Message.Chat.Type != "private" && rateLimiter.Allow() { + processMessage(bot, update.Message, linkFilter) + } +} + +// 处理管理员私聊消息 +func handleAdminCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, db *core.Database) { + command := message.Command() + args := message.CommandArguments() + + switch command { + case "add", "delete", "list", "deletecontaining": + HandleKeywordCommand(bot, message, command, args, db) + case "addwhite", "delwhite", "listwhite": + HandleWhitelistCommand(bot, message, command, args, db) + case "prompt": + prompt_reply.HandlePromptCommand(bot, message) + default: + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "未知命令, 听不懂")) + } +} + +// processMessage 处理群里接收到的消息。 +func processMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message, linkFilter *link_filter.LinkFilter) { + // 记录消息内容 + log.Printf("Processing message: %s", message.Text) + + // 处理 /ban 命令 + if message.ReplyToMessage != nil && message.Text == "/ban" { + group_member_management.HandleBanCommand(bot, message) + return + } + + // 如果不是管理员,才进行链接过滤 + if !core.IsAdmin(message.From.ID) { + // 判断消息是否应当被过滤及找出新的非白名单链接 + shouldFilter, newLinks := linkFilter.ShouldFilter(message.Text) + if shouldFilter { + // 记录被过滤的消息 + log.Printf("消息应该被过滤: %s", message.Text) + // 删除原始消息 + deleteMsg := tgbotapi.NewDeleteMessage(message.Chat.ID, message.MessageID) + _, err := bot.Request(deleteMsg) + if err != nil { + // 删除消息失败时记录错误 + log.Printf("删除消息失败: %v", err) + } + + // 发送提示消息 + notification := tgbotapi.NewMessage(message.Chat.ID, "已撤回该消息。注:一个链接不能发两次.") + sent, err := bot.Send(notification) + if err != nil { + // 发送通知失败时记录错误 + log.Printf("发送通知失败: %v", err) + } else { + // 3分钟后删除提示消息 + go deleteMessageAfterDelay(bot, message.Chat.ID, sent.MessageID, 3*time.Minute) + } + // 结束处理 + return + } + // 如果发现新的非白名单链接 + if len(newLinks) > 0 { + // 记录新的非白名单链接 + log.Printf("发现新的非白名单链接: %v", newLinks) + } + } + + // 检查消息文本是否匹配预设的提示词并回复 + if reply, found := prompt_reply.GetPromptReply(message.Text); found { + // 创建回复消息 + replyMsg := tgbotapi.NewMessage(message.Chat.ID, reply) + replyMsg.ReplyToMessageID = message.MessageID + sent, err := bot.Send(replyMsg) + if err != nil { + // 发送回复失败时记录错误 + log.Printf("未能发送及时回复: %v", err) + } else { + // 3分钟后删除回复消息 + go deleteMessageAfterDelay(bot, message.Chat.ID, sent.MessageID, 3*time.Minute) + } + } +} + +func RunMessageHandler() error { + log.Println("消息处理器启动...") + + baseDelay := time.Second + maxDelay := 5 * time.Minute + delay := baseDelay + db, err := core.NewDatabase() + if err != nil { + return fmt.Errorf("failed to initialize database: %w", err) + } + defer db.Close() // 确保在函数结束时关闭数据库连接 + + for { + err := func() error { + bot, err := tgbotapi.NewBotAPI(core.BOT_TOKEN) + if err != nil { + return fmt.Errorf("failed to create bot: %w", err) + } + + bot.Debug = core.DEBUG_MODE + + log.Printf("Authorized on account %s", bot.Self.UserName) + + err = core.RegisterCommands(bot) + if err != nil { + return fmt.Errorf("error registering commands: %w", err) + } + + linkFilter, err := link_filter.NewLinkFilter() + if err != nil { + return fmt.Errorf("failed to create LinkFilter: %v", err) + } + + rateLimiter := core.NewRateLimiter() + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + + for update := range updates { + go handleUpdate(bot, update, linkFilter, rateLimiter, db) + } + + return nil + }() + + if err != nil { + log.Printf("Bot encountered an error: %v", err) + log.Printf("Attempting to restart in %v...", delay) + time.Sleep(delay) + + delay *= 2 + if delay > maxDelay { + delay = maxDelay + } + } else { + delay = baseDelay + log.Println("Bot disconnected. Attempting to restart immediately...") + } + } +} diff --git a/service/message_handler_utils.go b/service/message_handler_utils.go new file mode 100644 index 0000000..df90f6b --- /dev/null +++ b/service/message_handler_utils.go @@ -0,0 +1,225 @@ +package service + +// 消息处理辅助函数 +import ( + "fmt" + "log" + "strings" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/woodchen-ink/Q58Bot/core" +) + +// deleteMessageAfterDelay 根据指定延迟删除消息。 +func deleteMessageAfterDelay(bot *tgbotapi.BotAPI, chatID int64, messageID int, delay time.Duration) { + // 让线程暂停指定的延迟时间。 + time.Sleep(delay) + + // 创建一个删除消息的请求。 + deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID) + + // 尝试发送删除消息的请求,并检查是否有错误发生。 + // 注意: 错误情况下只是记录错误,不进行其他操作。 + _, err := bot.Request(deleteMsg) + if err != nil { + log.Printf("删除消息失败: %v", err) + } +} + +// SendLongMessage 如有必要,可将长消息拆分为多条消息来发送 +func SendLongMessage(bot *tgbotapi.BotAPI, chatID int64, prefix string, items []string) error { + const maxMessageLength = 4000 // Leave some room for Telegram's message limit + + message := prefix + "\n" + for i, item := range items { + newLine := fmt.Sprintf("%d. %s\n", i+1, item) + if len(message)+len(newLine) > maxMessageLength { + msg := tgbotapi.NewMessage(chatID, message) + _, err := bot.Send(msg) + if err != nil { + return err + } + message = "" + } + message += newLine + } + + if message != "" { + msg := tgbotapi.NewMessage(chatID, message) + _, err := bot.Send(msg) + if err != nil { + return err + } + } + + return nil +} + +// HandleKeywordCommand 处理关键词命令 +func HandleKeywordCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string, db *core.Database) { + switch command { + case "list": + keywords, err := db.GetAllKeywords() + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "获取关键词列表时发生错误。")) + return + } + if len(keywords) == 0 { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "关键词列表为空。")) + } else { + SendLongMessage(bot, message.Chat.ID, "当前关键词列表:", keywords) + } + case "add": + if args != "" { + keyword := args + exists, err := db.KeywordExists(keyword) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "检查关键词时发生错误。")) + return + } + if !exists { + err = db.AddKeyword(keyword) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "添加关键词时发生错误。")) + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已添加。", keyword))) + } + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已存在。", keyword))) + } + } + case "delete": + if args != "" { + keyword := args + err := db.RemoveKeyword(keyword) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("删除关键词 '%s' 时发生错误: %v", keyword, err))) + return + } + + // 检查关键词是否仍然存在 + exists, err := db.KeywordExists(keyword) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("检查关键词 '%s' 是否存在时发生错误: %v", keyword, err))) + return + } + + if !exists { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已成功删除。", keyword))) + } else { + similarKeywords, err := db.SearchKeywords(keyword) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "搜索关键词时发生错误。")) + return + } + if len(similarKeywords) > 0 { + SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("未能删除关键词 '%s'。\n\n以下是相似的关键词:", keyword), similarKeywords) + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("未能删除关键词 '%s',且未找到相似的关键词。", keyword))) + } + } + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "请提供要删除的关键词。")) + } + case "deletecontaining": + if args != "" { + substring := args + removedKeywords, err := db.RemoveKeywordsContaining(substring) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "删除关键词时发生错误。")) + return + } + if len(removedKeywords) > 0 { + SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("已删除包含 '%s' 的以下关键词:", substring), removedKeywords) + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("没有找到包含 '%s' 的关键词。", substring))) + } + } + default: + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。")) + } +} + +func HandleWhitelistCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string, db *core.Database) { + switch command { + case "listwhite": + whitelist, err := db.GetAllWhitelist() + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("获取白名单时发生错误: %v", err))) + return + } + if len(whitelist) == 0 { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "白名单为空。")) + } else { + SendLongMessage(bot, message.Chat.ID, "白名单域名列表:", whitelist) + } + + case "addwhite": + if args == "" { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "请提供要添加的域名。")) + return + } + domain := strings.ToLower(args) + exists, err := db.WhitelistExists(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("检查白名单时发生错误: %v", err))) + return + } + if exists { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已在白名单中。", domain))) + return + } + err = db.AddWhitelist(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("添加到白名单时发生错误: %v", err))) + return + } + // 再次检查以确保添加成功 + exists, err = db.WhitelistExists(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("验证添加操作时发生错误: %v", err))) + return + } + if exists { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已成功添加到白名单。", domain))) + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("未能添加域名 '%s' 到白名单。", domain))) + } + + case "delwhite": + if args == "" { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "请提供要删除的域名。")) + return + } + domain := strings.ToLower(args) + exists, err := db.WhitelistExists(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("检查白名单时发生错误: %v", err))) + return + } + if !exists { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 不在白名单中。", domain))) + return + } + err = db.RemoveWhitelist(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("从白名单删除时发生错误: %v", err))) + return + } + // 再次检查以确保删除成功 + exists, err = db.WhitelistExists(domain) + if err != nil { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("验证删除操作时发生错误: %v", err))) + return + } + if !exists { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已成功从白名单中删除。", domain))) + } else { + bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("未能从白名单中删除域名 '%s'。", domain))) + } + + default: + bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。")) + } +} diff --git a/service/prompt_reply.go b/service/prompt_reply/prompt_reply.go similarity index 94% rename from service/prompt_reply.go rename to service/prompt_reply/prompt_reply.go index 18c83a7..3467602 100644 --- a/service/prompt_reply.go +++ b/service/prompt_reply/prompt_reply.go @@ -1,5 +1,6 @@ -package service +package prompt_reply +//提示词回复 import ( "fmt" "log" @@ -12,15 +13,6 @@ import ( var db *core.Database -func InitPromptService() error { - var err error - db, err = core.NewDatabase() - if err != nil { - return fmt.Errorf("failed to initialize database: %v", err) - } - return nil -} - func SetPromptReply(prompt, reply string) error { return db.AddPromptReply(prompt, reply) }