Files

116 lines
3.1 KiB
Go

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)
}
}