Files
urldb/services/telegram_bot_service.go
2025-10-10 19:17:03 +08:00

1696 lines
53 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
"golang.org/x/net/proxy"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/robfig/cron/v3"
)
// https://core.telegram.org/bots/api
type TelegramBotService interface {
Start() error
Stop() error
IsRunning() bool
ReloadConfig() error
GetRuntimeStatus() map[string]interface{}
ValidateApiKey(apiKey string) (bool, map[string]interface{}, error)
ValidateApiKeyWithProxy(apiKey string, proxyEnabled bool, proxyType, proxyHost string, proxyPort int, proxyUsername, proxyPassword string) (bool, map[string]interface{}, error)
GetBotUsername() string
SendMessage(chatID int64, text string, img string) error
DeleteMessage(chatID int64, messageID int) error
RegisterChannel(chatID int64, chatName, chatType string) error
IsChannelRegistered(chatID int64) bool
HandleWebhookUpdate(c interface{})
CleanupDuplicateChannels() error
}
type TelegramBotServiceImpl struct {
bot *tgbotapi.BotAPI
isRunning bool
systemConfigRepo repo.SystemConfigRepository
channelRepo repo.TelegramChannelRepository
resourceRepo repo.ResourceRepository // 添加资源仓库用于搜索
readyRepo repo.ReadyResourceRepository
cronScheduler *cron.Cron
config *TelegramBotConfig
}
type TelegramBotConfig struct {
Enabled bool
ApiKey string
AutoReplyEnabled bool
AutoReplyTemplate string
AutoDeleteEnabled bool
AutoDeleteInterval int // 分钟
ProxyEnabled bool
ProxyType string // http, https, socks5
ProxyHost string
ProxyPort int
ProxyUsername string
ProxyPassword string
}
func NewTelegramBotService(
systemConfigRepo repo.SystemConfigRepository,
channelRepo repo.TelegramChannelRepository,
resourceRepo repo.ResourceRepository,
readyResourceRepo repo.ReadyResourceRepository,
) TelegramBotService {
return &TelegramBotServiceImpl{
isRunning: false,
systemConfigRepo: systemConfigRepo,
channelRepo: channelRepo,
resourceRepo: resourceRepo,
readyRepo: readyResourceRepo,
cronScheduler: cron.New(),
config: &TelegramBotConfig{},
}
}
// loadConfig 加载配置
func (s *TelegramBotServiceImpl) loadConfig() error {
configs, err := s.systemConfigRepo.GetOrCreateDefault()
if err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
utils.Info("[TELEGRAM] 从数据库加载到 %d 个配置项", len(configs))
// 初始化默认值
s.config.Enabled = false
s.config.ApiKey = ""
s.config.AutoReplyEnabled = false // 默认禁用自动回复
s.config.AutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
s.config.AutoDeleteEnabled = false
s.config.AutoDeleteInterval = 60
// 初始化代理默认值
s.config.ProxyEnabled = false
s.config.ProxyType = "http"
s.config.ProxyHost = ""
s.config.ProxyPort = 8080
s.config.ProxyUsername = ""
s.config.ProxyPassword = ""
for _, config := range configs {
switch config.Key {
case entity.ConfigKeyTelegramBotEnabled:
s.config.Enabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (Enabled: %v)", config.Key, config.Value, s.config.Enabled)
case entity.ConfigKeyTelegramBotApiKey:
s.config.ApiKey = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyTelegramAutoReplyEnabled:
s.config.AutoReplyEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoReplyEnabled: %v)", config.Key, config.Value, s.config.AutoReplyEnabled)
case entity.ConfigKeyTelegramAutoReplyTemplate:
if config.Value != "" {
s.config.AutoReplyTemplate = config.Value
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, config.Value)
case entity.ConfigKeyTelegramAutoDeleteEnabled:
s.config.AutoDeleteEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoDeleteEnabled: %v)", config.Key, config.Value, s.config.AutoDeleteEnabled)
case entity.ConfigKeyTelegramAutoDeleteInterval:
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.AutoDeleteInterval)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoDeleteInterval: %d)", config.Key, config.Value, s.config.AutoDeleteInterval)
case entity.ConfigKeyTelegramProxyEnabled:
s.config.ProxyEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyEnabled: %v)", config.Key, config.Value, s.config.ProxyEnabled)
case entity.ConfigKeyTelegramProxyType:
s.config.ProxyType = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyType: %s)", config.Key, config.Value, s.config.ProxyType)
case entity.ConfigKeyTelegramProxyHost:
s.config.ProxyHost = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case entity.ConfigKeyTelegramProxyPort:
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.ProxyPort)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyPort: %d)", config.Key, config.Value, s.config.ProxyPort)
case entity.ConfigKeyTelegramProxyUsername:
s.config.ProxyUsername = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case entity.ConfigKeyTelegramProxyPassword:
s.config.ProxyPassword = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
default:
utils.Debug("未知配置: %s = %s", config.Key, config.Value)
}
}
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 配置加载完成: Enabled=%v, AutoReplyEnabled=%v, ApiKey长度=%d",
s.config.Enabled, s.config.AutoReplyEnabled, len(s.config.ApiKey))
return nil
}
// Start 启动机器人服务
func (s *TelegramBotServiceImpl) Start() error {
if s.isRunning {
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 服务已经在运行中")
return nil
}
// 加载配置
if err := s.loadConfig(); err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
if !s.config.Enabled || s.config.ApiKey == "" {
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 未启用或 API Key 未配置")
return nil
}
// 创建 Bot 实例
var bot *tgbotapi.BotAPI
if s.config.ProxyEnabled && s.config.ProxyHost != "" {
// 配置代理
utils.Info("[TELEGRAM:PROXY] 配置代理: %s://%s:%d", s.config.ProxyType, s.config.ProxyHost, s.config.ProxyPort)
var httpClient *http.Client
if s.config.ProxyType == "socks5" {
// SOCKS5 代理配置
var auth *proxy.Auth
if s.config.ProxyUsername != "" {
auth = &proxy.Auth{
User: s.config.ProxyUsername,
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 {
return fmt.Errorf("创建 SOCKS5 代理失败: %v", proxyErr)
}
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
Timeout: 30 * time.Second,
}
} else {
// HTTP/HTTPS 代理配置
proxyURL := &url.URL{
Scheme: s.config.ProxyType,
Host: fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort),
User: nil,
}
if s.config.ProxyUsername != "" {
proxyURL.User = url.UserPassword(s.config.ProxyUsername, s.config.ProxyPassword)
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 30 * time.Second,
}
}
botInstance, botErr := tgbotapi.NewBotAPIWithClient(s.config.ApiKey, tgbotapi.APIEndpoint, httpClient)
if botErr != nil {
return fmt.Errorf("创建 Telegram Bot (代理模式) 失败: %v", botErr)
}
bot = botInstance
utils.Info("[TELEGRAM:PROXY] Telegram Bot 已配置代理连接")
} else {
// 直接连接(无代理)
var err error
bot, err = tgbotapi.NewBotAPI(s.config.ApiKey)
if err != nil {
return fmt.Errorf("创建 Telegram Bot 失败: %v", err)
}
utils.Info("[TELEGRAM:PROXY] Telegram Bot 使用直连模式")
}
s.bot = bot
s.isRunning = true
utils.Info("[TELEGRAM:SERVICE] Telegram Bot (@%s) 已启动", s.GetBotUsername())
// 启动推送调度器
s.startContentPusher()
// 设置 webhook在实际部署时配置
if err := s.setupWebhook(); err != nil {
utils.Error("[TELEGRAM:SERVICE] 设置 Webhook 失败: %v", err)
}
// 启动消息处理循环(长轮询模式)
go s.messageLoop()
return nil
}
// Stop 停止机器人服务
func (s *TelegramBotServiceImpl) Stop() error {
if !s.isRunning {
return nil
}
s.isRunning = false
if s.cronScheduler != nil {
s.cronScheduler.Stop()
}
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 服务已停止")
return nil
}
// IsRunning 检查机器人服务是否正在运行
func (s *TelegramBotServiceImpl) IsRunning() bool {
return s.isRunning && s.bot != nil
}
// ReloadConfig 重新加载机器人配置
func (s *TelegramBotServiceImpl) ReloadConfig() error {
utils.Info("[TELEGRAM:SERVICE] 开始重新加载配置...")
// 重新加载配置
if err := s.loadConfig(); err != nil {
utils.Error("[TELEGRAM:SERVICE] 重新加载配置失败: %v", err)
return fmt.Errorf("重新加载配置失败: %v", err)
}
utils.Info("[TELEGRAM:SERVICE] 配置重新加载完成: Enabled=%v, AutoReplyEnabled=%v",
s.config.Enabled, s.config.AutoReplyEnabled)
return nil
}
// GetRuntimeStatus 获取机器人运行时状态
func (s *TelegramBotServiceImpl) GetRuntimeStatus() map[string]interface{} {
status := map[string]interface{}{
"is_running": s.IsRunning(),
"bot_initialized": s.bot != nil,
"config_loaded": s.config != nil,
"cron_running": s.cronScheduler != nil,
"username": "",
"uptime": 0,
}
if s.bot != nil {
status["username"] = s.GetBotUsername()
}
return status
}
// ValidateApiKey 验证 API Key
func (s *TelegramBotServiceImpl) ValidateApiKey(apiKey string) (bool, map[string]interface{}, error) {
if apiKey == "" {
return false, nil, fmt.Errorf("API Key 不能为空")
}
var bot *tgbotapi.BotAPI
var err error
// 如果启用了代理,使用代理验证
if s.config.ProxyEnabled && s.config.ProxyHost != "" {
var httpClient *http.Client
if s.config.ProxyType == "socks5" {
var auth *proxy.Auth
if s.config.ProxyUsername != "" {
auth = &proxy.Auth{
User: s.config.ProxyUsername,
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 {
// 如果代理失败,回退到直连
utils.Warn("[TELEGRAM:PROXY] SOCKS5 代理验证失败,回退到直连: %v", proxyErr)
bot, err = tgbotapi.NewBotAPI(apiKey)
} else {
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
Timeout: 10 * time.Second,
}
bot, err = tgbotapi.NewBotAPIWithClient(apiKey, tgbotapi.APIEndpoint, httpClient)
}
} else {
proxyURL := &url.URL{
Scheme: s.config.ProxyType,
Host: fmt.Sprintf("%s:%d", s.config.ProxyHost, s.config.ProxyPort),
User: nil,
}
if s.config.ProxyUsername != "" {
proxyURL.User = url.UserPassword(s.config.ProxyUsername, s.config.ProxyPassword)
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
bot, err = tgbotapi.NewBotAPIWithClient(apiKey, tgbotapi.APIEndpoint, httpClient)
}
} else {
// 直连验证
bot, err = tgbotapi.NewBotAPI(apiKey)
}
if err != nil {
return false, nil, fmt.Errorf("无效的 API Key: %v", err)
}
// 获取机器人信息
botInfo, err := bot.GetMe()
if err != nil {
return false, nil, fmt.Errorf("获取机器人信息失败: %v", err)
}
botData := map[string]interface{}{
"id": botInfo.ID,
"username": strings.TrimPrefix(botInfo.UserName, "@"),
"first_name": botInfo.FirstName,
"last_name": botInfo.LastName,
}
return true, botData, nil
}
// ValidateApiKeyWithProxy 使用代理配置验证 API Key
func (s *TelegramBotServiceImpl) ValidateApiKeyWithProxy(apiKey string, proxyEnabled bool, proxyType, proxyHost string, proxyPort int, proxyUsername, proxyPassword string) (bool, map[string]interface{}, error) {
if apiKey == "" {
return false, nil, fmt.Errorf("API Key 不能为空")
}
var bot *tgbotapi.BotAPI
var err error
// 使用提供的代理配置进行校验
if proxyEnabled && proxyHost != "" {
var httpClient *http.Client
if proxyType == "socks5" {
var auth *proxy.Auth
if proxyUsername != "" {
auth = &proxy.Auth{
User: proxyUsername,
Password: proxyPassword,
}
}
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", proxyHost, proxyPort), auth, proxy.Direct)
if proxyErr != nil {
return false, nil, fmt.Errorf("创建 SOCKS5 代理失败: %v", proxyErr)
}
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
Timeout: 10 * time.Second,
}
} else {
proxyURL := &url.URL{
Scheme: proxyType,
Host: fmt.Sprintf("%s:%d", proxyHost, proxyPort),
User: nil,
}
if proxyUsername != "" {
proxyURL.User = url.UserPassword(proxyUsername, proxyPassword)
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
}
bot, err = tgbotapi.NewBotAPIWithClient(apiKey, tgbotapi.APIEndpoint, httpClient)
if err != nil {
utils.Error(fmt.Sprintf("[TELEGRAM:VALIDATE] 创建 Telegram Bot (代理校验) 失败: %v", err))
return false, nil, fmt.Errorf("创建 Telegram Bot (代理校验) 失败: %v", err)
}
utils.Info("[TELEGRAM:VALIDATE] 使用代理配置校验 API Key")
} else {
// 直连校验
bot, err = tgbotapi.NewBotAPI(apiKey)
if err != nil {
utils.Error(fmt.Sprintf("[TELEGRAM:VALIDATE] 创建 Telegram Bot 失败: %v", err))
return false, nil, fmt.Errorf("无效的 API Key: %v", err)
}
utils.Info("[TELEGRAM:VALIDATE] 使用直连模式校验 API Key")
}
// 获取机器人信息
botInfo, err := bot.GetMe()
if err != nil {
return false, nil, fmt.Errorf("获取机器人信息失败: %v", err)
}
botData := map[string]interface{}{
"id": botInfo.ID,
"username": strings.TrimPrefix(botInfo.UserName, "@"),
"first_name": botInfo.FirstName,
"last_name": botInfo.LastName,
}
return true, botData, nil
}
// setupWebhook 设置 Webhook可选
func (s *TelegramBotServiceImpl) setupWebhook() error {
// 在生产环境中,这里会设置 webhook URL
// 暂时使用长轮询模式,不设置 webhook
utils.Info("[TELEGRAM:SERVICE] 使用长轮询模式处理消息")
return nil
}
// messageLoop 消息处理循环(长轮询模式)
func (s *TelegramBotServiceImpl) messageLoop() {
utils.Info("[TELEGRAM:MESSAGE] 开始监听 Telegram 消息更新...")
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := s.bot.GetUpdatesChan(u)
utils.Info("[TELEGRAM:MESSAGE] 消息监听循环已启动,等待消息...")
for update := range updates {
if update.Message != nil {
utils.Info("[TELEGRAM:MESSAGE] 接收到新消息更新")
s.handleMessage(update.Message)
} else {
utils.Debug("[TELEGRAM:MESSAGE] 接收到其他类型更新: %v", update)
}
}
utils.Info("[TELEGRAM:MESSAGE] 消息监听循环已结束")
}
// handleMessage 处理接收到的消息
func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
chatID := message.Chat.ID
text := strings.TrimSpace(message.Text)
utils.Info("[TELEGRAM:MESSAGE] 收到消息: ChatID=%d, Text='%s', User=%s", chatID, text, message.From.UserName)
if text == "" {
return
}
// 处理 /register 命令(包括参数)
if strings.HasPrefix(strings.ToLower(text), "/register") {
utils.Info("[TELEGRAM:MESSAGE] 处理 /register 命令 from ChatID=%d", chatID)
s.handleRegisterCommand(message)
return
}
// 处理 /start 命令
if strings.ToLower(text) == "/start" {
utils.Info("[TELEGRAM:MESSAGE] 处理 /start 命令 from ChatID=%d", chatID)
s.handleStartCommand(message)
return
}
if len(text) == 0 {
return
}
// 默认自动回复(只对正常消息,不对转发消息,且消息没有换行)
if s.config.AutoReplyEnabled {
// 检查是否是转发消息
isForward := message.ForwardFrom != nil ||
message.ForwardFromChat != nil ||
message.ForwardDate != 0
if isForward {
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复,转发消息 from ChatID=%d", chatID)
} else {
// 检查消息是否包含换行符
hasNewLine := strings.Contains(text, "\n") || strings.Contains(text, "\r")
if hasNewLine {
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复,消息包含换行 from ChatID=%d", chatID)
} else {
// 处理普通文本消息(搜索请求)
re := regexp.MustCompile(`^【(\d+)】.*?`)
matches := re.FindStringSubmatch(text)
if len(matches) >= 2 {
utils.Info("[TELEGRAM:MESSAGE] 处理搜索请求 from ChatID=%d: %s", chatID, text)
num, _ := strconv.Atoi(matches[1])
s.handleResourceRequest(message, uint(num))
return
}
sre := regexp.MustCompile(`^搜索(.*?)$`)
smatches := sre.FindStringSubmatch(text)
if len(smatches) >= 2 {
utils.Info("[TELEGRAM:MESSAGE] 处理搜索请求 from ChatID=%d: %s", chatID, text)
keyword := strings.TrimSpace(smatches[1])
s.handleSearchRequest(message, keyword)
return
}
utils.Info("[TELEGRAM:MESSAGE] 发送自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
s.sendReply(message, s.config.AutoReplyTemplate)
}
}
} else {
utils.Info("[TELEGRAM:MESSAGE] 跳过自动回复 to ChatID=%d (AutoReplyEnabled=%v)", chatID, s.config.AutoReplyEnabled)
}
}
// handleRegisterCommand 处理注册命令
func (s *TelegramBotServiceImpl) handleRegisterCommand(message *tgbotapi.Message) {
chatID := message.Chat.ID
text := strings.TrimSpace(message.Text)
// 检查是否是群组
isGroup := message.Chat.IsGroup() || message.Chat.IsSuperGroup()
if isGroup {
// 群组中需要管理员权限
if !s.isUserAdministrator(message.Chat.ID, message.From.ID) {
errorMsg := "❌ *权限不足*\n\n只有群组管理员才能注册此群组用于推送。\n\n请联系管理员执行注册命令。"
s.sendReply(message, errorMsg)
return
}
// 检查是否已经注册了群组
if s.hasActiveGroup() {
errorMsg := "❌ *注册限制*\n\n系统最多只支持注册一个群组用于推送。\n\n请先注销现有群组然后再注册新的群组。"
s.sendReply(message, errorMsg)
return
}
// 注册群组
chatTitle := message.Chat.Title
if chatTitle == "" {
chatTitle = fmt.Sprintf("Group_%d", chatID)
}
err := s.RegisterChannel(chatID, chatTitle, "group")
if err != nil {
if strings.Contains(err.Error(), "该频道/群组已注册") {
successMsg := fmt.Sprintf("⚠️ *群组已注册*\n\n群组: %s\n类型: 群组\n\n此群组已经注册无需重复注册。", chatTitle)
s.sendReply(message, successMsg)
} else {
errorMsg := fmt.Sprintf("❌ 注册失败: %v", err)
s.sendReply(message, errorMsg)
}
return
}
successMsg := fmt.Sprintf("✅ *群组注册成功!*\n\n群组: %s\n类型: 群组\n\n现在可以向此群组推送资源内容了。", chatTitle)
s.sendReply(message, successMsg)
return
}
// 私聊处理
parts := strings.Fields(text)
if len(parts) == 1 {
// 私聊中没有参数,显示注册帮助
helpMsg := `🤖 *注册帮助*
*注册群组:*
* 添加机器人,为频道管理员
* 管理员发送 /register 命令
*注册频道:*
私聊机器人, 发送注册命令
支持两种格式:
• /register <频道ID> - 如: /register -1001234567890
• /register @用户名 - 如: /register @xypan
*获取频道ID的方法:*
1. 将机器人添加到频道并设为管理员
2. 向频道发送消息,查看机器人收到的消息
3. 频道ID通常是负数如 -1001234567890
*示例:*
/register -1001234567890
/register @xypan
*注意:*
• 频道ID必须是纯数字包括负号
• 用户名格式必须以 @ 开头
• 机器人必须是频道的管理员才能注册
• 私聊不支持注册,只支持频道和群组注册`
s.sendReply(message, helpMsg)
} else if parts[1] == "help" || parts[1] == "-h" {
// 显示注册帮助
helpMsg := `🤖 *注册帮助*
*注册群组:*
* 添加机器人,为频道管理员
* 管理员发送 /register 命令
*注册频道:*
私聊机器人, 发送注册命令
支持两种格式:
• /register <频道ID> - 如: /register -1001234567890
• /register @用户名 - 如: /register @xypan
*获取频道ID的方法:*
1. 将机器人添加到频道并设为管理员
2. 向频道发送消息,查看机器人收到的消息
3. 频道ID通常是负数如 -1001234567890
*示例:*
/register -1001234567890
/register @xypan
*注意:*
• 频道ID必须是纯数字包括负号
• 用户名格式必须以 @ 开头
• 机器人必须是频道的管理员才能注册`
s.sendReply(message, helpMsg)
} else {
// 有参数,尝试注册频道
channelIDStr := strings.TrimSpace(parts[1])
s.handleChannelRegistration(message, channelIDStr)
}
}
// handleStartCommand 处理开始命令
func (s *TelegramBotServiceImpl) handleStartCommand(message *tgbotapi.Message) {
welcomeMsg := `🤖 欢迎使用老九网盘资源机器人!
• 发送 搜索 + 关键词 进行资源搜索
• 发送 /register 注册当前频道或群组,用于主动推送资源
• 私聊中使用 /register help 获取注册帮助
• 发送 /start 获取帮助信息
`
if s.config.AutoReplyEnabled && s.config.AutoReplyTemplate != "" {
welcomeMsg += "\n\n" + s.config.AutoReplyTemplate
}
s.sendReply(message, welcomeMsg)
}
// handleSearchRequest 处理搜索请求
func (s *TelegramBotServiceImpl) handleResourceRequest(message *tgbotapi.Message, id uint) {
// 使用资源仓库进行搜索
resources, err := s.resourceRepo.FindByIDs([]uint{uint(id)}) // 限制为5个结果
if err != nil {
utils.Error("[TELEGRAM:SEARCH] 搜索失败: %v", err)
s.sendReply(message, "搜索服务暂时不可用,请稍后重试")
return
}
if len(resources) == 0 {
s.sendReply(message, "未找到该资源")
return
}
// 构建搜索结果消息
resultText := ""
// 显示前5个结果
for i, resource := range resources {
if i >= 1 {
break
}
title := s.cleanMessageTextForHTML(resource.Title)
if resource.SaveURL != "" {
resultText += fmt.Sprintf("<b>%d. %s</>\n<i>%s</i>\n", i+1, title, resource.SaveURL)
} else {
resultText += fmt.Sprintf("<b>%d. %s</>\n<i>%s</i>\n", i+1, title, resource.URL)
}
}
// 使用包含资源的自动删除功能
s.sendReplyWithResourceAutoDelete(message, resultText)
s.sendReply(message, "资源已发送,请注意查收")
}
// handleSearchRequest 处理搜索请求
func (s *TelegramBotServiceImpl) handleSearchRequest(message *tgbotapi.Message, keyword string) {
// 使用资源仓库进行搜索
resources, total, err := s.resourceRepo.Search(keyword, nil, 1, 5) // 限制为5个结果
if err != nil {
utils.Error("[TELEGRAM:SEARCH] 搜索失败: %v", err)
s.sendReply(message, "搜索服务暂时不可用,请稍后重试")
return
}
if total == 0 {
response := fmt.Sprintf("🔍 *搜索结果*\n\n关键词: `%s`\n\n❌ 未找到相关资源\n\n💡 建议:\n• 尝试使用更通用的关键词\n• 检查拼写是否正确\n• 减少关键词数量", keyword)
// 没有找到资源,不使用资源自动删除
s.sendReply(message, response)
return
}
// 构建搜索结果消息
resultText := fmt.Sprintf("🔍 *搜索结果* 总共找到: %d 个资源\n\n", total)
// 显示前5个结果
for i, resource := range resources {
if i >= 5 {
break
}
title := s.cleanMessageTextForHTML(resource.Title)
// description := s.cleanMessageTextForHTML(resource.Description)
if resource.SaveURL != "" {
resultText += fmt.Sprintf("<b>%s</b>\n<a href=\"%s\">%s</a>\n", title, resource.SaveURL, resource.SaveURL)
} else {
resultText += fmt.Sprintf("<b>%s</b>\n<a href=\"%s\">%s</a>\n", title, resource.URL, resource.URL)
}
}
// 如果有更多结果,添加提示
if total > 5 {
resultText += fmt.Sprintf("... 还有 %d 个结果\n\n", total-5)
}
resultText += "<i>如果资源失效请访问,发送搜索 + 关键字,可以搜索资源, 或者访问 https://pan.l9.lc 搜索最新资源</i>"
// 使用包含资源的自动删除功能
s.sendReplyWithResourceAutoDelete(message, resultText)
}
// sendReply 发送回复消息
func (s *TelegramBotServiceImpl) sendReply(message *tgbotapi.Message, text string) {
s.sendReplyWithAutoDelete(message, text, false)
}
// sendReplyWithAutoDelete 发送回复消息,支持指定是否自动删除
func (s *TelegramBotServiceImpl) sendReplyWithAutoDelete(message *tgbotapi.Message, text string, autoDelete bool) {
// 清理消息文本确保UTF-8编码
originalText := text
utils.Info("[TELEGRAM:MESSAGE] 尝试发送回复消息到 ChatID=%d, 原始长度=%d, 清理后长度=%d", message.Chat.ID, len(originalText), len(text))
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ParseMode = "HTML"
msg.ReplyToMessageID = message.MessageID
utils.Debug("[TELEGRAM:MESSAGE] 发送Markdown版本消息: %s", text[:min(100, len(text))])
sentMsg, err := s.bot.Send(msg)
if err != nil {
utils.Error("[TELEGRAM:MESSAGE:ERROR] 发送Markdown消息失败: %v", err)
return
}
utils.Info("[TELEGRAM:MESSAGE:SUCCESS] 消息发送成功 to ChatID=%d, MessageID=%d", sentMsg.Chat.ID, sentMsg.MessageID)
// 如果启用了自动删除,启动删除定时器
if autoDelete && s.config.AutoDeleteInterval > 0 {
utils.Info("[TELEGRAM:MESSAGE] 设置自动删除定时器: %d 分钟后删除消息", s.config.AutoDeleteInterval)
time.AfterFunc(time.Duration(s.config.AutoDeleteInterval)*time.Minute, func() {
deleteConfig := tgbotapi.DeleteMessageConfig{
ChatID: sentMsg.Chat.ID,
MessageID: sentMsg.MessageID,
}
_, err := s.bot.Request(deleteConfig)
if err != nil {
utils.Error("[TELEGRAM:MESSAGE:ERROR] 删除消息失败: %v", err)
} else {
utils.Info("[TELEGRAM:MESSAGE] 消息已自动删除: ChatID=%d, MessageID=%d", sentMsg.Chat.ID, sentMsg.MessageID)
}
})
}
}
// 辅助函数:返回两个数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
// cleanMessageTextForHTML 清理消息文本为HTML格式
func (s *TelegramBotServiceImpl) cleanMessageTextForHTML(text string) string {
if text == "" {
return text
}
text = strings.ReplaceAll(text, "&", "&amp;")
text = strings.ReplaceAll(text, "<", "&lt;")
text = strings.ReplaceAll(text, ">", "&gt;")
return text
}
// sendReplyWithResourceAutoDelete 发送包含资源的回复消息,自动添加删除提醒
func (s *TelegramBotServiceImpl) sendReplyWithResourceAutoDelete(message *tgbotapi.Message, text string) {
// 如果启用了自动删除且有资源,在消息中添加删除提醒
if s.config.AutoDeleteEnabled && s.config.AutoDeleteInterval > 0 {
deleteNotice := fmt.Sprintf("\n\n⏰ <b>此消息将在 %d 分钟后自动删除</b>", s.config.AutoDeleteInterval)
text += deleteNotice
}
// 使用资源消息的特殊删除逻辑
s.sendReplyWithAutoDelete(message, text, true)
}
// startContentPusher 启动内容推送器
func (s *TelegramBotServiceImpl) startContentPusher() {
// 每分钟检查一次需要推送的频道
s.cronScheduler.AddFunc("@every 1m", func() {
s.pushContentToChannels()
})
s.cronScheduler.Start()
utils.Info("[TELEGRAM:PUSH] 内容推送调度器已启动")
}
// pushContentToChannels 推送内容到频道
func (s *TelegramBotServiceImpl) pushContentToChannels() {
// 获取需要推送的频道
channels, err := s.channelRepo.FindDueForPush()
if err != nil {
utils.Error("[TELEGRAM:PUSH:ERROR] 获取推送频道失败: %v", err)
return
}
if len(channels) == 0 {
utils.Debug("[TELEGRAM:PUSH] 没有需要推送的频道")
return
}
utils.Info("[TELEGRAM:PUSH] 开始推送内容到 %d 个频道", len(channels))
for _, channel := range channels {
go s.pushToChannel(channel)
}
}
// pushToChannel 推送内容到一个频道
func (s *TelegramBotServiceImpl) pushToChannel(channel entity.TelegramChannel) {
utils.Info("[TELEGRAM:PUSH] 开始推送到频道: %s (ID: %d)", channel.ChatName, channel.ChatID)
// 1. 根据频道设置过滤资源
resources := s.findResourcesForChannel(channel)
if len(resources) == 0 {
utils.Info("[TELEGRAM:PUSH] 频道 %s 没有可推送的内容", channel.ChatName)
return
}
// 2. 构建推送消息
message, img := s.buildPushMessage(channel, resources)
// 3. 发送消息(推送消息不自动删除,使用 HTML 格式)
err := s.SendMessage(channel.ChatID, message, img)
if err != nil {
utils.Error("[TELEGRAM:PUSH:ERROR] 推送失败到频道 %s (%d): %v", channel.ChatName, channel.ChatID, err)
return
}
// 4. 更新最后推送时间
err = s.channelRepo.UpdateLastPushAt(channel.ID, time.Now())
if err != nil {
utils.Error("[TELEGRAM:PUSH:ERROR] 更新推送时间失败: %v", err)
return
}
utils.Info("[TELEGRAM:PUSH:SUCCESS] 成功推送内容到频道: %s (%d 条资源)", channel.ChatName, len(resources))
}
// findResourcesForChannel 查找适合频道的资源
func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.TelegramChannel) []interface{} {
utils.Info("[TELEGRAM:PUSH] 开始为频道 %s (%d) 查找资源", channel.ChatName, channel.ChatID)
// 获取最近推送的历史资源ID避免重复推送
excludeResourceIDs := s.getRecentlyPushedResourceIDs(channel.ChatID)
// 解析资源策略
strategy := channel.ResourceStrategy
if strategy == "" {
strategy = "random" // 默认纯随机
}
utils.Info("[TELEGRAM:PUSH] 使用策略: %s, 时间限制: %s, 排除最近推送资源数: %d",
strategy, channel.TimeLimit, len(excludeResourceIDs))
// 根据策略获取资源
switch strategy {
case "latest":
// 最新优先策略 - 获取最近的资源
return s.findLatestResources(channel, excludeResourceIDs)
case "transferred":
// 已转存优先策略 - 优先获取有转存链接的资源
return s.findTransferredResources(channel, excludeResourceIDs)
case "random":
// 纯随机策略(原逻辑)
return s.findRandomResources(channel, excludeResourceIDs)
default:
// 默认随机策略
return s.findRandomResources(channel, excludeResourceIDs)
}
}
// findLatestResources 查找最新资源
func (s *TelegramBotServiceImpl) findLatestResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} {
params := s.buildFilterParams(channel)
// 使用现有的搜索功能,按更新时间倒序获取最新资源
resources, _, err := s.resourceRepo.SearchWithFilters(params)
if err != nil {
utils.Error("[TELEGRAM:PUSH] 获取最新资源失败: %v", err)
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
resources = s.excludePushedResources(resources, excludeResourceIDs)
}
// 应用时间限制
if channel.TimeLimit != "none" && len(resources) > 0 {
resources = s.applyTimeFilter(resources, channel.TimeLimit)
}
if len(resources) == 0 {
utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的最新资源")
return []interface{}{}
}
// 返回最新资源(第一条)
utils.Info("[TELEGRAM:PUSH] 成功获取最新资源: %s", resources[0].Title)
return []interface{}{resources[0]}
}
// findTransferredResources 查找已转存资源
func (s *TelegramBotServiceImpl) findTransferredResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} {
params := s.buildFilterParams(channel)
// 添加转存链接条件
params["has_save_url"] = true
// 优先获取有转存链接的资源
resources, _, err := s.resourceRepo.SearchWithFilters(params)
if err != nil {
utils.Error("[TELEGRAM:PUSH] 获取已转存资源失败: %v", err)
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
resources = s.excludePushedResources(resources, excludeResourceIDs)
}
// 应用时间限制
if channel.TimeLimit != "none" && len(resources) > 0 {
resources = s.applyTimeFilter(resources, channel.TimeLimit)
}
if len(resources) == 0 {
utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的已转存资源,尝试获取随机资源")
// 如果没有已转存资源,回退到随机策略
return s.findRandomResources(channel, excludeResourceIDs)
}
// 返回第一个有转存链接的资源
utils.Info("[TELEGRAM:PUSH] 成功获取已转存资源: %s", resources[0].Title)
return []interface{}{resources[0]}
}
// findRandomResources 查找随机资源(原有逻辑)
func (s *TelegramBotServiceImpl) findRandomResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} {
params := s.buildFilterParams(channel)
// 如果是已转存优先策略但没有找到转存资源,这里会回退到随机策略
// 此时不需要额外的转存链接条件,让随机函数处理
// 先尝试获取候选资源列表,然后从中排除已推送的资源
var candidateResources []entity.Resource
var err error
// 使用搜索功能获取候选资源,然后过滤
params["limit"] = 100 // 获取更多候选资源
candidateResources, _, err = s.resourceRepo.SearchWithFilters(params)
if err != nil {
utils.Error("[TELEGRAM:PUSH] 获取候选资源失败: %v", err)
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
candidateResources = s.excludePushedResources(candidateResources, excludeResourceIDs)
}
// 应用时间限制
if channel.TimeLimit != "none" && len(candidateResources) > 0 {
candidateResources = s.applyTimeFilter(candidateResources, channel.TimeLimit)
}
// 如果还有候选资源,随机选择一个
if len(candidateResources) > 0 {
// 简单随机选择(未来可以考虑使用更好的随机算法)
randomIndex := time.Now().Nanosecond() % len(candidateResources)
selectedResource := candidateResources[randomIndex]
utils.Info("[TELEGRAM:PUSH] 成功获取随机资源: %s (从 %d 个候选资源中选择)",
selectedResource.Title, len(candidateResources))
return []interface{}{selectedResource}
}
// 如果候选资源不足,回退到数据库随机函数
defer func() {
if r := recover(); r != nil {
utils.Warn("[TELEGRAM:PUSH] 随机查询失败,回退到传统方法: %v", r)
}
}()
randomResource, err := s.resourceRepo.GetRandomResourceWithFilters(params["category"].(string), params["tag"].(string), channel.IsPushSavedInfo)
if err == nil && randomResource != nil {
utils.Info("[TELEGRAM:PUSH] 使用数据库随机函数获取资源: %s", randomResource.Title)
return []interface{}{randomResource}
}
return []interface{}{}
}
// applyTimeFilter 应用时间限制过滤
func (s *TelegramBotServiceImpl) applyTimeFilter(resources []entity.Resource, timeLimit string) []entity.Resource {
now := time.Now()
var filtered []entity.Resource
for _, resource := range resources {
include := false
switch timeLimit {
case "week":
// 一周内
if resource.CreatedAt.After(now.AddDate(0, 0, -7)) {
include = true
}
case "month":
// 一月内
if resource.CreatedAt.After(now.AddDate(0, -1, 0)) {
include = true
}
case "none":
// 无限制,包含所有
include = true
}
if include {
filtered = append(filtered, resource)
}
}
return filtered
}
// buildFilterParams 构建过滤参数
func (s *TelegramBotServiceImpl) buildFilterParams(channel entity.TelegramChannel) map[string]interface{} {
params := map[string]interface{}{"category": "", "tag": ""}
if channel.ContentCategories != "" {
categories := strings.Split(channel.ContentCategories, ",")
for i, category := range categories {
categories[i] = strings.TrimSpace(category)
}
params["category"] = categories[0]
}
if channel.ContentTags != "" {
tags := strings.Split(channel.ContentTags, ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
params["tag"] = tags[0]
}
return params
}
// buildPushMessage 构建推送消息
func (s *TelegramBotServiceImpl) buildPushMessage(channel entity.TelegramChannel, resources []interface{}) (string, string) {
resource := resources[0].(*entity.Resource)
message := fmt.Sprintf("🆕 <b>%s</b>\n", s.cleanMessageTextForHTML(resource.Title))
if resource.Description != "" {
message += fmt.Sprintf("<blockquote>%s</blockquote>\n", s.cleanMessageTextForHTML(resource.Description))
}
// 添加标签
if len(resource.Tags) > 0 {
message += "\n🏷 "
for i, tag := range resource.Tags {
if i > 0 {
message += " "
}
message += fmt.Sprintf("#%s", tag.Name)
}
message += "\n"
}
// 添加资源信息
message += fmt.Sprintf("\n💡 评论区评论 (<code>【%v】%s</code>) 即可获取资源,括号内名称点击可复制📋\n", resource.ID, resource.Title)
img := ""
if resource.Cover != "" {
img = resource.Cover
} else {
// 从 readyRepo 中取出 extra 字段,解析 JSON 获取 fid用于构造图片URL
readyResources, err := s.readyRepo.FindByKey(resource.Key)
if err == nil && len(readyResources) > 0 {
readyResource := readyResources[0]
if readyResource.Extra != "" {
var extraData map[string]interface{}
if err := json.Unmarshal([]byte(readyResource.Extra), &extraData); err == nil {
if fid, ok := extraData["fid"].(string); ok && fid != "" {
img = fid
}
}
}
}
}
return message, img
}
func (s *TelegramBotServiceImpl) GetImgUrl(fid string) string {
return fmt.Sprintf("http://tg.9book.top:3000/api/tool/file/%s", fid)
}
// GetBotUsername 获取机器人用户名
func (s *TelegramBotServiceImpl) GetBotUsername() string {
if s.bot != nil {
return s.bot.Self.UserName
}
return ""
}
// SendMessage 发送消息(默认使用 HTML 格式)
func (s *TelegramBotServiceImpl) SendMessage(chatID int64, text string, img string) error {
if img == "" {
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "HTML"
_, err := s.bot.Send(msg)
if err != nil {
utils.Error("[TELEGRAM:MESSAGE:ERROR] 发送消息失败: %v", err)
}
return err
} else {
// 如果 img 以 http 开头则为图片URL否则为文件remote_id
if strings.HasPrefix(img, "http") {
// 发送图片URL
photoMsg := tgbotapi.NewPhoto(chatID, tgbotapi.FileURL(img))
photoMsg.Caption = text
photoMsg.ParseMode = "HTML"
_, err := s.bot.Send(photoMsg)
if err != nil {
utils.Error("[TELEGRAM:MESSAGE:ERROR] 发送图片消息失败: %v", err)
}
return err
} else {
// imgUrl := s.GetImgUrl(img)
//todo 判断 imgUrl 是否可用
// 发送文件ID
photoMsg := tgbotapi.NewPhoto(chatID, tgbotapi.FileID(img))
photoMsg.Caption = text
photoMsg.ParseMode = "HTML"
_, err := s.bot.Send(photoMsg)
if err != nil {
utils.Error("[TELEGRAM:MESSAGE:ERROR] 发送图片消息失败: %v", err)
}
return err
}
}
}
// DeleteMessage 删除消息
func (s *TelegramBotServiceImpl) DeleteMessage(chatID int64, messageID int) error {
if s.bot == nil {
return fmt.Errorf("Bot 未初始化")
}
deleteConfig := tgbotapi.NewDeleteMessage(chatID, messageID)
_, err := s.bot.Request(deleteConfig)
return err
}
// RegisterChannel 注册频道
func (s *TelegramBotServiceImpl) RegisterChannel(chatID int64, chatName, chatType string) error {
// 检查是否已注册
if s.IsChannelRegistered(chatID) {
return fmt.Errorf("该频道/群组已注册")
}
channel := entity.TelegramChannel{
ChatID: chatID,
ChatName: chatName,
ChatType: chatType,
PushEnabled: true,
PushFrequency: 15, // 默认15分钟
PushStartTime: "08:30", // 默认开始时间8:30
PushEndTime: "11:30", // 默认结束时间11:30
IsActive: true,
RegisteredBy: "bot_command",
RegisteredAt: time.Now(),
ContentCategories: "",
ContentTags: "",
API: "", // 后续可配置
Token: "", // 后续可配置
ApiType: "l9", // 默认l9类型
IsPushSavedInfo: false, // 默认推送所有资源
ResourceStrategy: "random", // 默认纯随机
TimeLimit: "none", // 默认无限制
}
return s.channelRepo.Create(&channel)
}
// IsChannelRegistered 检查频道是否已注册
func (s *TelegramBotServiceImpl) IsChannelRegistered(chatID int64) bool {
channel, err := s.channelRepo.FindByChatID(chatID)
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"
}
// isBotAdministrator 检查机器人是否为频道管理员
func (s *TelegramBotServiceImpl) isBotAdministrator(chatID int64) bool {
if s.bot == nil {
return false
}
// 获取机器人自己的信息
botInfo, err := s.bot.GetMe()
if err != nil {
utils.Error("[TELEGRAM:ADMIN:BOT] 获取机器人信息失败: %v", err)
return false
}
// 获取机器人作为频道成员的信息
memberConfig := tgbotapi.GetChatMemberConfig{
ChatConfigWithUser: tgbotapi.ChatConfigWithUser{
ChatID: chatID,
UserID: botInfo.ID,
},
}
member, err := s.bot.GetChatMember(memberConfig)
if err != nil {
utils.Error("[TELEGRAM:ADMIN:BOT] 获取机器人频道成员信息失败: %v", err)
return false
}
// 检查机器人是否为管理员或创建者
botStatus := string(member.Status)
utils.Info("[TELEGRAM:ADMIN:BOT] 机器人状态: %s (ChatID: %d)", botStatus, chatID)
return botStatus == "administrator" || botStatus == "creator"
}
// hasActiveGroup 检查是否已经注册了活跃的群组
func (s *TelegramBotServiceImpl) hasActiveGroup() bool {
channels, err := s.channelRepo.FindByChatType("group")
if err != nil {
utils.Error("[TELEGRAM:LIMIT] 检查活跃群组失败: %v", err)
return false
}
// 检查是否有活跃的群组
for _, channel := range channels {
if channel.IsActive {
return true
}
}
return false
}
// hasActiveChannel 检查是否已经注册了活跃的频道
func (s *TelegramBotServiceImpl) hasActiveChannel() bool {
channels, err := s.channelRepo.FindByChatType("channel")
if err != nil {
utils.Error("[TELEGRAM:LIMIT] 检查活跃频道失败: %v", err)
return false
}
// 检查是否有活跃的频道
for _, channel := range channels {
if channel.IsActive {
return true
}
}
return false
}
// 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
// 首先获取频道信息,然后检查机器人权限
// 这一步会在后面的逻辑中完成获取chat对象后再检查权限
// 判断是频道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)
// 检查机器人是否是频道管理员
if !s.isBotAdministrator(chat.ID) {
errorMsg := "❌ *权限不足*\n\n机器人必须是频道的管理员才能注册此频道用于推送。\n\n请先将机器人添加为频道管理员然后重试注册命令。"
s.sendReply(message, errorMsg)
return
}
} 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
}
// 检查机器人是否是频道管理员
if !s.isBotAdministrator(chat.ID) {
errorMsg := "❌ *权限不足*\n\n机器人必须是频道的管理员才能注册此频道用于推送。\n\n请先将机器人添加为频道管理员然后重试注册命令。"
s.sendReply(message, errorMsg)
return
}
// 检查是否已经注册了频道
if s.hasActiveChannel() {
errorMsg := "❌ *注册限制*\n\n系统最多只支持注册一个频道用于推送。\n\n请先注销现有频道然后再注册新的频道。"
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
}
// 尝试查找现有频道
existingChannel, findErr := s.channelRepo.FindByChatID(chat.ID)
if findErr == nil && existingChannel != nil {
// 频道已存在,更新信息
existingChannel.ChatName = chat.Title
existingChannel.RegisteredBy = message.From.UserName
existingChannel.RegisteredAt = time.Now()
existingChannel.IsActive = true
existingChannel.PushEnabled = true
// 为现有频道设置默认值
if existingChannel.ApiType == "" {
existingChannel.ApiType = "telegram"
}
if existingChannel.ResourceStrategy == "" {
existingChannel.ResourceStrategy = "random"
}
if existingChannel.TimeLimit == "" {
existingChannel.TimeLimit = "none"
}
if existingChannel.PushFrequency == 0 {
existingChannel.PushFrequency = 15
}
if existingChannel.PushStartTime == "" {
existingChannel.PushStartTime = "08:30"
}
if existingChannel.PushEndTime == "" {
existingChannel.PushEndTime = "11:30"
}
err := s.channelRepo.Update(existingChannel)
if err != nil {
errorMsg := fmt.Sprintf("❌ 频道更新失败: %v", err)
s.sendReply(message, errorMsg)
return
}
successMsg := fmt.Sprintf("✅ *频道更新成功!*\n\n频道: %s\n%s\n类型: 频道\n\n频道信息已更新现在可以正常推送内容。", chat.Title, identifier)
s.sendReply(message, successMsg)
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: "",
API: "", // 后续可配置
Token: "", // 后续可配置
ApiType: "telegram", // 默认telegram类型
IsPushSavedInfo: false, // 默认推送所有资源
}
createErr := s.channelRepo.Create(&channel)
if createErr != nil {
// 如果创建失败,可能是因为并发或其他问题,再次尝试查找
if existing, retryErr := s.channelRepo.FindByChatID(chat.ID); retryErr == nil && existing != nil {
successMsg := fmt.Sprintf("⚠️ *频道已注册*\n\n频道: %s\n%s\n类型: 频道\n\n此频道已经注册无需重复注册。", chat.Title, identifier)
s.sendReply(message, successMsg)
} else {
errorMsg := fmt.Sprintf("❌ 频道注册失败: %v", createErr)
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 更新(预留接口,目前使用长轮询)
func (s *TelegramBotServiceImpl) HandleWebhookUpdate(c interface{}) {
// 目前使用长轮询模式webhook 接口预留
// 将来可以实现从 webhook 接收消息的处理逻辑
// 如果需要实现 webhook 模式,可以在这里添加处理逻辑
}
// CleanupDuplicateChannels 清理数据库中的重复频道记录
func (s *TelegramBotServiceImpl) CleanupDuplicateChannels() error {
utils.Info("[TELEGRAM:CLEANUP] 开始清理重复的频道记录...")
err := s.channelRepo.CleanupDuplicateChannels()
if err != nil {
utils.Error("[TELEGRAM:CLEANUP:ERROR] 清理重复频道记录失败: %v", err)
return fmt.Errorf("清理重复频道记录失败: %v", err)
}
utils.Info("[TELEGRAM:CLEANUP:SUCCESS] 成功清理重复的频道记录")
return nil
}
// getRecentlyPushedResourceIDs 获取最近推送过的资源ID列表
func (s *TelegramBotServiceImpl) getRecentlyPushedResourceIDs(chatID int64) []uint {
// 这里需要实现获取推送历史的逻辑
// 由于没有现有的推送历史表,我们暂时返回空列表
// 未来可以添加一个 TelegramPushHistory 实体来跟踪推送历史
utils.Debug("[TELEGRAM:PUSH] 获取推送历史ChatID: %d", chatID)
// 暂时返回空列表,表示没有历史推送记录
// TODO: 实现推送历史跟踪功能
return []uint{}
}
// excludePushedResources 从候选资源中排除已推送过的资源
func (s *TelegramBotServiceImpl) excludePushedResources(resources []entity.Resource, excludeIDs []uint) []entity.Resource {
if len(excludeIDs) == 0 {
return resources
}
utils.Debug("[TELEGRAM:PUSH] 排除 %d 个已推送资源", len(excludeIDs))
// 创建排除ID的映射提高查找效率
excludeMap := make(map[uint]bool)
for _, id := range excludeIDs {
excludeMap[id] = true
}
// 过滤资源列表
var filtered []entity.Resource
for _, resource := range resources {
if !excludeMap[resource.ID] {
filtered = append(filtered, resource)
}
}
utils.Debug("[TELEGRAM:PUSH] 过滤后剩余 %d 个资源", len(filtered))
return filtered
}