Files
2026-07-13 12:31:02 +08:00

214 lines
5.9 KiB
Go

package notification
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// DiscordService handles Discord notifications
type DiscordService struct{}
// NewDiscordService creates a new Discord notification service
func NewDiscordService() *DiscordService {
return &DiscordService{}
}
// DiscordPayload represents the payload for Discord webhook
type DiscordPayload struct {
Content string `json:"content,omitempty"`
Embeds []DiscordEmbed `json:"embeds,omitempty"`
}
// DiscordEmbed represents a Discord embed
type DiscordEmbed struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Color int `json:"color,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Fields []DiscordEmbedField `json:"fields,omitempty"`
Footer *DiscordEmbedFooter `json:"footer,omitempty"`
}
// DiscordEmbedField represents a field in Discord embed
type DiscordEmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline,omitempty"`
}
// DiscordEmbedFooter represents footer in Discord embed
type DiscordEmbedFooter struct {
Text string `json:"text"`
}
// SendNotification sends a notification via Discord webhook
func (ds *DiscordService) SendNotification(config *AlertConfiguration, message string) error {
if config.DiscordWebhookURL == "" {
return fmt.Errorf("discord webhook URL is required")
}
// Create rich embed for better formatting
embed := ds.createDiscordEmbed(message)
payload := DiscordPayload{
Embeds: []DiscordEmbed{embed},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal Discord payload: %v", err)
}
resp, err := http.Post(config.DiscordWebhookURL, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("discord webhook request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("discord webhook error, status: %d", resp.StatusCode)
}
return nil
}
// createDiscordEmbed creates a formatted Discord embed from the message
func (ds *DiscordService) createDiscordEmbed(message string) DiscordEmbed {
// Parse the message to extract structured information
lines := strings.Split(message, "\n")
if len(lines) == 0 {
return DiscordEmbed{
Description: message,
Color: ds.getDefaultColor(),
Timestamp: time.Now().Format(time.RFC3339),
}
}
// Extract title from first line and determine status
title := strings.TrimSpace(lines[0])
color := ds.determineColorFromMessage(title)
// Create fields from the remaining lines
var fields []DiscordEmbedField
for i, line := range lines {
if i == 0 {
continue // Skip title line
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Parse field from "• Key: Value" format
if strings.HasPrefix(line, "•") || strings.HasPrefix(line, "-") {
line = strings.TrimPrefix(line, "•")
line = strings.TrimPrefix(line, "-")
line = strings.TrimSpace(line)
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
fieldName := strings.TrimSpace(parts[0])
fieldValue := strings.TrimSpace(parts[1])
if fieldName != "" && fieldValue != "" {
fields = append(fields, DiscordEmbedField{
Name: fieldName,
Value: fieldValue,
Inline: true,
})
}
} else {
// If not in key:value format, add as description field
fields = append(fields, DiscordEmbedField{
Name: "Details",
Value: line,
Inline: false,
})
}
}
}
// If no structured fields found, use the entire message as description
if len(fields) == 0 {
// Remove the first line from description since it's used as title
description := ""
if len(lines) > 1 {
description = strings.Join(lines[1:], "\n")
}
return DiscordEmbed{
Title: title,
Description: description,
Color: color,
Timestamp: time.Now().Format(time.RFC3339),
Footer: &DiscordEmbedFooter{
Text: "CheckCle System Alert",
},
}
}
return DiscordEmbed{
Title: title,
Color: color,
Timestamp: time.Now().Format(time.RFC3339),
Fields: fields,
Footer: &DiscordEmbedFooter{
Text: "CheckCle System Alert",
},
}
}
// determineColorFromMessage determines Discord embed color based on message content
func (ds *DiscordService) determineColorFromMessage(message string) int {
messageLower := strings.ToLower(message)
// Red for critical/error states
if strings.Contains(messageLower, "expired") ||
strings.Contains(messageLower, "down") ||
strings.Contains(messageLower, "failed") ||
strings.Contains(messageLower, "error") ||
strings.Contains(messageLower, "critical") ||
strings.Contains(messageLower, "🚨") ||
strings.Contains(messageLower, "🔴") {
return 15158332 // Red (#E74C3C)
}
// Orange for warnings
if strings.Contains(messageLower, "expiring_soon") ||
strings.Contains(messageLower, "expiring soon") ||
strings.Contains(messageLower, "warning") ||
strings.Contains(messageLower, "maintenance") ||
strings.Contains(messageLower, "paused") ||
strings.Contains(messageLower, "⚠️") ||
strings.Contains(messageLower, "🟡") ||
strings.Contains(messageLower, "🟠") {
return 15105570 // Orange (#E67E22)
}
// Green for success/up states
if strings.Contains(messageLower, "up") ||
strings.Contains(messageLower, "resolved") ||
strings.Contains(messageLower, "success") ||
strings.Contains(messageLower, "restored") ||
strings.Contains(messageLower, "valid") ||
strings.Contains(messageLower, "🟢") ||
strings.Contains(messageLower, "✅") {
return 3066993 // Green (#2ECC71)
}
// Blue for info/default
return 3447003 // Blue (#3498DB)
}
// getDefaultColor returns the default Discord embed color (blue)
func (ds *DiscordService) getDefaultColor() int {
return 3447003 // Blue (#3498DB)
}