mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
add: tgbot
This commit is contained in:
181
db/converter/telegram_channel_converter.go
Normal file
181
db/converter/telegram_channel_converter.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// TelegramChannelToResponse 将TelegramChannel实体转换为响应DTO
|
||||
func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChannelResponse {
|
||||
return dto.TelegramChannelResponse{
|
||||
ID: channel.ID,
|
||||
ChatID: channel.ChatID,
|
||||
ChatName: channel.ChatName,
|
||||
ChatType: channel.ChatType,
|
||||
PushEnabled: channel.PushEnabled,
|
||||
PushFrequency: channel.PushFrequency,
|
||||
ContentCategories: channel.ContentCategories,
|
||||
ContentTags: channel.ContentTags,
|
||||
IsActive: channel.IsActive,
|
||||
LastPushAt: channel.LastPushAt,
|
||||
RegisteredBy: channel.RegisteredBy,
|
||||
RegisteredAt: channel.RegisteredAt,
|
||||
}
|
||||
}
|
||||
|
||||
// TelegramChannelsToResponse 将TelegramChannel实体列表转换为响应DTO列表
|
||||
func TelegramChannelsToResponse(channels []entity.TelegramChannel) []dto.TelegramChannelResponse {
|
||||
var responses []dto.TelegramChannelResponse
|
||||
for _, channel := range channels {
|
||||
responses = append(responses, TelegramChannelToResponse(channel))
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// RequestToTelegramChannel 将请求DTO转换为TelegramChannel实体
|
||||
func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy string) entity.TelegramChannel {
|
||||
return entity.TelegramChannel{
|
||||
ChatID: req.ChatID,
|
||||
ChatName: req.ChatName,
|
||||
ChatType: req.ChatType,
|
||||
PushEnabled: req.PushEnabled,
|
||||
PushFrequency: req.PushFrequency,
|
||||
ContentCategories: req.ContentCategories,
|
||||
ContentTags: req.ContentTags,
|
||||
IsActive: req.IsActive,
|
||||
RegisteredBy: registeredBy,
|
||||
RegisteredAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// TelegramBotConfigToResponse 将Telegram bot配置转换为响应DTO
|
||||
func TelegramBotConfigToResponse(
|
||||
botEnabled bool,
|
||||
botApiKey string,
|
||||
autoReplyEnabled bool,
|
||||
autoReplyTemplate string,
|
||||
autoDeleteEnabled bool,
|
||||
autoDeleteInterval int,
|
||||
) dto.TelegramBotConfigResponse {
|
||||
return dto.TelegramBotConfigResponse{
|
||||
BotEnabled: botEnabled,
|
||||
BotApiKey: botApiKey,
|
||||
AutoReplyEnabled: autoReplyEnabled,
|
||||
AutoReplyTemplate: autoReplyTemplate,
|
||||
AutoDeleteEnabled: autoDeleteEnabled,
|
||||
AutoDeleteInterval: autoDeleteInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemConfigToTelegramBotConfig 将系统配置转换为Telegram bot配置响应
|
||||
func SystemConfigToTelegramBotConfig(configs []entity.SystemConfig) dto.TelegramBotConfigResponse {
|
||||
botEnabled := false
|
||||
botApiKey := ""
|
||||
autoReplyEnabled := true
|
||||
autoReplyTemplate := "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
|
||||
autoDeleteEnabled := false
|
||||
autoDeleteInterval := 60
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyTelegramBotEnabled:
|
||||
botEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramBotApiKey:
|
||||
botApiKey = config.Value
|
||||
case entity.ConfigKeyTelegramAutoReplyEnabled:
|
||||
autoReplyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoReplyTemplate:
|
||||
autoReplyTemplate = config.Value
|
||||
case entity.ConfigKeyTelegramAutoDeleteEnabled:
|
||||
autoDeleteEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoDeleteInterval:
|
||||
if config.Value != "" {
|
||||
// 简单解析整数,这里可以改进错误处理
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(config.Value, "%d", &val); err == nil {
|
||||
autoDeleteInterval = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TelegramBotConfigToResponse(
|
||||
botEnabled,
|
||||
botApiKey,
|
||||
autoReplyEnabled,
|
||||
autoReplyTemplate,
|
||||
autoDeleteEnabled,
|
||||
autoDeleteInterval,
|
||||
)
|
||||
}
|
||||
|
||||
// TelegramBotConfigRequestToSystemConfigs 将Telegram bot配置请求转换为系统配置实体列表
|
||||
func TelegramBotConfigRequestToSystemConfigs(req dto.TelegramBotConfigRequest) []entity.SystemConfig {
|
||||
configs := []entity.SystemConfig{}
|
||||
|
||||
if req.BotEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramBotEnabled,
|
||||
Value: boolToString(*req.BotEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.BotApiKey != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramBotApiKey,
|
||||
Value: *req.BotApiKey,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoReplyEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoReplyEnabled,
|
||||
Value: boolToString(*req.AutoReplyEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoReplyTemplate != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoReplyTemplate,
|
||||
Value: *req.AutoReplyTemplate,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoDeleteEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoDeleteEnabled,
|
||||
Value: boolToString(*req.AutoDeleteEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoDeleteInterval != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoDeleteInterval,
|
||||
Value: intToString(*req.AutoDeleteInterval),
|
||||
Type: entity.ConfigTypeInt,
|
||||
})
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
// 辅助函数:布尔转换为字符串
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// 辅助函数:整数转换为字符串
|
||||
func intToString(i int) string {
|
||||
return fmt.Sprintf("%d", i)
|
||||
}
|
||||
63
db/dto/telegram_channel.go
Normal file
63
db/dto/telegram_channel.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// TelegramChannelRequest 创建/更新 Telegram 频道/群组请求
|
||||
type TelegramChannelRequest struct {
|
||||
ChatID int64 `json:"chat_id" binding:"required"`
|
||||
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"`
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// TelegramChannelResponse Telegram 频道/群组响应
|
||||
type TelegramChannelResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ChatID int64 `json:"chat_id"`
|
||||
ChatName string `json:"chat_name"`
|
||||
ChatType string `json:"chat_type"`
|
||||
PushEnabled bool `json:"push_enabled"`
|
||||
PushFrequency int `json:"push_frequency"`
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
LastPushAt *time.Time `json:"last_push_at"`
|
||||
RegisteredBy string `json:"registered_by"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
}
|
||||
|
||||
// TelegramBotConfigRequest Telegram 机器人配置请求
|
||||
type TelegramBotConfigRequest struct {
|
||||
BotEnabled *bool `json:"bot_enabled"`
|
||||
BotApiKey *string `json:"bot_api_key"`
|
||||
AutoReplyEnabled *bool `json:"auto_reply_enabled"`
|
||||
AutoReplyTemplate *string `json:"auto_reply_template"`
|
||||
AutoDeleteEnabled *bool `json:"auto_delete_enabled"`
|
||||
AutoDeleteInterval *int `json:"auto_delete_interval"`
|
||||
}
|
||||
|
||||
// TelegramBotConfigResponse Telegram 机器人配置响应
|
||||
type TelegramBotConfigResponse struct {
|
||||
BotEnabled bool `json:"bot_enabled"`
|
||||
BotApiKey string `json:"bot_api_key"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
AutoReplyTemplate string `json:"auto_reply_template"`
|
||||
AutoDeleteEnabled bool `json:"auto_delete_enabled"`
|
||||
AutoDeleteInterval int `json:"auto_delete_interval"`
|
||||
}
|
||||
|
||||
// ValidateTelegramApiKeyRequest 验证 Telegram API Key 请求
|
||||
type ValidateTelegramApiKeyRequest struct {
|
||||
ApiKey string `json:"api_key" binding:"required"`
|
||||
}
|
||||
|
||||
// ValidateTelegramApiKeyResponse 验证 Telegram API Key 响应
|
||||
type ValidateTelegramApiKeyResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Error string `json:"error,omitempty"`
|
||||
BotInfo map[string]interface{} `json:"bot_info,omitempty"`
|
||||
}
|
||||
@@ -42,6 +42,14 @@ const (
|
||||
ConfigKeyMeilisearchPort = "meilisearch_port"
|
||||
ConfigKeyMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigKeyMeilisearchIndexName = "meilisearch_index_name"
|
||||
|
||||
// Telegram配置
|
||||
ConfigKeyTelegramBotEnabled = "telegram_bot_enabled"
|
||||
ConfigKeyTelegramBotApiKey = "telegram_bot_api_key"
|
||||
ConfigKeyTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
|
||||
ConfigKeyTelegramAutoReplyTemplate = "telegram_auto_reply_template"
|
||||
ConfigKeyTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
|
||||
ConfigKeyTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -98,6 +106,14 @@ const (
|
||||
ConfigResponseFieldMeilisearchPort = "meilisearch_port"
|
||||
ConfigResponseFieldMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigResponseFieldMeilisearchIndexName = "meilisearch_index_name"
|
||||
|
||||
// Telegram配置字段
|
||||
ConfigResponseFieldTelegramBotEnabled = "telegram_bot_enabled"
|
||||
ConfigResponseFieldTelegramBotApiKey = "telegram_bot_api_key"
|
||||
ConfigResponseFieldTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
|
||||
ConfigResponseFieldTelegramAutoReplyTemplate = "telegram_auto_reply_template"
|
||||
ConfigResponseFieldTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
|
||||
ConfigResponseFieldTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
@@ -141,4 +157,12 @@ const (
|
||||
ConfigDefaultMeilisearchPort = "7700"
|
||||
ConfigDefaultMeilisearchMasterKey = ""
|
||||
ConfigDefaultMeilisearchIndexName = "resources"
|
||||
|
||||
// Telegram配置默认值
|
||||
ConfigDefaultTelegramBotEnabled = "false"
|
||||
ConfigDefaultTelegramBotApiKey = ""
|
||||
ConfigDefaultTelegramAutoReplyEnabled = "true"
|
||||
ConfigDefaultTelegramAutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
|
||||
ConfigDefaultTelegramAutoDeleteEnabled = "false"
|
||||
ConfigDefaultTelegramAutoDeleteInterval = "60"
|
||||
)
|
||||
|
||||
36
db/entity/telegram_channel.go
Normal file
36
db/entity/telegram_channel.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TelegramChannel Telegram 频道/群组实体
|
||||
type TelegramChannel struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Telegram 频道/群组信息
|
||||
ChatID int64 `json:"chat_id" gorm:"not null;comment:Telegram 聊天ID"`
|
||||
ChatName string `json:"chat_name" gorm:"size:255;not null;comment:聊天名称"`
|
||||
ChatType string `json:"chat_type" gorm:"size:50;not null;comment:类型:channel/group"`
|
||||
|
||||
// 推送配置
|
||||
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
|
||||
PushFrequency int `json:"push_frequency" gorm:"default:24;comment:推送频率(小时)"`
|
||||
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
|
||||
ContentTags string `json:"content_tags" gorm:"type:text;comment:推送的标签,用逗号分隔"`
|
||||
|
||||
// 频道状态
|
||||
IsActive bool `json:"is_active" gorm:"default:true;comment:是否活跃"`
|
||||
LastPushAt *time.Time `json:"last_push_at" gorm:"comment:最后推送时间"`
|
||||
|
||||
// 注册信息
|
||||
RegisteredBy string `json:"registered_by" gorm:"size:100;comment:注册者用户名"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (TelegramChannel) TableName() string {
|
||||
return "telegram_channels"
|
||||
}
|
||||
@@ -6,38 +6,40 @@ import (
|
||||
|
||||
// RepositoryManager Repository管理器
|
||||
type RepositoryManager struct {
|
||||
PanRepository PanRepository
|
||||
CksRepository CksRepository
|
||||
ResourceRepository ResourceRepository
|
||||
CategoryRepository CategoryRepository
|
||||
TagRepository TagRepository
|
||||
ReadyResourceRepository ReadyResourceRepository
|
||||
UserRepository UserRepository
|
||||
SearchStatRepository SearchStatRepository
|
||||
SystemConfigRepository SystemConfigRepository
|
||||
HotDramaRepository HotDramaRepository
|
||||
ResourceViewRepository ResourceViewRepository
|
||||
TaskRepository TaskRepository
|
||||
TaskItemRepository TaskItemRepository
|
||||
FileRepository FileRepository
|
||||
PanRepository PanRepository
|
||||
CksRepository CksRepository
|
||||
ResourceRepository ResourceRepository
|
||||
CategoryRepository CategoryRepository
|
||||
TagRepository TagRepository
|
||||
ReadyResourceRepository ReadyResourceRepository
|
||||
UserRepository UserRepository
|
||||
SearchStatRepository SearchStatRepository
|
||||
SystemConfigRepository SystemConfigRepository
|
||||
HotDramaRepository HotDramaRepository
|
||||
ResourceViewRepository ResourceViewRepository
|
||||
TaskRepository TaskRepository
|
||||
TaskItemRepository TaskItemRepository
|
||||
FileRepository FileRepository
|
||||
TelegramChannelRepository TelegramChannelRepository
|
||||
}
|
||||
|
||||
// NewRepositoryManager 创建Repository管理器
|
||||
func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
||||
return &RepositoryManager{
|
||||
PanRepository: NewPanRepository(db),
|
||||
CksRepository: NewCksRepository(db),
|
||||
ResourceRepository: NewResourceRepository(db),
|
||||
CategoryRepository: NewCategoryRepository(db),
|
||||
TagRepository: NewTagRepository(db),
|
||||
ReadyResourceRepository: NewReadyResourceRepository(db),
|
||||
UserRepository: NewUserRepository(db),
|
||||
SearchStatRepository: NewSearchStatRepository(db),
|
||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||
HotDramaRepository: NewHotDramaRepository(db),
|
||||
ResourceViewRepository: NewResourceViewRepository(db),
|
||||
TaskRepository: NewTaskRepository(db),
|
||||
TaskItemRepository: NewTaskItemRepository(db),
|
||||
FileRepository: NewFileRepository(db),
|
||||
PanRepository: NewPanRepository(db),
|
||||
CksRepository: NewCksRepository(db),
|
||||
ResourceRepository: NewResourceRepository(db),
|
||||
CategoryRepository: NewCategoryRepository(db),
|
||||
TagRepository: NewTagRepository(db),
|
||||
ReadyResourceRepository: NewReadyResourceRepository(db),
|
||||
UserRepository: NewUserRepository(db),
|
||||
SearchStatRepository: NewSearchStatRepository(db),
|
||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||
HotDramaRepository: NewHotDramaRepository(db),
|
||||
ResourceViewRepository: NewResourceViewRepository(db),
|
||||
TaskRepository: NewTaskRepository(db),
|
||||
TaskItemRepository: NewTaskItemRepository(db),
|
||||
FileRepository: NewFileRepository(db),
|
||||
TelegramChannelRepository: NewTelegramChannelRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
94
db/repo/telegram_channel_repository.go
Normal file
94
db/repo/telegram_channel_repository.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TelegramChannelRepository interface {
|
||||
BaseRepository[entity.TelegramChannel]
|
||||
FindActiveChannels() ([]entity.TelegramChannel, error)
|
||||
FindByChatID(chatID int64) (*entity.TelegramChannel, error)
|
||||
FindByChatType(chatType string) ([]entity.TelegramChannel, error)
|
||||
UpdateLastPushAt(id uint, lastPushAt time.Time) error
|
||||
FindDueForPush() ([]entity.TelegramChannel, error)
|
||||
}
|
||||
|
||||
type TelegramChannelRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.TelegramChannel]
|
||||
}
|
||||
|
||||
func NewTelegramChannelRepository(db *gorm.DB) TelegramChannelRepository {
|
||||
return &TelegramChannelRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.TelegramChannel]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// 实现基类方法
|
||||
func (r *TelegramChannelRepositoryImpl) Create(entity *entity.TelegramChannel) error {
|
||||
return r.db.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) Update(entity *entity.TelegramChannel) error {
|
||||
return r.db.Save(entity).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) Delete(id uint) error {
|
||||
return r.db.Delete(&entity.TelegramChannel{}, id).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) FindByID(id uint) (*entity.TelegramChannel, error) {
|
||||
var channel entity.TelegramChannel
|
||||
err := r.db.First(&channel, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) FindAll() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindActiveChannels 查找活跃的频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindActiveChannels() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("is_active = ? AND push_enabled = ?", true, true).Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindByChatID 根据 ChatID 查找频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindByChatID(chatID int64) (*entity.TelegramChannel, error) {
|
||||
var channel entity.TelegramChannel
|
||||
err := r.db.Where("chat_id = ?", chatID).First(&channel).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// FindByChatType 根据类型查找频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindByChatType(chatType string) ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("chat_type = ?", chatType).Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// UpdateLastPushAt 更新最后推送时间
|
||||
func (r *TelegramChannelRepositoryImpl) UpdateLastPushAt(id uint, lastPushAt time.Time) error {
|
||||
return r.db.Model(&entity.TelegramChannel{}).Where("id = ?", id).Update("last_push_at", lastPushAt).Error
|
||||
}
|
||||
|
||||
// FindDueForPush 查找需要推送的频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindDueForPush() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
// 查找活跃、启用推送的频道,且距离上次推送已超过推送频率小时的记录
|
||||
err := r.db.Where("is_active = ? AND push_enabled = ? AND (last_push_at IS NULL OR last_push_at < DATE_SUB(NOW(), INTERVAL push_frequency HOUR))",
|
||||
true, true).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -8,9 +8,11 @@ require (
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/meilisearch/meilisearch-go v0.33.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
golang.org/x/crypto v0.40.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -36,6 +36,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
@@ -98,6 +100,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
|
||||
258
handlers/telegram_handler.go
Normal file
258
handlers/telegram_handler.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TelegramHandler Telegram 处理器
|
||||
type TelegramHandler struct {
|
||||
telegramChannelRepo repo.TelegramChannelRepository
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
telegramBotService services.TelegramBotService
|
||||
}
|
||||
|
||||
// NewTelegramHandler 创建 Telegram 处理器
|
||||
func NewTelegramHandler(
|
||||
telegramChannelRepo repo.TelegramChannelRepository,
|
||||
systemConfigRepo repo.SystemConfigRepository,
|
||||
telegramBotService services.TelegramBotService,
|
||||
) *TelegramHandler {
|
||||
return &TelegramHandler{
|
||||
telegramChannelRepo: telegramChannelRepo,
|
||||
systemConfigRepo: systemConfigRepo,
|
||||
telegramBotService: telegramBotService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBotConfig 获取机器人配置
|
||||
func (h *TelegramHandler) GetBotConfig(c *gin.Context) {
|
||||
configs, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
botConfig := converter.SystemConfigToTelegramBotConfig(configs)
|
||||
SuccessResponse(c, botConfig)
|
||||
}
|
||||
|
||||
// UpdateBotConfig 更新机器人配置
|
||||
func (h *TelegramHandler) UpdateBotConfig(c *gin.Context) {
|
||||
var req dto.TelegramBotConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为系统配置实体
|
||||
configs := converter.TelegramBotConfigRequestToSystemConfigs(req)
|
||||
|
||||
// 保存配置
|
||||
if len(configs) > 0 {
|
||||
err := h.systemConfigRepo.UpsertConfigs(configs)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "保存配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载配置缓存
|
||||
if err := h.systemConfigRepo.SafeRefreshConfigCache(); err != nil {
|
||||
ErrorResponse(c, "刷新配置缓存失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 这里应该启动或停止 Telegram bot 服务
|
||||
// if req.BotEnabled != nil && *req.BotEnabled {
|
||||
// go h.telegramBotService.Start()
|
||||
// } else {
|
||||
// go h.telegramBotService.Stop()
|
||||
// }
|
||||
|
||||
// 返回成功
|
||||
SuccessResponse(c, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "配置更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateApiKey 校验 API Key
|
||||
func (h *TelegramHandler) ValidateApiKey(c *gin.Context) {
|
||||
var req dto.ValidateTelegramApiKeyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
valid, botInfo, err := h.telegramBotService.ValidateApiKey(req.ApiKey)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "校验失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.ValidateTelegramApiKeyResponse{
|
||||
Valid: valid,
|
||||
BotInfo: botInfo,
|
||||
}
|
||||
|
||||
if !valid {
|
||||
response.Error = "无效的 API Key"
|
||||
}
|
||||
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetChannels 获取频道列表
|
||||
func (h *TelegramHandler) GetChannels(c *gin.Context) {
|
||||
channels, err := h.telegramChannelRepo.FindAll()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取频道列表失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
channelResponses := converter.TelegramChannelsToResponse(channels)
|
||||
SuccessResponse(c, channelResponses)
|
||||
}
|
||||
|
||||
// CreateChannel 创建频道
|
||||
func (h *TelegramHandler) CreateChannel(c *gin.Context) {
|
||||
var req dto.TelegramChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查频道是否已存在
|
||||
existing, err := h.telegramChannelRepo.FindByChatID(req.ChatID)
|
||||
if err == nil && existing != nil {
|
||||
ErrorResponse(c, "该频道/群组已注册", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户信息作为注册者
|
||||
username := getCurrentUsername(c) // 需要实现获取用户信息的方法
|
||||
|
||||
channel := converter.RequestToTelegramChannel(req, username)
|
||||
|
||||
if err := h.telegramChannelRepo.Create(&channel); err != nil {
|
||||
ErrorResponse(c, "创建频道失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.TelegramChannelToResponse(channel)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// UpdateChannel 更新频道
|
||||
func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.TelegramChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找现有频道
|
||||
channel, err := h.telegramChannelRepo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "频道不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新频道信息
|
||||
channel.ChatName = req.ChatName
|
||||
channel.ChatType = req.ChatType
|
||||
channel.PushEnabled = req.PushEnabled
|
||||
channel.PushFrequency = req.PushFrequency
|
||||
channel.ContentCategories = req.ContentCategories
|
||||
channel.ContentTags = req.ContentTags
|
||||
channel.IsActive = req.IsActive
|
||||
|
||||
if err := h.telegramChannelRepo.Update(channel); err != nil {
|
||||
ErrorResponse(c, "更新频道失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.TelegramChannelToResponse(*channel)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// DeleteChannel 删除频道
|
||||
func (h *TelegramHandler) DeleteChannel(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查频道是否存在
|
||||
channel, err := h.telegramChannelRepo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "频道不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 删除频道
|
||||
if err := h.telegramChannelRepo.Delete(uint(id)); err != nil {
|
||||
ErrorResponse(c, "删除频道失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "频道 " + channel.ChatName + " 已成功移除",
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterChannelByCommand 通过命令注册频道(供内部调用)
|
||||
func (h *TelegramHandler) RegisterChannelByCommand(chatID int64, chatName, chatType string) error {
|
||||
// 检查是否已注册
|
||||
existing, err := h.telegramChannelRepo.FindByChatID(chatID)
|
||||
if err == nil && existing != nil {
|
||||
// 已存在,返回成功
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建新的频道记录
|
||||
channel := entity.TelegramChannel{
|
||||
ChatID: chatID,
|
||||
ChatName: chatName,
|
||||
ChatType: chatType,
|
||||
PushEnabled: true,
|
||||
PushFrequency: 24, // 默认24小时
|
||||
IsActive: true,
|
||||
RegisteredBy: "bot_command",
|
||||
}
|
||||
|
||||
return h.telegramChannelRepo.Create(&channel)
|
||||
}
|
||||
|
||||
// HandleWebhook 处理 Telegram Webhook
|
||||
func (h *TelegramHandler) HandleWebhook(c *gin.Context) {
|
||||
// 将消息交给 bot 服务处理
|
||||
// 这里可以根据需要添加身份验证
|
||||
h.telegramBotService.HandleWebhookUpdate(c)
|
||||
}
|
||||
|
||||
// getCurrentUsername 获取当前用户名(临时实现)
|
||||
func getCurrentUsername(c *gin.Context) string {
|
||||
// 这里应该从中间件中获取用户信息
|
||||
// 暂时返回默认值
|
||||
return "admin"
|
||||
}
|
||||
27
main.go
27
main.go
@@ -323,6 +323,33 @@ func main() {
|
||||
api.GET("/files", middleware.AuthMiddleware(), fileHandler.GetFileList)
|
||||
api.DELETE("/files", middleware.AuthMiddleware(), fileHandler.DeleteFiles)
|
||||
api.PUT("/files", middleware.AuthMiddleware(), fileHandler.UpdateFile)
|
||||
|
||||
// 创建Telegram Bot服务
|
||||
telegramBotService := services.NewTelegramBotService(
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.TelegramChannelRepository,
|
||||
repoManager.ResourceRepository,
|
||||
)
|
||||
|
||||
// 启动Telegram Bot服务
|
||||
if err := telegramBotService.Start(); err != nil {
|
||||
utils.Error("启动Telegram Bot服务失败: %v", err)
|
||||
}
|
||||
|
||||
// Telegram相关路由
|
||||
telegramHandler := handlers.NewTelegramHandler(
|
||||
repoManager.TelegramChannelRepository,
|
||||
repoManager.SystemConfigRepository,
|
||||
telegramBotService,
|
||||
)
|
||||
api.GET("/telegram/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetBotConfig)
|
||||
api.PUT("/telegram/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.UpdateBotConfig)
|
||||
api.POST("/telegram/validate-api-key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.ValidateApiKey)
|
||||
api.GET("/telegram/channels", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetChannels)
|
||||
api.POST("/telegram/channels", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.CreateChannel)
|
||||
api.PUT("/telegram/channels/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.UpdateChannel)
|
||||
api.DELETE("/telegram/channels/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.DeleteChannel)
|
||||
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
|
||||
}
|
||||
|
||||
// 静态文件服务
|
||||
|
||||
491
services/telegram_bot_service.go
Normal file
491
services/telegram_bot_service.go
Normal file
@@ -0,0 +1,491 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type TelegramBotService interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
ValidateApiKey(apiKey string) (bool, map[string]interface{}, error)
|
||||
GetBotUsername() string
|
||||
SendMessage(chatID int64, text string) error
|
||||
DeleteMessage(chatID int64, messageID int) error
|
||||
RegisterChannel(chatID int64, chatName, chatType string) error
|
||||
IsChannelRegistered(chatID int64) bool
|
||||
HandleWebhookUpdate(c interface{})
|
||||
}
|
||||
|
||||
type TelegramBotServiceImpl struct {
|
||||
bot *tgbotapi.BotAPI
|
||||
isRunning bool
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
channelRepo repo.TelegramChannelRepository
|
||||
resourceRepo repo.ResourceRepository // 添加资源仓库用于搜索
|
||||
cronScheduler *cron.Cron
|
||||
config *TelegramBotConfig
|
||||
}
|
||||
|
||||
type TelegramBotConfig struct {
|
||||
Enabled bool
|
||||
ApiKey string
|
||||
AutoReplyEnabled bool
|
||||
AutoReplyTemplate string
|
||||
AutoDeleteEnabled bool
|
||||
AutoDeleteInterval int // 分钟
|
||||
}
|
||||
|
||||
func NewTelegramBotService(
|
||||
systemConfigRepo repo.SystemConfigRepository,
|
||||
channelRepo repo.TelegramChannelRepository,
|
||||
resourceRepo repo.ResourceRepository,
|
||||
) TelegramBotService {
|
||||
return &TelegramBotServiceImpl{
|
||||
isRunning: false,
|
||||
systemConfigRepo: systemConfigRepo,
|
||||
channelRepo: channelRepo,
|
||||
resourceRepo: resourceRepo,
|
||||
cronScheduler: cron.New(),
|
||||
config: &TelegramBotConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfig 加载配置
|
||||
func (s *TelegramBotServiceImpl) loadConfig() error {
|
||||
configs, err := s.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
s.config.Enabled = false
|
||||
s.config.ApiKey = ""
|
||||
s.config.AutoReplyEnabled = true
|
||||
s.config.AutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
|
||||
s.config.AutoDeleteEnabled = false
|
||||
s.config.AutoDeleteInterval = 60
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyTelegramBotEnabled:
|
||||
s.config.Enabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramBotApiKey:
|
||||
s.config.ApiKey = config.Value
|
||||
case entity.ConfigKeyTelegramAutoReplyEnabled:
|
||||
s.config.AutoReplyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoReplyTemplate:
|
||||
if config.Value != "" {
|
||||
s.config.AutoReplyTemplate = config.Value
|
||||
}
|
||||
case entity.ConfigKeyTelegramAutoDeleteEnabled:
|
||||
s.config.AutoDeleteEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoDeleteInterval:
|
||||
if config.Value != "" {
|
||||
fmt.Sscanf(config.Value, "%d", &s.config.AutoDeleteInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Info("Telegram Bot 配置已加载: Enabled=%v", s.config.Enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动机器人服务
|
||||
func (s *TelegramBotServiceImpl) Start() error {
|
||||
if s.isRunning {
|
||||
utils.Info("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 Bot 未启用或 API Key 未配置")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建 Bot 实例
|
||||
bot, err := tgbotapi.NewBotAPI(s.config.ApiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 Telegram Bot 失败: %v", err)
|
||||
}
|
||||
|
||||
s.bot = bot
|
||||
s.isRunning = true
|
||||
|
||||
utils.Info("Telegram Bot (@%s) 已启动", s.GetBotUsername())
|
||||
|
||||
// 启动推送调度器
|
||||
s.startContentPusher()
|
||||
|
||||
// 设置 webhook(在实际部署时配置)
|
||||
if err := s.setupWebhook(); err != nil {
|
||||
utils.Error("设置 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 Bot 服务已停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateApiKey 验证 API Key
|
||||
func (s *TelegramBotServiceImpl) ValidateApiKey(apiKey string) (bool, map[string]interface{}, error) {
|
||||
if apiKey == "" {
|
||||
return false, nil, fmt.Errorf("API Key 不能为空")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// setupWebhook 设置 Webhook(可选)
|
||||
func (s *TelegramBotServiceImpl) setupWebhook() error {
|
||||
// 在生产环境中,这里会设置 webhook URL
|
||||
// 暂时使用长轮询模式,不设置 webhook
|
||||
utils.Info("使用长轮询模式处理消息")
|
||||
return nil
|
||||
}
|
||||
|
||||
// messageLoop 消息处理循环(长轮询模式)
|
||||
func (s *TelegramBotServiceImpl) messageLoop() {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := s.bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message != nil {
|
||||
s.handleMessage(update.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage 处理接收到的消息
|
||||
func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
|
||||
chatID := message.Chat.ID
|
||||
text := strings.TrimSpace(message.Text)
|
||||
|
||||
utils.Info("收到消息: ChatID=%d, Text='%s', User=%s", chatID, text, message.From.UserName)
|
||||
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 /register 命令
|
||||
if strings.ToLower(text) == "/register" {
|
||||
s.handleRegisterCommand(message)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 /start 命令
|
||||
if strings.ToLower(text) == "/start" {
|
||||
s.handleStartCommand(message)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理普通文本消息(搜索请求)
|
||||
if len(text) > 0 && !strings.HasPrefix(text, "/") {
|
||||
s.handleSearchRequest(message)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认自动回复
|
||||
if s.config.AutoReplyEnabled {
|
||||
s.sendReply(message, s.config.AutoReplyTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegisterCommand 处理注册命令
|
||||
func (s *TelegramBotServiceImpl) handleRegisterCommand(message *tgbotapi.Message) {
|
||||
chatID := message.Chat.ID
|
||||
chatTitle := message.Chat.Title
|
||||
if chatTitle == "" {
|
||||
// 如果没有标题,使用用户名作为名称
|
||||
if message.Chat.UserName != "" {
|
||||
chatTitle = message.Chat.UserName
|
||||
} else {
|
||||
chatTitle = fmt.Sprintf("Chat_%d", chatID)
|
||||
}
|
||||
}
|
||||
|
||||
chatType := "private"
|
||||
if message.Chat.IsChannel() {
|
||||
chatType = "channel"
|
||||
} else if message.Chat.IsGroup() || message.Chat.IsSuperGroup() {
|
||||
chatType = "group"
|
||||
}
|
||||
|
||||
err := s.RegisterChannel(chatID, chatTitle, chatType)
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("注册失败: %v", err)
|
||||
s.sendReply(message, errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("✅ 注册成功!\n\n频道/群组: %s\n类型: %s\n\n现在可以向此频道推送资源内容了。", chatTitle, chatType)
|
||||
s.sendReply(message, successMsg)
|
||||
}
|
||||
|
||||
// handleStartCommand 处理开始命令
|
||||
func (s *TelegramBotServiceImpl) handleStartCommand(message *tgbotapi.Message) {
|
||||
welcomeMsg := `🤖 欢迎使用网盘资源机器人!
|
||||
|
||||
我会帮您搜索网盘资源。使用方法:
|
||||
• 直接发送关键词搜索资源
|
||||
• 发送 /register 注册当前频道用于推送
|
||||
|
||||
享受使用吧!`
|
||||
|
||||
if s.config.AutoReplyEnabled && s.config.AutoReplyTemplate != "" {
|
||||
welcomeMsg += "\n\n" + s.config.AutoReplyTemplate
|
||||
}
|
||||
|
||||
s.sendReply(message, welcomeMsg)
|
||||
}
|
||||
|
||||
// handleSearchRequest 处理搜索请求
|
||||
func (s *TelegramBotServiceImpl) handleSearchRequest(message *tgbotapi.Message) {
|
||||
query := strings.TrimSpace(message.Text)
|
||||
if query == "" {
|
||||
s.sendReply(message, "请输入搜索关键词")
|
||||
return
|
||||
}
|
||||
|
||||
// 这里使用简单的资源搜索,实际项目中需要完善搜索逻辑
|
||||
// resources, err := s.resourceRepo.Search(query, nil, 0, 10)
|
||||
// 暂时模拟一个搜索结果
|
||||
results := []string{
|
||||
fmt.Sprintf("🔍 搜索关键词: %s", query),
|
||||
"暂无相关资源,请尝试其他关键词。",
|
||||
"",
|
||||
fmt.Sprintf("💡 提示:如需精确搜索,请使用更具体的关键词。"),
|
||||
}
|
||||
|
||||
resultText := strings.Join(results, "\n")
|
||||
s.sendReply(message, resultText)
|
||||
}
|
||||
|
||||
// sendReply 发送回复消息
|
||||
func (s *TelegramBotServiceImpl) sendReply(message *tgbotapi.Message, text string) {
|
||||
msg := tgbotapi.NewMessage(message.Chat.ID, text)
|
||||
msg.ParseMode = "Markdown"
|
||||
msg.ReplyToMessageID = message.MessageID
|
||||
|
||||
sentMsg, err := s.bot.Send(msg)
|
||||
if err != nil {
|
||||
utils.Error("发送消息失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果启用了自动删除,启动删除定时器
|
||||
if s.config.AutoDeleteEnabled && s.config.AutoDeleteInterval > 0 {
|
||||
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("删除消息失败: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// startContentPusher 启动内容推送器
|
||||
func (s *TelegramBotServiceImpl) startContentPusher() {
|
||||
// 每小时检查一次需要推送的频道
|
||||
s.cronScheduler.AddFunc("@every 1h", func() {
|
||||
s.pushContentToChannels()
|
||||
})
|
||||
|
||||
s.cronScheduler.Start()
|
||||
utils.Info("内容推送调度器已启动")
|
||||
}
|
||||
|
||||
// pushContentToChannels 推送内容到频道
|
||||
func (s *TelegramBotServiceImpl) pushContentToChannels() {
|
||||
// 获取需要推送的频道
|
||||
channels, err := s.channelRepo.FindDueForPush()
|
||||
if err != nil {
|
||||
utils.Error("获取推送频道失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(channels) == 0 {
|
||||
utils.Debug("没有需要推送的频道")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("开始推送内容到 %d 个频道", len(channels))
|
||||
|
||||
for _, channel := range channels {
|
||||
go s.pushToChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
// pushToChannel 推送内容到一个频道
|
||||
func (s *TelegramBotServiceImpl) pushToChannel(channel entity.TelegramChannel) {
|
||||
// 这里实现推送逻辑
|
||||
// 1. 根据频道设置过滤资源
|
||||
resources := s.findResourcesForChannel(channel)
|
||||
if len(resources) == 0 {
|
||||
utils.Debug("频道 %s 没有可推送的内容", channel.ChatName)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 构建推送消息
|
||||
message := s.buildPushMessage(channel, resources)
|
||||
|
||||
// 3. 发送消息
|
||||
err := s.SendMessage(channel.ChatID, message)
|
||||
if err != nil {
|
||||
utils.Error("推送失败到频道 %s (%d): %v", channel.ChatName, channel.ChatID, err)
|
||||
// 可以考虑将频道标记为非活跃或记录错误
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 更新最后推送时间
|
||||
err = s.channelRepo.UpdateLastPushAt(channel.ID, time.Now())
|
||||
if err != nil {
|
||||
utils.Error("更新推送时间失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Info("成功推送内容到频道: %s", channel.ChatName)
|
||||
}
|
||||
|
||||
// findResourcesForChannel 查找适合频道的资源
|
||||
func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.TelegramChannel) []interface{} {
|
||||
// 这里需要实现根据频道配置过滤资源
|
||||
// 暂时返回空数组,实际实现中需要查询资源数据库
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// buildPushMessage 构建推送消息
|
||||
func (s *TelegramBotServiceImpl) buildPushMessage(channel entity.TelegramChannel, resources []interface{}) string {
|
||||
message := fmt.Sprintf("📢 **%s**\n\n", channel.ChatName)
|
||||
|
||||
if len(resources) == 0 {
|
||||
message += "暂无新内容推送"
|
||||
} else {
|
||||
message += fmt.Sprintf("🆕 发现 %d 个新资源:\n\n", len(resources))
|
||||
// 这里需要格式化资源列表
|
||||
message += "*详细资源列表请查看网站*"
|
||||
}
|
||||
|
||||
message += fmt.Sprintf("\n\n⏰ 下次推送: %d 小时后", channel.PushFrequency)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// GetBotUsername 获取机器人用户名
|
||||
func (s *TelegramBotServiceImpl) GetBotUsername() string {
|
||||
if s.bot != nil {
|
||||
return s.bot.Self.UserName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SendMessage 发送消息
|
||||
func (s *TelegramBotServiceImpl) SendMessage(chatID int64, text string) error {
|
||||
if s.bot == nil {
|
||||
return fmt.Errorf("Bot 未初始化")
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ParseMode = "Markdown"
|
||||
_, err := s.bot.Send(msg)
|
||||
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: 24, // 默认24小时
|
||||
IsActive: true,
|
||||
RegisteredBy: "bot_command",
|
||||
RegisteredAt: time.Now(),
|
||||
ContentCategories: "",
|
||||
ContentTags: "",
|
||||
}
|
||||
|
||||
return s.channelRepo.Create(&channel)
|
||||
}
|
||||
|
||||
// IsChannelRegistered 检查频道是否已注册
|
||||
func (s *TelegramBotServiceImpl) IsChannelRegistered(chatID int64) bool {
|
||||
channel, err := s.channelRepo.FindByChatID(chatID)
|
||||
return err == nil && channel != nil
|
||||
}
|
||||
|
||||
// HandleWebhookUpdate 处理 Webhook 更新(预留接口,目前使用长轮询)
|
||||
func (s *TelegramBotServiceImpl) HandleWebhookUpdate(c interface{}) {
|
||||
// 目前使用长轮询模式,webhook 接口预留
|
||||
// 将来可以实现从 webhook 接收消息的处理逻辑
|
||||
// 如果需要实现 webhook 模式,可以在这里添加处理逻辑
|
||||
}
|
||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -30,6 +30,7 @@ declare module 'vue' {
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
|
||||
@@ -173,10 +173,234 @@
|
||||
|
||||
<n-tab-pane name="telegram" tab="Telegram机器人">
|
||||
<div class="tab-content-container">
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Telegram机器人功能正在开发中,敬请期待</p>
|
||||
<div class="space-y-8">
|
||||
<!-- 机器人基本配置 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center mr-3">
|
||||
<span class="text-sm font-bold">1</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">机器人配置</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 机器人启用开关 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">启用 Telegram 机器人</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">开启后机器人将开始工作</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="telegramBotConfig.bot_enabled"
|
||||
@update:value="handleBotConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API Key 配置 -->
|
||||
<div v-if="telegramBotConfig.bot_enabled" class="space-y-3">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">Bot API Key</label>
|
||||
<div class="flex space-x-3">
|
||||
<n-input
|
||||
v-model:value="telegramBotConfig.bot_api_key"
|
||||
placeholder="请输入 Telegram Bot API Key"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
class="flex-1"
|
||||
@input="handleBotConfigChange"
|
||||
/>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="validatingApiKey"
|
||||
@click="validateApiKey"
|
||||
>
|
||||
校验
|
||||
</n-button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
从 @BotFather 获取 API Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 校验结果 -->
|
||||
<div v-if="apiKeyValidationResult" class="p-3 rounded-md"
|
||||
:class="apiKeyValidationResult.valid ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300' : 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'">
|
||||
<div class="flex items-center">
|
||||
<i :class="apiKeyValidationResult.valid ? 'fas fa-check-circle' : 'fas fa-times-circle'"
|
||||
class="mr-2"></i>
|
||||
<span>{{ apiKeyValidationResult.message }}</span>
|
||||
</div>
|
||||
<div v-if="apiKeyValidationResult.valid && apiKeyValidationResult.botInfo" class="mt-2 text-xs">
|
||||
机器人:@{{ apiKeyValidationResult.botInfo.username }} ({{ apiKeyValidationResult.botInfo.first_name }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="savingBotConfig"
|
||||
:disabled="!hasBotConfigChanges"
|
||||
@click="saveBotConfig"
|
||||
>
|
||||
保存配置
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动回复配置 -->
|
||||
<div v-if="telegramBotConfig.bot_enabled" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-8 h-8 bg-green-600 text-white rounded-full flex items-center justify-center mr-3">
|
||||
<span class="text-sm font-bold">2</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">自动回复设置</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 自动回复开关 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">启用自动回复</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">收到消息时自动回复帮助信息</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="telegramBotConfig.auto_reply_enabled"
|
||||
@update:value="handleBotConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 回复模板 -->
|
||||
<div v-if="telegramBotConfig.auto_reply_enabled">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">回复模板</label>
|
||||
<n-input
|
||||
v-model:value="telegramBotConfig.auto_reply_template"
|
||||
type="textarea"
|
||||
placeholder="请输入自动回复内容"
|
||||
:rows="3"
|
||||
@input="handleBotConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 自动删除开关 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">自动删除回复</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">定时删除机器人发送的回复消息</p>
|
||||
</div>
|
||||
<n-switch
|
||||
v-model:value="telegramBotConfig.auto_delete_enabled"
|
||||
@update:value="handleBotConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 删除间隔 -->
|
||||
<div v-if="telegramBotConfig.auto_delete_enabled">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">删除间隔(分钟)</label>
|
||||
<n-input-number
|
||||
v-model:value="telegramBotConfig.auto_delete_interval"
|
||||
:min="1"
|
||||
:max="1440"
|
||||
@update:value="handleBotConfigChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 频道和群组管理 -->
|
||||
<div v-if="telegramBotConfig.bot_enabled" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-purple-600 text-white rounded-full flex items-center justify-center mr-3">
|
||||
<span class="text-sm font-bold">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">频道和群组管理</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">管理推送对象的频道和群组</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="showRegisterChannelDialog = true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-plus"></i>
|
||||
</template>
|
||||
注册频道/群组
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 频道列表 -->
|
||||
<div v-if="telegramChannels.length > 0" class="space-y-4">
|
||||
<div v-for="channel in telegramChannels" :key="channel.id"
|
||||
class="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i :class="channel.chat_type === 'channel' ? 'fab fa-telegram-plane' : 'fas fa-users'"
|
||||
class="text-lg text-blue-600 dark:text-blue-400"></i>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">{{ channel.chat_name }}</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ channel.chat_type === 'channel' ? '频道' : '群组' }} • ID: {{ channel.chat_id }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<n-tag :type="channel.is_active ? 'success' : 'warning'" size="small">
|
||||
{{ channel.is_active ? '活跃' : '非活跃' }}
|
||||
</n-tag>
|
||||
<n-button size="small" @click="editChannel(channel)">
|
||||
<template #icon>
|
||||
<i class="fas fa-edit"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button size="small" type="error" @click="unregisterChannel(channel)">
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推送配置 -->
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<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.content_categories || '全部' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<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.content_tags || '全部' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">推送已禁用</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="text-center py-8">
|
||||
<i class="fab fa-telegram-plane text-4xl text-gray-400 mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">暂无频道或群组</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">点击上方按钮注册推送对象</p>
|
||||
<n-button type="primary" @click="showRegisterChannelDialog = true">
|
||||
立即注册
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
@@ -191,6 +415,55 @@
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- 注册频道对话框 -->
|
||||
<n-modal
|
||||
v-model:show="showRegisterChannelDialog"
|
||||
preset="card"
|
||||
title="注册频道/群组"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
将机器人添加到频道或群组,然后发送命令获取频道信息并注册为推送对象。
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-1"></i>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">注册步骤:</h4>
|
||||
<ol class="text-sm text-blue-700 dark:text-blue-300 space-y-1 list-decimal list-inside">
|
||||
<li>将 @{{ telegramBotConfig.bot_enabled ? '机器人用户名' : '机器人' }} 添加为频道管理员或群组成员</li>
|
||||
<li>在频道/群组中发送 <code class="bg-blue-200 dark:bg-blue-800 px-1 rounded">/register</code> 命令</li>
|
||||
<li>机器人将自动识别并注册该频道/群组</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!telegramBotConfig.bot_enabled || !telegramBotConfig.bot_api_key" class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 mt-1"></i>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">配置未完成</h4>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">请先启用机器人并配置有效的 API Key。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center py-4">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="showRegisterChannelDialog = false"
|
||||
>
|
||||
我知道了
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
</AdminPageLayout>
|
||||
@@ -226,6 +499,182 @@ const {
|
||||
}
|
||||
})
|
||||
|
||||
// Telegram 相关数据和状态
|
||||
const telegramBotConfig = ref({
|
||||
bot_enabled: false,
|
||||
bot_api_key: '',
|
||||
auto_reply_enabled: true,
|
||||
auto_reply_template: '您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。',
|
||||
auto_delete_enabled: false,
|
||||
auto_delete_interval: 60,
|
||||
})
|
||||
|
||||
const telegramChannels = ref([])
|
||||
const validatingApiKey = ref(false)
|
||||
const savingBotConfig = ref(false)
|
||||
const apiKeyValidationResult = ref(null)
|
||||
const hasBotConfigChanges = ref(false)
|
||||
const showRegisterChannelDialog = ref(false)
|
||||
|
||||
// 获取 Telegram 配置
|
||||
const fetchTelegramConfig = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/telegram/bot-config', {
|
||||
method: 'GET'
|
||||
})
|
||||
if (response && response.data) {
|
||||
telegramBotConfig.value = { ...response.data }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 Telegram 配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取频道列表
|
||||
const fetchTelegramChannels = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/telegram/channels', {
|
||||
method: 'GET'
|
||||
})
|
||||
if (response && response.data) {
|
||||
telegramChannels.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取频道列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理机器人配置变更
|
||||
const handleBotConfigChange = () => {
|
||||
hasBotConfigChanges.value = true
|
||||
}
|
||||
|
||||
// 校验 API Key
|
||||
const validateApiKey = async () => {
|
||||
if (!telegramBotConfig.value.bot_api_key) {
|
||||
notification.error({
|
||||
content: '请输入 API Key',
|
||||
duration: 2000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
validatingApiKey.value = true
|
||||
try {
|
||||
const response = await $fetch('/api/telegram/validate-api-key', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
api_key: telegramBotConfig.value.bot_api_key
|
||||
}
|
||||
})
|
||||
|
||||
if (response && response.data) {
|
||||
apiKeyValidationResult.value = response.data
|
||||
if (response.data.valid) {
|
||||
notification.success({
|
||||
content: 'API Key 校验成功',
|
||||
duration: 2000
|
||||
})
|
||||
} else {
|
||||
notification.error({
|
||||
content: response.data.error,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
apiKeyValidationResult.value = {
|
||||
valid: false,
|
||||
error: error.data?.message || '校验失败'
|
||||
}
|
||||
notification.error({
|
||||
content: 'API Key 校验失败',
|
||||
duration: 2000
|
||||
})
|
||||
} finally {
|
||||
validatingApiKey.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存机器人配置
|
||||
const saveBotConfig = async () => {
|
||||
savingBotConfig.value = true
|
||||
try {
|
||||
const configRequest = {}
|
||||
if (hasBotConfigChanges.value) {
|
||||
configRequest.bot_enabled = telegramBotConfig.value.bot_enabled
|
||||
configRequest.bot_api_key = telegramBotConfig.value.bot_api_key
|
||||
configRequest.auto_reply_enabled = telegramBotConfig.value.auto_reply_enabled
|
||||
configRequest.auto_reply_template = telegramBotConfig.value.auto_reply_template
|
||||
configRequest.auto_delete_enabled = telegramBotConfig.value.auto_delete_enabled
|
||||
configRequest.auto_delete_interval = telegramBotConfig.value.auto_delete_interval
|
||||
}
|
||||
|
||||
const response = await $fetch('/api/telegram/bot-config', {
|
||||
method: 'PUT',
|
||||
body: configRequest
|
||||
})
|
||||
|
||||
if (response && response.success) {
|
||||
notification.success({
|
||||
content: '配置保存成功',
|
||||
duration: 2000
|
||||
})
|
||||
hasBotConfigChanges.value = false
|
||||
// 重新获取配置以确保同步
|
||||
await fetchTelegramConfig()
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
content: error.data?.message || '配置保存失败',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
savingBotConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑频道
|
||||
const editChannel = (channel) => {
|
||||
// TODO: 实现编辑频道功能
|
||||
console.log('编辑频道:', channel)
|
||||
}
|
||||
|
||||
// 注销频道
|
||||
const unregisterChannel = async (channel) => {
|
||||
try {
|
||||
await notification.warning({
|
||||
content: `确定取消注册 "${channel.chat_name}" 吗?`,
|
||||
duration: 10000,
|
||||
action: () => performUnregisterChannel(channel)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('注销频道操作失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const performUnregisterChannel = async (channel) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/telegram/channels/${channel.id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
if (response && response.success) {
|
||||
notification.success({
|
||||
content: '频道已取消注册',
|
||||
duration: 2000
|
||||
})
|
||||
// 重新获取频道列表
|
||||
await fetchTelegramChannels()
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
content: error.data?.message || '取消注册失败',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const notification = useNotification()
|
||||
const activeTab = ref('qq')
|
||||
|
||||
@@ -280,8 +729,10 @@ const copyToClipboard = async (text: string) => {
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
fetchApiToken()
|
||||
await fetchTelegramConfig()
|
||||
await fetchTelegramChannels()
|
||||
console.log('机器人管理页面已加载')
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user