package notice import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "strconv" "strings" "time" ) type feishuRenderer struct{} func (feishuRenderer) Capabilities() Capabilities { return Capabilities{Text: true, ColorText: true, Markdown: true, Card: true} } func (feishuRenderer) Render(config Config, message Message) (renderedRequest, error) { body := map[string]any{} switch message.Type { case MessageText: body["msg_type"] = "text" body["content"] = map[string]any{"text": message.Content} case MessageColorText: body["msg_type"] = "interactive" body["card"] = feishuCard("", CardThemeDefault, feishuColorMarkdown(message.Segments), nil, nil) case MessageMarkdown: body["msg_type"] = "interactive" body["card"] = feishuCard(message.Title, CardThemeDefault, message.Content, nil, nil) case MessageCard: body["msg_type"] = "interactive" body["card"] = feishuCard(message.Card.Title, message.Card.Theme, message.Card.Markdown, message.Card.Fields, message.Card.Actions) } secret := signatureSecret(config) if secret != "" { timestamp := strconv.FormatInt(time.Now().Unix(), 10) body["timestamp"] = timestamp body["sign"] = feishuSign(timestamp, secret) } return marshalRequest(config.Webhook, body) } func (feishuRenderer) ValidateResponse(response networkResponse) error { if err := validateHTTP(ChannelFeishu, response); err != nil { return err } code, message, err := responseStatus(response.Body, "code", "StatusCode", "msg", "StatusMessage") if err != nil { return fmt.Errorf("notice: parse feishu response: %w", err) } if code != "0" { return &PlatformError{Channel: ChannelFeishu, Code: code, Message: message} } return nil } func feishuSign(timestamp, secret string) string { key := []byte(timestamp + "\n" + secret) digest := hmac.New(sha256.New, key) return base64.StdEncoding.EncodeToString(digest.Sum(nil)) } func feishuColorMarkdown(segments []TextSegment) string { var builder strings.Builder for _, segment := range segments { content := escapeText(segment.Text) if segment.Bold { content = "**" + content + "**" } color := map[Color]string{ ColorInfo: "blue", ColorSuccess: "green", ColorWarning: "orange", ColorDanger: "red", ColorMuted: "grey", }[segment.Color] if color != "" { content = `` + content + `` } builder.WriteString(content) } return builder.String() } func feishuCard(title string, theme CardTheme, markdown string, fields []CardField, actions []CardAction) map[string]any { if theme == "" { theme = CardThemeDefault } elements := make([]any, 0, 2) content := strings.TrimSpace(markdown) if len(fields) > 0 { content = strings.TrimSpace(cardMarkdown(&CardContent{Markdown: markdown, Fields: fields})) } if content != "" { elements = append(elements, map[string]any{"tag": "markdown", "content": content}) } if len(actions) > 0 { for _, action := range actions { elements = append(elements, map[string]any{ "tag": "button", "text": map[string]any{"tag": "plain_text", "content": action.Text}, "type": "default", "behaviors": []any{map[string]any{"type": "open_url", "default_url": action.URL}}, }) } } card := map[string]any{ "schema": "2.0", "body": map[string]any{"direction": "vertical", "elements": elements}, } if strings.TrimSpace(title) == "" && theme != CardThemeDefault { title = "消息通知" } if strings.TrimSpace(title) != "" { card["header"] = map[string]any{ "title": map[string]any{"tag": "plain_text", "content": title}, "template": string(theme), } } return card } func responseStatus(body []byte, codeKey, alternateCodeKey, messageKey, alternateMessageKey string) (string, string, error) { var payload map[string]json.RawMessage if err := json.Unmarshal(body, &payload); err != nil { return "", "", err } codeRaw := payload[codeKey] if len(codeRaw) == 0 { codeRaw = payload[alternateCodeKey] } if len(codeRaw) == 0 { return "", "", fmt.Errorf("business code is missing") } var code any if err := json.Unmarshal(codeRaw, &code); err != nil { return "", "", err } messageRaw := payload[messageKey] if len(messageRaw) == 0 { messageRaw = payload[alternateMessageKey] } var message string _ = json.Unmarshal(messageRaw, &message) return fmt.Sprint(code), message, nil }