package notice import ( "context" "fmt" "net/url" "strings" "time" ) const defaultTimeout = 5 * time.Second type channelSender struct { channel Channel `json:"-" dc:"当前消息渠道"` renderer renderer `json:"-" dc:"渠道消息渲染器"` client networkClient `json:"-" dc:"HTTP 网络请求客户端"` config Config `json:"-" dc:"当前渠道配置"` } func New(channel Channel, config Config) (Sender, error) { return newSender(channel, config, defaultNetworkClient{}) } func newSender(channel Channel, config Config, client networkClient) (Sender, error) { if err := validateConfig(channel, config); err != nil { return nil, err } if config.Timeout <= 0 { config.Timeout = defaultTimeout } selected, err := rendererFor(channel) if err != nil { return nil, err } return &channelSender{channel: channel, renderer: selected, client: client, config: config}, nil } func (s *channelSender) Send(ctx context.Context, message Message) error { if err := validateMessage(message); err != nil { return err } request, err := s.renderer.Render(s.config, message) if err != nil { return err } response, err := s.client.Post(networkRequest{ Context: ctx, URL: request.URL, Body: request.Body, Timeout: s.config.Timeout, }) if err != nil { return fmt.Errorf("notice: send %s webhook: %w", s.channel, err) } return s.renderer.ValidateResponse(response) } func (s *channelSender) SendAsync(ctx context.Context, message Message) <-chan error { result := make(chan error, 1) go func() { defer close(result) result <- s.Send(ctx, message) }() return result } func validateConfig(channel Channel, config Config) error { if !validChannel(channel) { return fmt.Errorf("%w: %q", ErrUnsupportedChannel, channel) } parsed, err := url.ParseRequestURI(strings.TrimSpace(config.Webhook)) if err != nil || parsed.Scheme != "https" || parsed.Host == "" { return fmt.Errorf("%w: webhook must be a valid HTTPS URL", ErrInvalidConfig) } if config.Security != nil && config.Security.SignEnabled { if channel != ChannelFeishu && channel != ChannelDingTalk { return fmt.Errorf("%w: channel %s does not support independent signature verification", ErrInvalidConfig, channel) } if strings.TrimSpace(config.Security.SignSecret) == "" { return fmt.Errorf("%w: signature secret is empty", ErrInvalidConfig) } } return nil } func signatureSecret(config Config) string { if config.Security == nil || !config.Security.SignEnabled { return "" } return strings.TrimSpace(config.Security.SignSecret) } func validChannel(channel Channel) bool { switch channel { case ChannelFeishu, ChannelDingTalk, ChannelWeCom, ChannelWebhook: return true default: return false } } func rendererFor(channel Channel) (renderer, error) { switch channel { case ChannelFeishu: return feishuRenderer{}, nil case ChannelDingTalk: return dingTalkRenderer{}, nil case ChannelWeCom: return weComRenderer{}, nil case ChannelWebhook: return webhookRenderer{}, nil default: return nil, fmt.Errorf("%w: %q", ErrUnsupportedChannel, channel) } }