mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
187 lines
5.4 KiB
Go
187 lines
5.4 KiB
Go
// Package service — 通知服务事件分发引擎。
|
|
//
|
|
// NotifyService 管理所有通知渠道,根据事件类型将通知分发给
|
|
// 订阅了该事件的渠道。支持 4 种内置事件类型和 5 种通知渠道。
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// 通知事件类型常量。
|
|
const (
|
|
EventSubscriptionHit = "subscription_hit"
|
|
EventDownloadComplete = "download_complete"
|
|
EventScrapeFailed = "scrape_failed"
|
|
EventSystemAlert = "system_alert"
|
|
EventLibraryIngest = "library_ingest"
|
|
)
|
|
|
|
// NotifyEvent 是通知事件的数据结构。
|
|
type NotifyEvent struct {
|
|
Type string `json:"type"`
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
// NotifyProvider 定义通知渠道的发送接口。
|
|
type NotifyProvider interface {
|
|
Send(ctx context.Context, cfg map[string]string, event NotifyEvent) error
|
|
ValidateConfig(cfg map[string]string) error
|
|
}
|
|
|
|
// NotifyService 是事件驱动的通知分发引擎。
|
|
type NotifyService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
crypto *CryptoService
|
|
|
|
mu sync.RWMutex
|
|
providers map[string]NotifyProvider // type -> provider
|
|
}
|
|
|
|
// NewNotifyService 创建通知服务。
|
|
func NewNotifyService(log *zap.Logger, repo *repository.Container, crypto *CryptoService) *NotifyService {
|
|
ns := &NotifyService{
|
|
log: log,
|
|
repo: repo,
|
|
crypto: crypto,
|
|
providers: make(map[string]NotifyProvider),
|
|
}
|
|
// 注册内置 Provider
|
|
ns.registerProviders()
|
|
return ns
|
|
}
|
|
|
|
// registerProviders 注册所有内置通知 Provider。
|
|
func (s *NotifyService) registerProviders() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.providers["telegram"] = &TelegramProvider{}
|
|
s.providers["wechat"] = &WechatProvider{}
|
|
s.providers["bark"] = &BarkProvider{}
|
|
s.providers["webhook"] = &WebhookProvider{}
|
|
s.providers["email"] = &EmailProvider{}
|
|
}
|
|
|
|
// Dispatch 将事件分发给所有订阅了该事件类型的已启用渠道。
|
|
func (s *NotifyService) Dispatch(ctx context.Context, event NotifyEvent) {
|
|
channels, err := s.repo.NotifyChannel.ListByEvent(ctx, event.Type)
|
|
if err != nil {
|
|
s.log.Error("failed to list channels for event",
|
|
zap.String("event", event.Type),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
for _, ch := range channels {
|
|
go func(channel model.NotifyChannel) {
|
|
if sendErr := s.sendToChannel(ctx, channel, event); sendErr != nil {
|
|
s.log.Error("failed to send notification",
|
|
zap.String("channel", channel.Name),
|
|
zap.String("type", channel.Type),
|
|
zap.String("event", event.Type),
|
|
zap.Error(sendErr),
|
|
)
|
|
}
|
|
}(ch)
|
|
}
|
|
}
|
|
|
|
// SendTest 向指定渠道发送测试通知。
|
|
func (s *NotifyService) SendTest(ctx context.Context, channelID string) error {
|
|
ch, err := s.repo.NotifyChannel.FindByID(ctx, channelID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ch == nil {
|
|
return ErrNotifyChannelNotFound
|
|
}
|
|
|
|
testEvent := NotifyEvent{
|
|
Type: "test",
|
|
Title: "MediaStationGo 测试通知",
|
|
Message: "这是一条测试通知,如果您看到此消息,说明通知渠道配置正确。",
|
|
}
|
|
|
|
return s.sendToChannel(ctx, *ch, testEvent)
|
|
}
|
|
|
|
// ValidateChannelConfig 验证渠道配置是否合法。
|
|
func (s *NotifyService) ValidateChannelConfig(channelType string, config map[string]string) error {
|
|
s.mu.RLock()
|
|
provider, ok := s.providers[channelType]
|
|
s.mu.RUnlock()
|
|
if !ok {
|
|
return ErrUnknownNotifyType
|
|
}
|
|
return provider.ValidateConfig(config)
|
|
}
|
|
|
|
// GetProviderTypes 返回支持的通知渠道类型列表。
|
|
func (s *NotifyService) GetProviderTypes() []NotifyProviderInfo {
|
|
return []NotifyProviderInfo{
|
|
{Type: "telegram", Name: "Telegram", Description: "通过 Telegram Bot 发送消息"},
|
|
{Type: "wechat", Name: "Server酱", Description: "通过 Server酱 推送到微信"},
|
|
{Type: "bark", Name: "Bark", Description: "通过 Bark 推送到 iOS"},
|
|
{Type: "webhook", Name: "Webhook", Description: "通过自定义 HTTP Webhook 发送"},
|
|
{Type: "email", Name: "Email", Description: "通过 SMTP 发送邮件"},
|
|
}
|
|
}
|
|
|
|
// NotifyProviderInfo 描述通知渠道类型信息。
|
|
type NotifyProviderInfo struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// sendToChannel 解密渠道配置并通过对应的 Provider 发送通知。
|
|
func (s *NotifyService) sendToChannel(ctx context.Context, channel model.NotifyChannel, event NotifyEvent) error {
|
|
// 解密配置
|
|
configStr := channel.Config
|
|
if s.crypto != nil && configStr != "" {
|
|
configStr = s.crypto.Decrypt(configStr)
|
|
}
|
|
|
|
var cfg map[string]string
|
|
if err := json.Unmarshal([]byte(configStr), &cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.RLock()
|
|
provider, ok := s.providers[channel.Type]
|
|
s.mu.RUnlock()
|
|
if !ok {
|
|
return ErrUnknownNotifyType
|
|
}
|
|
|
|
return provider.Send(ctx, cfg, event)
|
|
}
|
|
|
|
// 通知服务错误定义。
|
|
var (
|
|
ErrNotifyChannelNotFound = &NotifyError{Code: "CHANNEL_NOT_FOUND", Message: "notification channel not found"}
|
|
ErrUnknownNotifyType = &NotifyError{Code: "UNKNOWN_TYPE", Message: "unknown notification type"}
|
|
)
|
|
|
|
// NotifyError 是通知服务专用错误类型。
|
|
type NotifyError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Error 实现 error 接口。
|
|
func (e *NotifyError) Error() string {
|
|
return e.Message
|
|
}
|