mirror of
https://github.com/woodchen-ink/Q58Bot.git
synced 2025-07-18 05:42:06 +08:00
整体构架重构
This commit is contained in:
parent
a12a9b789b
commit
4291640190
49
README.md
49
README.md
@ -7,18 +7,8 @@
|
||||

|
||||
|
||||
|
||||
## 项目简介
|
||||
## 项目功能
|
||||
|
||||
这个项目主要功能:
|
||||
|
||||
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 可能有请求限制,请注意控制请求频率
|
||||
- 定期检查日志以确保服务正常运行
|
||||
|
||||
## 贡献
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package core
|
||||
|
||||
//数据库处理
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
|
@ -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
|
||||
}
|
84
core/init.go
Normal file
84
core/init.go
Normal file
@ -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
|
||||
}
|
41
core/ratelimiter.go
Normal file
41
core/ratelimiter.go
Normal file
@ -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
|
||||
}
|
15
main.go
15
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 {}
|
||||
}
|
||||
|
@ -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
|
70
service/group_member_management/group_member_management.go
Normal file
70
service/group_member_management/group_member_management.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
185
service/guard.go
185
service/guard.go
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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, "无效的命令或参数。"))
|
||||
}
|
||||
}
|
240
service/link_filter/link_filter.go
Normal file
240
service/link_filter/link_filter.go
Normal file
@ -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
|
||||
}
|
175
service/message_handler.go
Normal file
175
service/message_handler.go
Normal file
@ -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...")
|
||||
}
|
||||
}
|
||||
}
|
225
service/message_handler_utils.go
Normal file
225
service/message_handler_utils.go
Normal file
@ -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, "无效的命令或参数。"))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user