package notice import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "net/url" "strconv" "strings" "time" ) type dingTalkRenderer struct{} func (dingTalkRenderer) Capabilities() Capabilities { return Capabilities{Text: true, ColorText: true, Markdown: true, Card: true} } func (dingTalkRenderer) Render(config Config, message Message) (renderedRequest, error) { body := map[string]any{} switch message.Type { case MessageText: body = map[string]any{"msgtype": "text", "text": map[string]any{"content": message.Content}} case MessageColorText: body = dingTalkMarkdown("消息通知", dingTalkColorMarkdown(message.Segments)) case MessageMarkdown: body = dingTalkMarkdown(plainTitle(message.Title, "消息通知"), message.Content) case MessageCard: content := cardMarkdown(message.Card) if strings.TrimSpace(content) == "" { content = message.Card.Title } if len(message.Card.Actions) == 0 { body = dingTalkMarkdown(plainTitle(message.Card.Title, "消息通知"), content) break } buttons := make([]any, 0, len(message.Card.Actions)) for _, action := range message.Card.Actions { buttons = append(buttons, map[string]any{"title": action.Text, "actionURL": action.URL}) } body = map[string]any{"msgtype": "actionCard", "actionCard": map[string]any{ "title": plainTitle(message.Card.Title, "消息通知"), "text": content, "btnOrientation": "0", "btns": buttons, }} } webhook := config.Webhook secret := signatureSecret(config) if secret != "" { timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) sign := dingTalkSign(timestamp, secret) parsed, err := url.Parse(webhook) if err != nil { return renderedRequest{}, err } query := parsed.Query() query.Set("timestamp", timestamp) query.Set("sign", sign) parsed.RawQuery = query.Encode() webhook = parsed.String() } return marshalRequest(webhook, body) } func (dingTalkRenderer) ValidateResponse(response networkResponse) error { if err := validateHTTP(ChannelDingTalk, response); err != nil { return err } code, message, err := responseStatus(response.Body, "errcode", "", "errmsg", "") if err != nil { return fmt.Errorf("notice: parse dingtalk response: %w", err) } if code != "0" { return &PlatformError{Channel: ChannelDingTalk, Code: code, Message: message} } return nil } func dingTalkSign(timestamp, secret string) string { digest := hmac.New(sha256.New, []byte(secret)) _, _ = digest.Write([]byte(timestamp + "\n" + secret)) return base64.StdEncoding.EncodeToString(digest.Sum(nil)) } func dingTalkMarkdown(title, content string) map[string]any { return map[string]any{"msgtype": "markdown", "markdown": map[string]any{"title": title, "text": content}} } func dingTalkColorMarkdown(segments []TextSegment) string { var builder strings.Builder for _, segment := range segments { prefix := map[Color]string{ ColorInfo: "【提示】", ColorSuccess: "【成功】", ColorWarning: "【警告】", ColorDanger: "【异常】", }[segment.Color] content := escapeText(segment.Text) if prefix != "" { content = prefix + content } if segment.Bold || segment.Color == ColorWarning || segment.Color == ColorDanger { content = "**" + content + "**" } builder.WriteString(content) } return builder.String() }