116 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|