mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-26 03:44:55 +08:00
update: tg bot
This commit is contained in:
@@ -2,7 +2,7 @@ package dto
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// TelegramChannelRequest 创建/更新 Telegram 频道/群组请求
|
// TelegramChannelRequest 创建 Telegram 频道/群组请求
|
||||||
type TelegramChannelRequest struct {
|
type TelegramChannelRequest struct {
|
||||||
ChatID int64 `json:"chat_id" binding:"required"`
|
ChatID int64 `json:"chat_id" binding:"required"`
|
||||||
ChatName string `json:"chat_name" binding:"required"`
|
ChatName string `json:"chat_name" binding:"required"`
|
||||||
@@ -16,6 +16,20 @@ type TelegramChannelRequest struct {
|
|||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TelegramChannelUpdateRequest 更新 Telegram 频道/群组请求(ChatID可选)
|
||||||
|
type TelegramChannelUpdateRequest struct {
|
||||||
|
ChatID int64 `json:"chat_id"` // 可选,用于验证
|
||||||
|
ChatName string `json:"chat_name" binding:"required"`
|
||||||
|
ChatType string `json:"chat_type" binding:"required"` // channel 或 group
|
||||||
|
PushEnabled bool `json:"push_enabled"`
|
||||||
|
PushFrequency int `json:"push_frequency"`
|
||||||
|
PushStartTime string `json:"push_start_time"`
|
||||||
|
PushEndTime string `json:"push_end_time"`
|
||||||
|
ContentCategories string `json:"content_categories"`
|
||||||
|
ContentTags string `json:"content_tags"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
// TelegramChannelResponse Telegram 频道/群组响应
|
// TelegramChannelResponse Telegram 频道/群组响应
|
||||||
type TelegramChannelResponse struct {
|
type TelegramChannelResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type TelegramChannel struct {
|
|||||||
|
|
||||||
// 推送配置
|
// 推送配置
|
||||||
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
|
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
|
||||||
PushFrequency int `json:"push_frequency" gorm:"default:24;comment:推送频率(小时)"`
|
PushFrequency int `json:"push_frequency" gorm:"default:5;comment:推送频率(分钟)"`
|
||||||
PushStartTime string `json:"push_start_time" gorm:"size:10;comment:推送开始时间,格式HH:mm"`
|
PushStartTime string `json:"push_start_time" gorm:"size:10;comment:推送开始时间,格式HH:mm"`
|
||||||
PushEndTime string `json:"push_end_time" gorm:"size:10;comment:推送结束时间,格式HH:mm"`
|
PushEndTime string `json:"push_end_time" gorm:"size:10;comment:推送结束时间,格式HH:mm"`
|
||||||
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
|
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ func (r *TelegramChannelRepositoryImpl) FindDueForPush() ([]entity.TelegramChann
|
|||||||
if channel.LastPushAt == nil {
|
if channel.LastPushAt == nil {
|
||||||
dueChannels = append(dueChannels, channel)
|
dueChannels = append(dueChannels, channel)
|
||||||
} else {
|
} else {
|
||||||
// 计算下次推送时间:上次推送时间 + 推送频率小时
|
// 计算下次推送时间:上次推送时间 + 推送频率分钟
|
||||||
nextPushTime := channel.LastPushAt.Add(time.Duration(channel.PushFrequency) * time.Hour)
|
nextPushTime := channel.LastPushAt.Add(time.Duration(channel.PushFrequency) * time.Minute)
|
||||||
if now.After(nextPushTime) {
|
if now.After(nextPushTime) {
|
||||||
dueChannels = append(dueChannels, channel)
|
dueChannels = append(dueChannels, channel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.TelegramChannelRequest
|
var req dto.TelegramChannelUpdateRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -213,6 +213,12 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果前端传递了ChatID,验证它是否与现有频道匹配
|
||||||
|
if req.ChatID != 0 && req.ChatID != channel.ChatID {
|
||||||
|
ErrorResponse(c, "ChatID不匹配,无法更新此频道", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 更新频道信息
|
// 更新频道信息
|
||||||
channel.ChatName = req.ChatName
|
channel.ChatName = req.ChatName
|
||||||
channel.ChatType = req.ChatType
|
channel.ChatType = req.ChatType
|
||||||
@@ -276,7 +282,7 @@ func (h *TelegramHandler) RegisterChannelByCommand(chatID int64, chatName, chatT
|
|||||||
ChatName: chatName,
|
ChatName: chatName,
|
||||||
ChatType: chatType,
|
ChatType: chatType,
|
||||||
PushEnabled: true,
|
PushEnabled: true,
|
||||||
PushFrequency: 24, // 默认24小时
|
PushFrequency: 5, // 默认5分钟
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
RegisteredBy: "bot_command",
|
RegisteredBy: "bot_command",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -516,8 +518,8 @@ func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 /register 命令
|
// 处理 /register 命令(包括参数)
|
||||||
if strings.ToLower(text) == "/register" {
|
if strings.HasPrefix(strings.ToLower(text), "/register") {
|
||||||
utils.Info("[TELEGRAM:MESSAGE] 处理 /register 命令 from ChatID=%d", chatID)
|
utils.Info("[TELEGRAM:MESSAGE] 处理 /register 命令 from ChatID=%d", chatID)
|
||||||
s.handleRegisterCommand(message)
|
s.handleRegisterCommand(message)
|
||||||
return
|
return
|
||||||
@@ -537,10 +539,19 @@ func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认自动回复
|
// 默认自动回复(只对正常消息,不对转发消息)
|
||||||
if s.config.AutoReplyEnabled {
|
if s.config.AutoReplyEnabled {
|
||||||
utils.Info("[TELEGRAM:MESSAGE] 发送自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
|
// 检查是否是转发消息
|
||||||
s.sendReply(message, s.config.AutoReplyTemplate)
|
isForward := message.ForwardFrom != nil ||
|
||||||
|
message.ForwardFromChat != nil ||
|
||||||
|
message.ForwardDate != 0
|
||||||
|
|
||||||
|
if isForward {
|
||||||
|
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复,转发消息 from ChatID=%d", chatID)
|
||||||
|
} else {
|
||||||
|
utils.Info("[TELEGRAM:MESSAGE] 发送自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
|
||||||
|
s.sendReply(message, s.config.AutoReplyTemplate)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
|
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
|
||||||
}
|
}
|
||||||
@@ -549,43 +560,103 @@ func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
|
|||||||
// handleRegisterCommand 处理注册命令
|
// handleRegisterCommand 处理注册命令
|
||||||
func (s *TelegramBotServiceImpl) handleRegisterCommand(message *tgbotapi.Message) {
|
func (s *TelegramBotServiceImpl) handleRegisterCommand(message *tgbotapi.Message) {
|
||||||
chatID := message.Chat.ID
|
chatID := message.Chat.ID
|
||||||
chatTitle := message.Chat.Title
|
text := strings.TrimSpace(message.Text)
|
||||||
if chatTitle == "" {
|
|
||||||
// 如果没有标题,使用用户名作为名称
|
// 检查是否是群组
|
||||||
if message.Chat.UserName != "" {
|
isGroup := message.Chat.IsGroup() || message.Chat.IsSuperGroup()
|
||||||
chatTitle = message.Chat.UserName
|
|
||||||
} else {
|
if isGroup {
|
||||||
chatTitle = fmt.Sprintf("Chat_%d", chatID)
|
// 群组中需要管理员权限
|
||||||
|
if !s.isUserAdministrator(message.Chat.ID, message.From.ID) {
|
||||||
|
errorMsg := "❌ *权限不足*\n\n只有群组管理员才能注册此群组用于推送。\n\n请联系管理员执行注册命令。"
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
chatType := "private"
|
// 注册群组
|
||||||
if message.Chat.IsChannel() {
|
chatTitle := message.Chat.Title
|
||||||
chatType = "channel"
|
if chatTitle == "" {
|
||||||
} else if message.Chat.IsGroup() || message.Chat.IsSuperGroup() {
|
chatTitle = fmt.Sprintf("Group_%d", chatID)
|
||||||
chatType = "group"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err := s.RegisterChannel(chatID, chatTitle, chatType)
|
err := s.RegisterChannel(chatID, chatTitle, "group")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorMsg := fmt.Sprintf("注册失败: %v", err)
|
errorMsg := fmt.Sprintf("❌ 注册失败: %v", err)
|
||||||
s.sendReply(message, errorMsg)
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := fmt.Sprintf("✅ *群组注册成功!*\n\n群组: %s\n类型: 群组\n\n现在可以向此群组推送资源内容了。", chatTitle)
|
||||||
|
s.sendReply(message, successMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
successMsg := fmt.Sprintf("✅ 注册成功!\n\n频道/群组: %s\n类型: %s\n\n现在可以向此频道推送资源内容了。", chatTitle, chatType)
|
// 私聊处理
|
||||||
s.sendReply(message, successMsg)
|
parts := strings.Fields(text)
|
||||||
|
|
||||||
|
if len(parts) == 1 {
|
||||||
|
// 没有参数,注册私聊
|
||||||
|
chatTitle := message.Chat.UserName
|
||||||
|
if chatTitle == "" {
|
||||||
|
chatTitle = fmt.Sprintf("Private_%d", message.From.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.RegisterChannel(chatID, chatTitle, "private")
|
||||||
|
if err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ 注册失败: %v", err)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := fmt.Sprintf("✅ *私聊注册成功!*\n\n聊天: %s\n类型: 私聊\n\n现在可以向此私聊推送资源内容了。", chatTitle)
|
||||||
|
s.sendReply(message, successMsg)
|
||||||
|
} else if parts[1] == "help" || parts[1] == "-h" {
|
||||||
|
// 显示注册帮助
|
||||||
|
helpMsg := `🤖 *私聊注册帮助*
|
||||||
|
|
||||||
|
*注册私聊:*
|
||||||
|
/register - 注册当前私聊用于推送
|
||||||
|
|
||||||
|
*注册频道:*
|
||||||
|
支持两种格式:
|
||||||
|
• /register <频道ID> - 如: /register -1001234567890
|
||||||
|
• /register @用户名 - 如: /register @xypan
|
||||||
|
|
||||||
|
*获取频道ID的方法:*
|
||||||
|
1. 将机器人添加到频道并设为管理员
|
||||||
|
2. 向频道发送消息,查看机器人收到的消息
|
||||||
|
3. 频道ID通常是负数,如 -1001234567890
|
||||||
|
|
||||||
|
*获取频道用户名的其他方法:*
|
||||||
|
• 频道链接: https://t.me/用户名
|
||||||
|
• 频道设置中的用户名
|
||||||
|
• @用户名 格式
|
||||||
|
|
||||||
|
*示例:*
|
||||||
|
/register -1001234567890
|
||||||
|
/register @xypan
|
||||||
|
|
||||||
|
*注意:*
|
||||||
|
• 频道ID必须是纯数字(包括负号)
|
||||||
|
• 用户名格式必须以 @ 开头
|
||||||
|
• 机器人必须是频道的管理员才能注册`
|
||||||
|
s.sendReply(message, helpMsg)
|
||||||
|
} else {
|
||||||
|
// 有参数,尝试注册频道
|
||||||
|
channelIDStr := strings.TrimSpace(parts[1])
|
||||||
|
s.handleChannelRegistration(message, channelIDStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStartCommand 处理开始命令
|
// handleStartCommand 处理开始命令
|
||||||
func (s *TelegramBotServiceImpl) handleStartCommand(message *tgbotapi.Message) {
|
func (s *TelegramBotServiceImpl) handleStartCommand(message *tgbotapi.Message) {
|
||||||
welcomeMsg := `🤖 欢迎使用网盘资源机器人!
|
welcomeMsg := `🤖 欢迎使用老九网盘资源机器人!
|
||||||
|
|
||||||
我会帮您搜索网盘资源。使用方法:
|
• 发送 搜索 + 关键词 进行资源搜索
|
||||||
• 直接发送关键词搜索资源
|
• 发送 /register 注册当前频道或群组,用于主动推送资源
|
||||||
• 发送 /register 注册当前频道用于推送
|
• 私聊中使用 /register help 获取注册帮助
|
||||||
|
• 发送 /start 获取帮助信息
|
||||||
享受使用吧!`
|
`
|
||||||
|
|
||||||
if s.config.AutoReplyEnabled && s.config.AutoReplyTemplate != "" {
|
if s.config.AutoReplyEnabled && s.config.AutoReplyTemplate != "" {
|
||||||
welcomeMsg += "\n\n" + s.config.AutoReplyTemplate
|
welcomeMsg += "\n\n" + s.config.AutoReplyTemplate
|
||||||
@@ -855,8 +926,8 @@ func (s *TelegramBotServiceImpl) sendReplyWithResourceAutoDelete(message *tgbota
|
|||||||
|
|
||||||
// startContentPusher 启动内容推送器
|
// startContentPusher 启动内容推送器
|
||||||
func (s *TelegramBotServiceImpl) startContentPusher() {
|
func (s *TelegramBotServiceImpl) startContentPusher() {
|
||||||
// 每小时检查一次需要推送的频道
|
// 每分钟检查一次需要推送的频道
|
||||||
s.cronScheduler.AddFunc("@every 1h", func() {
|
s.cronScheduler.AddFunc("@every 1m", func() {
|
||||||
s.pushContentToChannels()
|
s.pushContentToChannels()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -935,7 +1006,7 @@ func (s *TelegramBotServiceImpl) buildPushMessage(channel entity.TelegramChannel
|
|||||||
message += "*详细资源列表请查看网站*"
|
message += "*详细资源列表请查看网站*"
|
||||||
}
|
}
|
||||||
|
|
||||||
message += fmt.Sprintf("\n\n⏰ 下次推送: %d 小时后", channel.PushFrequency)
|
message += fmt.Sprintf("\n\n⏰ 下次推送: %d 分钟后", channel.PushFrequency)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
@@ -996,7 +1067,7 @@ func (s *TelegramBotServiceImpl) RegisterChannel(chatID int64, chatName, chatTyp
|
|||||||
ChatName: chatName,
|
ChatName: chatName,
|
||||||
ChatType: chatType,
|
ChatType: chatType,
|
||||||
PushEnabled: true,
|
PushEnabled: true,
|
||||||
PushFrequency: 24, // 默认24小时
|
PushFrequency: 5, // 默认5分钟
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
RegisteredBy: "bot_command",
|
RegisteredBy: "bot_command",
|
||||||
RegisteredAt: time.Now(),
|
RegisteredAt: time.Now(),
|
||||||
@@ -1013,6 +1084,210 @@ func (s *TelegramBotServiceImpl) IsChannelRegistered(chatID int64) bool {
|
|||||||
return err == nil && channel != nil
|
return err == nil && channel != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUserAdministrator 检查用户是否为群组管理员
|
||||||
|
func (s *TelegramBotServiceImpl) isUserAdministrator(chatID int64, userID int64) bool {
|
||||||
|
if s.bot == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户在群组中的信息
|
||||||
|
memberConfig := tgbotapi.GetChatMemberConfig{
|
||||||
|
ChatConfigWithUser: tgbotapi.ChatConfigWithUser{
|
||||||
|
ChatID: chatID,
|
||||||
|
UserID: userID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
member, err := s.bot.GetChatMember(memberConfig)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("[TELEGRAM:ADMIN] 获取用户群组成员信息失败: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否为管理员或创建者
|
||||||
|
userStatus := string(member.Status)
|
||||||
|
return userStatus == "administrator" || userStatus == "creator"
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleChannelRegistration 处理频道注册(支持频道ID和用户名)
|
||||||
|
func (s *TelegramBotServiceImpl) handleChannelRegistration(message *tgbotapi.Message, channelParam string) {
|
||||||
|
channelParam = strings.TrimSpace(channelParam)
|
||||||
|
|
||||||
|
var chat tgbotapi.Chat
|
||||||
|
var err error
|
||||||
|
var identifier string
|
||||||
|
|
||||||
|
// 判断是频道ID还是用户名格式
|
||||||
|
if strings.HasPrefix(channelParam, "@") {
|
||||||
|
// 用户名格式:@username
|
||||||
|
username := strings.TrimPrefix(channelParam, "@")
|
||||||
|
if username == "" {
|
||||||
|
errorMsg := "❌ *用户名格式错误*\n\n用户名不能为空,如 @mychannel"
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试通过用户名获取频道信息
|
||||||
|
// 手动构造请求URL并发送
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getChat", s.config.ApiKey)
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("chat_id", "@"+username)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
// 如果有代理,配置代理
|
||||||
|
if s.config.ProxyEnabled && s.config.ProxyHost != "" {
|
||||||
|
var proxyClient *http.Client
|
||||||
|
if s.config.ProxyType == "socks5" {
|
||||||
|
// SOCKS5代理配置
|
||||||
|
auth := &proxy.Auth{}
|
||||||
|
if s.config.ProxyUsername != "" {
|
||||||
|
auth.User = s.config.ProxyUsername
|
||||||
|
auth.Password = s.config.ProxyPassword
|
||||||
|
}
|
||||||
|
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort), auth, proxy.Direct)
|
||||||
|
if proxyErr != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ *代理配置错误*\n\n无法连接到代理服务器: %v", proxyErr)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Dial: dialer.Dial,
|
||||||
|
},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// HTTP/HTTPS代理配置
|
||||||
|
proxyURL := &url.URL{
|
||||||
|
Scheme: s.config.ProxyType,
|
||||||
|
Host: fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort),
|
||||||
|
}
|
||||||
|
if s.config.ProxyUsername != "" {
|
||||||
|
proxyURL.User = url.UserPassword(s.config.ProxyUsername, s.config.ProxyPassword)
|
||||||
|
}
|
||||||
|
proxyClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
|
},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client = proxyClient
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, httpErr := client.PostForm(apiURL, data)
|
||||||
|
if httpErr != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ *无法访问频道*\n\n请确保:\n• 机器人已被添加到频道 @%s\n• 机器人已被设为频道管理员\n• 用户名正确\n\n错误详情: %v", username, httpErr)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var apiResponse struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Result struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"result"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
|
||||||
|
errorMsg := "❌ *解析服务器响应失败*\n\n请稍后重试"
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apiResponse.OK {
|
||||||
|
errorMsg := fmt.Sprintf("❌ *获取频道信息失败*\n\n错误: %s", apiResponse.Description)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是频道
|
||||||
|
if apiResponse.Result.Type != "channel" {
|
||||||
|
errorMsg := "❌ *这不是一个频道*\n\n请提供有效的频道用户名。"
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造Chat对象
|
||||||
|
chat = tgbotapi.Chat{
|
||||||
|
ID: apiResponse.Result.ID,
|
||||||
|
Title: apiResponse.Result.Title,
|
||||||
|
UserName: apiResponse.Result.Username,
|
||||||
|
Type: apiResponse.Result.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier = fmt.Sprintf("@%s", username)
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(channelParam, "-") && len(channelParam) > 10 {
|
||||||
|
// 频道ID格式:-1001234567890
|
||||||
|
channelID, parseErr := strconv.ParseInt(channelParam, 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ *频道ID格式错误*\n\n频道ID必须是数字,如 -1001234567890\n\n您输入的: %s", channelParam)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过频道ID获取频道信息
|
||||||
|
chat, err = s.bot.GetChat(tgbotapi.ChatInfoConfig{
|
||||||
|
ChatConfig: tgbotapi.ChatConfig{
|
||||||
|
ChatID: channelID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ *无法访问频道*\n\n请确保:\n• 机器人已被添加到频道\n• 机器人已被设为频道管理员\n• 频道ID正确\n\n错误详情: %v", err)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经是频道
|
||||||
|
if !chat.IsChannel() {
|
||||||
|
errorMsg := "❌ *这不是一个频道*\n\n请提供有效的频道ID。"
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier = fmt.Sprintf("ID: %d", chat.ID)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 无效格式
|
||||||
|
errorMsg := fmt.Sprintf("❌ *格式错误*\n\n支持的格式:\n• 频道ID: -1001234567890\n• 用户名: @mychannel\n\n您输入的: %s", channelParam)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册频道
|
||||||
|
channel := entity.TelegramChannel{
|
||||||
|
ChatID: chat.ID,
|
||||||
|
ChatName: chat.Title,
|
||||||
|
ChatType: "channel",
|
||||||
|
PushEnabled: true,
|
||||||
|
PushFrequency: 60, // 默认1小时
|
||||||
|
IsActive: true,
|
||||||
|
RegisteredBy: message.From.UserName,
|
||||||
|
RegisteredAt: time.Now(),
|
||||||
|
ContentCategories: "",
|
||||||
|
ContentTags: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.channelRepo.Create(&channel)
|
||||||
|
if err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("❌ 频道注册失败: %v", err)
|
||||||
|
s.sendReply(message, errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := fmt.Sprintf("✅ *频道注册成功!*\n\n频道: %s\n%s\n类型: 频道\n\n现在可以向此频道推送资源内容了。\n\n可以通过管理界面调整推送设置。", chat.Title, identifier)
|
||||||
|
s.sendReply(message, successMsg)
|
||||||
|
}
|
||||||
|
|
||||||
// HandleWebhookUpdate 处理 Webhook 更新(预留接口,目前使用长轮询)
|
// HandleWebhookUpdate 处理 Webhook 更新(预留接口,目前使用长轮询)
|
||||||
func (s *TelegramBotServiceImpl) HandleWebhookUpdate(c interface{}) {
|
func (s *TelegramBotServiceImpl) HandleWebhookUpdate(c interface{}) {
|
||||||
// 目前使用长轮询模式,webhook 接口预留
|
// 目前使用长轮询模式,webhook 接口预留
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ func GetTelegramLogs(startTime *time.Time, endTime *time.Time, limit int) ([]Tel
|
|||||||
var allEntries []TelegramLogEntry
|
var allEntries []TelegramLogEntry
|
||||||
|
|
||||||
// 编译Telegram相关的正则表达式
|
// 编译Telegram相关的正则表达式
|
||||||
telegramRegex := regexp.MustCompile(`(?i)(telegram|bot|推送|消息|频道|群组|\[.*telegram.*\])`)
|
telegramRegex := regexp.MustCompile(`(?i)(\[TELEGRAM.*?\])`)
|
||||||
messageRegex := regexp.MustCompile(`\[(\w+)\]\s+(\d{4}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2})\s+(.*)`)
|
messageRegex := regexp.MustCompile(`\[(\w+)\]\s+(\d{4}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[.*?\]\s+(.*)`)
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
entries, err := parseTelegramLogsFromFile(file, telegramRegex, messageRegex, startTime, endTime)
|
entries, err := parseTelegramLogsFromFile(file, telegramRegex, messageRegex, startTime, endTime)
|
||||||
@@ -119,10 +119,10 @@ func parseTelegramLogsFromFile(filePath string, telegramRegex, messageRegex *reg
|
|||||||
|
|
||||||
// parseLogLine 解析单行日志
|
// parseLogLine 解析单行日志
|
||||||
func parseLogLine(line string, messageRegex *regexp.Regexp) (TelegramLogEntry, error) {
|
func parseLogLine(line string, messageRegex *regexp.Regexp) (TelegramLogEntry, error) {
|
||||||
// 匹配日志格式: [LEVEL] 2006/01/02 15:04:05 message
|
// 匹配日志格式: [LEVEL] 2006/01/02 15:04:05 [file:line] message
|
||||||
matches := messageRegex.FindStringSubmatch(line)
|
matches := messageRegex.FindStringSubmatch(line)
|
||||||
if len(matches) < 4 {
|
if len(matches) < 4 {
|
||||||
return TelegramLogEntry{}, fmt.Errorf("无法解析日志行")
|
return TelegramLogEntry{}, fmt.Errorf("无法解析日志行: %s", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
level := matches[1]
|
level := matches[1]
|
||||||
|
|||||||
@@ -311,7 +311,7 @@
|
|||||||
<div v-if="channel.push_enabled" class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
<div v-if="channel.push_enabled" class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">推送频率</label>
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">推送频率</label>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ channel.push_frequency }} 小时</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ channel.push_frequency }} 分钟</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">内容分类</label>
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">内容分类</label>
|
||||||
@@ -484,18 +484,19 @@
|
|||||||
<n-select
|
<n-select
|
||||||
v-model:value="editingChannel.push_frequency"
|
v-model:value="editingChannel.push_frequency"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '每5分钟', value: 0.0833 },
|
{ label: '每5分钟', value: 5 },
|
||||||
{ label: '每10分钟', value: 0.1667 },
|
{ label: '每10分钟', value: 10 },
|
||||||
{ label: '每15分钟', value: 0.25 },
|
{ label: '每15分钟', value: 15 },
|
||||||
{ label: '每30分钟', value: 0.5 },
|
{ label: '每30分钟', value: 30 },
|
||||||
{ label: '每小时', value: 1 },
|
{ label: '每45分钟', value: 45 },
|
||||||
{ label: '每2小时', value: 2 },
|
{ label: '每小时', value: 60 },
|
||||||
{ label: '每3小时', value: 3 },
|
{ label: '每2小时', value: 120 },
|
||||||
{ label: '每6小时', value: 6 },
|
{ label: '每3小时', value: 180 },
|
||||||
{ label: '每12小时', value: 12 },
|
{ label: '每6小时', value: 360 },
|
||||||
{ label: '每天', value: 24 },
|
{ label: '每12小时', value: 720 },
|
||||||
{ label: '每2天', value: 48 },
|
{ label: '每天', value: 1440 },
|
||||||
{ label: '每周', value: 168 }
|
{ label: '每2天', value: 2880 },
|
||||||
|
{ label: '每周', value: 10080 }
|
||||||
]"
|
]"
|
||||||
placeholder="选择推送频率"
|
placeholder="选择推送频率"
|
||||||
/>
|
/>
|
||||||
@@ -512,6 +513,8 @@
|
|||||||
format="HH:mm"
|
format="HH:mm"
|
||||||
placeholder="选择开始时间"
|
placeholder="选择开始时间"
|
||||||
clearable
|
clearable
|
||||||
|
:value-format="'HH:mm'"
|
||||||
|
:actions="['clear', 'confirm']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -521,6 +524,8 @@
|
|||||||
format="HH:mm"
|
format="HH:mm"
|
||||||
placeholder="选择结束时间"
|
placeholder="选择结束时间"
|
||||||
clearable
|
clearable
|
||||||
|
:value-format="'HH:mm'"
|
||||||
|
:actions="['clear', 'confirm']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -901,7 +906,41 @@ const saveBotConfig = async () => {
|
|||||||
|
|
||||||
// 编辑频道
|
// 编辑频道
|
||||||
const editChannel = (channel: any) => {
|
const editChannel = (channel: any) => {
|
||||||
editingChannel.value = { ...channel }
|
// 复制频道数据并处理时间字段
|
||||||
|
const channelCopy = { ...channel }
|
||||||
|
|
||||||
|
// 处理时间字段,确保时间选择器可以正确显示
|
||||||
|
try {
|
||||||
|
// 处理开始时间
|
||||||
|
if (channelCopy.push_start_time) {
|
||||||
|
if (isValidTimeString(channelCopy.push_start_time)) {
|
||||||
|
// 确保格式正确,转换为标准格式
|
||||||
|
channelCopy.push_start_time = normalizeTimeString(channelCopy.push_start_time)
|
||||||
|
} else {
|
||||||
|
channelCopy.push_start_time = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channelCopy.push_start_time = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理结束时间
|
||||||
|
if (channelCopy.push_end_time) {
|
||||||
|
if (isValidTimeString(channelCopy.push_end_time)) {
|
||||||
|
// 确保格式正确,转换为标准格式
|
||||||
|
channelCopy.push_end_time = normalizeTimeString(channelCopy.push_end_time)
|
||||||
|
} else {
|
||||||
|
channelCopy.push_end_time = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channelCopy.push_end_time = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('处理频道时间字段时出错:', error)
|
||||||
|
channelCopy.push_start_time = null
|
||||||
|
channelCopy.push_end_time = null
|
||||||
|
}
|
||||||
|
|
||||||
|
editingChannel.value = channelCopy
|
||||||
showEditChannelDialog.value = true
|
showEditChannelDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1147,13 +1186,47 @@ const getCategoryLabel = (category: string): string => {
|
|||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
// 检查时间字符串是否有效
|
||||||
|
const isValidTimeString = (timeStr: string): boolean => {
|
||||||
|
if (!timeStr || typeof timeStr !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 HH:mm 格式
|
||||||
|
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/
|
||||||
|
if (!timeRegex.test(timeStr)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规范化时间字符串
|
||||||
|
const normalizeTimeString = (timeStr: string): string => {
|
||||||
|
if (!timeStr || typeof timeStr !== 'string') {
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 HH:mm 格式,补齐前导零
|
||||||
|
const parts = timeStr.split(':')
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const hours = parts[0].padStart(2, '0')
|
||||||
|
const minutes = parts[1].padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
|
||||||
// 保存频道设置
|
// 保存频道设置
|
||||||
const saveChannelSettings = async () => {
|
const saveChannelSettings = async () => {
|
||||||
if (!editingChannel.value) return
|
if (!editingChannel.value) return
|
||||||
|
|
||||||
savingChannel.value = true
|
savingChannel.value = true
|
||||||
try {
|
try {
|
||||||
|
// 处理时间字段,确保保存为字符串格式
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
chat_id: editingChannel.value.chat_id,
|
||||||
chat_name: editingChannel.value.chat_name,
|
chat_name: editingChannel.value.chat_name,
|
||||||
chat_type: editingChannel.value.chat_type,
|
chat_type: editingChannel.value.chat_type,
|
||||||
push_enabled: editingChannel.value.push_enabled,
|
push_enabled: editingChannel.value.push_enabled,
|
||||||
@@ -1161,8 +1234,8 @@ const saveChannelSettings = async () => {
|
|||||||
content_categories: editingChannel.value.content_categories,
|
content_categories: editingChannel.value.content_categories,
|
||||||
content_tags: editingChannel.value.content_tags,
|
content_tags: editingChannel.value.content_tags,
|
||||||
is_active: editingChannel.value.is_active,
|
is_active: editingChannel.value.is_active,
|
||||||
push_start_time: editingChannel.value.push_start_time,
|
push_start_time: formatTimeForSave(editingChannel.value.push_start_time),
|
||||||
push_end_time: editingChannel.value.push_end_time
|
push_end_time: formatTimeForSave(editingChannel.value.push_end_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
await telegramApi.updateChannel(editingChannel.value.id, updateData)
|
await telegramApi.updateChannel(editingChannel.value.id, updateData)
|
||||||
@@ -1188,6 +1261,27 @@ const saveChannelSettings = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化时间字段以便保存
|
||||||
|
const formatTimeForSave = (timeValue: any): string | null => {
|
||||||
|
if (!timeValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经是字符串格式,直接返回
|
||||||
|
if (typeof timeValue === 'string') {
|
||||||
|
return timeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 Date 对象,格式化为 HH:mm
|
||||||
|
if (timeValue instanceof Date) {
|
||||||
|
const hours = timeValue.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = timeValue.getMinutes().toString().padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// 启动机器人服务
|
// 启动机器人服务
|
||||||
const startBotService = async () => {
|
const startBotService = async () => {
|
||||||
startingBot.value = true
|
startingBot.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user