package notification import ( "bytes" "encoding/json" "fmt" "net/http" "strings" "text/template" "time" ) // WebhookService handles webhook notifications type WebhookService struct{} // NewWebhookService creates a new webhook notification service func NewWebhookService() *WebhookService { return &WebhookService{} } // WebhookPayload represents the payload for webhook type WebhookPayload struct { Message string `json:"message"` NotifyName string `json:"notify_name"` Timestamp string `json:"timestamp"` ServiceName string `json:"service_name,omitempty"` Status string `json:"status,omitempty"` Host string `json:"host,omitempty"` URL string `json:"url,omitempty"` Port int `json:"port,omitempty"` ServiceType string `json:"service_type,omitempty"` ResponseTime int64 `json:"response_time,omitempty"` // Server monitoring fields CPUUsage string `json:"cpu_usage,omitempty"` RAMUsage string `json:"ram_usage,omitempty"` DiskUsage string `json:"disk_usage,omitempty"` NetworkUsage string `json:"network_usage,omitempty"` CPUTemp string `json:"cpu_temp,omitempty"` DiskIO string `json:"disk_io,omitempty"` Threshold string `json:"threshold,omitempty"` // SSL fields CertificateName string `json:"certificate_name,omitempty"` ExpiryDate string `json:"expiry_date,omitempty"` DaysLeft string `json:"days_left,omitempty"` IssuerCN string `json:"issuer_cn,omitempty"` } // DiscordWebhookPayload represents Discord-specific webhook payload type DiscordWebhookPayload struct { Content string `json:"content"` } // SendNotification sends a notification via webhook func (ws *WebhookService) SendNotification(config *AlertConfiguration, message string) error { // fmt.Printf("📡 [WEBHOOK] Attempting to send notification...\n") // fmt.Printf("📡 [WEBHOOK] Config - URL: %s, Notify Name: %s\n", config.WebhookURL, config.NotifyName) // fmt.Printf("📡 [WEBHOOK] Message: %s\n", message) // Use the webhook_url field from alert_configurations if config.WebhookURL == "" { return fmt.Errorf("webhook URL is required") } // Create payload using webhook_payload_template if available var jsonData []byte var err error if config.WebhookPayloadTemplate != "" && strings.TrimSpace(config.WebhookPayloadTemplate) != "" { // Use custom payload template jsonData, err = ws.generateCustomPayload(config, message) if err != nil { // fmt.Printf("❌ [WEBHOOK] Custom payload generation error: %v\n", err) return err } } else { // Detect Discord webhook and use appropriate payload if strings.Contains(strings.ToLower(config.WebhookURL), "discord") { jsonData, err = ws.generateDiscordPayload(message) } else { // Use default payload structure for other webhooks jsonData, err = ws.generateDefaultPayload(config, message) } if err != nil { // fmt.Printf("❌ [WEBHOOK] JSON marshal error: %v\n", err) return err } } // fmt.Printf("📡 [WEBHOOK] Payload: %s\n", string(jsonData)) // fmt.Printf("📡 [WEBHOOK] Sending POST request...\n") resp, err := http.Post(config.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { // fmt.Printf("❌ [WEBHOOK] HTTP error: %v\n", err) return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { // fmt.Printf("❌ [WEBHOOK] API error, status: %d\n", resp.StatusCode) return fmt.Errorf("webhook error, status: %d", resp.StatusCode) } // fmt.Printf("✅ [WEBHOOK] Message sent successfully!\n") return nil } // generateDiscordPayload generates a Discord-compatible webhook payload func (ws *WebhookService) generateDiscordPayload(message string) ([]byte, error) { payload := DiscordWebhookPayload{ Content: message, } return json.Marshal(payload) } // generateDefaultPayload generates the default webhook payload func (ws *WebhookService) generateDefaultPayload(config *AlertConfiguration, message string) ([]byte, error) { payload := WebhookPayload{ Message: message, NotifyName: config.NotifyName, Timestamp: time.Now().Format(time.RFC3339), } return json.Marshal(payload) } // generateCustomPayload generates payload using the custom template func (ws *WebhookService) generateCustomPayload(config *AlertConfiguration, message string) ([]byte, error) { // Parse the template tmpl, err := template.New("webhook").Parse(config.WebhookPayloadTemplate) if err != nil { return nil, fmt.Errorf("failed to parse webhook payload template: %v", err) } // Create template data templateData := map[string]interface{}{ "message": message, "notify_name": config.NotifyName, "timestamp": time.Now().Format(time.RFC3339), } // Execute template var buf bytes.Buffer err = tmpl.Execute(&buf, templateData) if err != nil { return nil, fmt.Errorf("failed to execute webhook payload template: %v", err) } // Validate that the result is valid JSON var jsonCheck interface{} if err := json.Unmarshal(buf.Bytes(), &jsonCheck); err != nil { return nil, fmt.Errorf("webhook payload template did not generate valid JSON: %v", err) } return buf.Bytes(), nil } // SendServerNotification sends a server-specific notification via webhook func (ws *WebhookService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error { message := ws.generateServerMessage(payload, template, resourceType) return ws.SendNotification(config, message) } // SendServiceNotification sends a service-specific notification via webhook func (ws *WebhookService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error { message := ws.generateServiceMessage(payload, template) return ws.SendNotification(config, message) } // generateServerMessage creates a message for server notifications using server template func (ws *WebhookService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string { var templateMessage string // Select appropriate template message based on status and resource type switch strings.ToLower(payload.Status) { case "down": templateMessage = template.DownMessage case "up": templateMessage = template.UpMessage case "warning": templateMessage = template.WarningMessage case "paused": templateMessage = template.PausedMessage default: // Handle resource-specific messages switch resourceType { case "cpu": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreCPUMessage } else { templateMessage = template.CPUMessage } case "ram", "memory": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreRAMMessage } else { templateMessage = template.RAMMessage } case "disk": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreDiskMessage } else { templateMessage = template.DiskMessage } case "network": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreNetworkMessage } else { templateMessage = template.NetworkMessage } case "cpu_temp", "cpu_temperature": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreCPUTempMessage } else { templateMessage = template.CPUTempMessage } case "disk_io": if strings.Contains(strings.ToLower(payload.Status), "restore") { templateMessage = template.RestoreDiskIOMessage } else { templateMessage = template.DiskIOMessage } default: templateMessage = template.WarningMessage } } // If no template message found, use a default if templateMessage == "" { templateMessage = ws.generateDefaultServerMessage(payload, resourceType) } return ws.replacePlaceholders(templateMessage, payload) } // generateServiceMessage creates a message for service notifications using service template func (ws *WebhookService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string { var templateMessage string // Select appropriate template message based on status switch strings.ToLower(payload.Status) { case "up": templateMessage = template.UpMessage case "down": templateMessage = template.DownMessage case "maintenance": templateMessage = template.MaintenanceMessage case "incident": templateMessage = template.IncidentMessage case "resolved": templateMessage = template.ResolvedMessage case "warning": templateMessage = template.WarningMessage default: templateMessage = template.WarningMessage } // If no template message found, use a default if templateMessage == "" { templateMessage = ws.generateDefaultUptimeMessage(payload) } return ws.replacePlaceholders(templateMessage, payload) } // replacePlaceholders replaces all placeholders in the message with actual values func (ws *WebhookService) replacePlaceholders(message string, payload *NotificationPayload) string { // Replace basic placeholders message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName) message = strings.ReplaceAll(message, "${server_name}", payload.ServiceName) // server_name maps to service_name message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status)) message = strings.ReplaceAll(message, "${host}", ws.safeString(payload.Host)) message = strings.ReplaceAll(message, "${hostname}", ws.safeString(payload.Hostname)) // Replace URL with fallback to host url := ws.safeString(payload.URL) if url == "N/A" && payload.Host != "" { url = payload.Host } message = strings.ReplaceAll(message, "${url}", url) // Replace domain message = strings.ReplaceAll(message, "${domain}", ws.safeString(payload.Domain)) // Replace service type if payload.ServiceType != "" { message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType)) } else { message = strings.ReplaceAll(message, "${service_type}", "N/A") } // Replace region and agent info message = strings.ReplaceAll(message, "${region_name}", ws.safeString(payload.RegionName)) message = strings.ReplaceAll(message, "${agent_id}", ws.safeString(payload.AgentID)) // Handle numeric fields safely if payload.Port > 0 { message = strings.ReplaceAll(message, "${port}", fmt.Sprintf("%d", payload.Port)) } else { message = strings.ReplaceAll(message, "${port}", "N/A") } if payload.ResponseTime > 0 { message = strings.ReplaceAll(message, "${response_time}", fmt.Sprintf("%dms", payload.ResponseTime)) } else { message = strings.ReplaceAll(message, "${response_time}", "N/A") } if payload.Uptime > 0 { message = strings.ReplaceAll(message, "${uptime}", fmt.Sprintf("%d%%", payload.Uptime)) } else { message = strings.ReplaceAll(message, "${uptime}", "N/A") } // Replace server monitoring fields message = strings.ReplaceAll(message, "${cpu_usage}", ws.safeString(payload.CPUUsage)) message = strings.ReplaceAll(message, "${ram_usage}", ws.safeString(payload.RAMUsage)) message = strings.ReplaceAll(message, "${disk_usage}", ws.safeString(payload.DiskUsage)) message = strings.ReplaceAll(message, "${network_usage}", ws.safeString(payload.NetworkUsage)) message = strings.ReplaceAll(message, "${cpu_temp}", ws.safeString(payload.CPUTemp)) message = strings.ReplaceAll(message, "${disk_io}", ws.safeString(payload.DiskIO)) message = strings.ReplaceAll(message, "${threshold}", ws.safeString(payload.Threshold)) // Replace SSL certificate fields message = strings.ReplaceAll(message, "${certificate_name}", ws.safeString(payload.CertificateName)) message = strings.ReplaceAll(message, "${expiry_date}", ws.safeString(payload.ExpiryDate)) message = strings.ReplaceAll(message, "${days_left}", ws.safeString(payload.DaysLeft)) message = strings.ReplaceAll(message, "${issuer_cn}", ws.safeString(payload.IssuerCN)) message = strings.ReplaceAll(message, "${issuer}", ws.safeString(payload.IssuerCN)) // alias // Replace error message - important for uptime services message = strings.ReplaceAll(message, "${error_message}", ws.safeString(payload.ErrorMessage)) message = strings.ReplaceAll(message, "${error}", ws.safeString(payload.ErrorMessage)) message = strings.ReplaceAll(message, "${message}", ws.safeString(payload.Message)) // Replace time placeholders message = strings.ReplaceAll(message, "${time}", payload.Timestamp.Format("2006-01-02 15:04:05")) message = strings.ReplaceAll(message, "${timestamp}", payload.Timestamp.Format("2006-01-02 15:04:05")) return message } // safeString returns the string value or "N/A" if empty func (ws *WebhookService) safeString(value string) string { if value == "" { return "N/A" } return value } // generateDefaultUptimeMessage creates a default uptime message with proper formatting func (ws *WebhookService) generateDefaultUptimeMessage(payload *NotificationPayload) string { // Status emoji mapping statusEmoji := "🔵" switch strings.ToLower(payload.Status) { case "up": statusEmoji = "🟢" case "down": statusEmoji = "🔴" case "warning": statusEmoji = "🟡" case "maintenance", "paused": statusEmoji = "🟠" } message := fmt.Sprintf("%s Service %s is %s.", statusEmoji, payload.ServiceName, strings.ToUpper(payload.Status)) // Build formatted details details := []string{} // Add URL or host if payload.URL != "" { details = append(details, fmt.Sprintf(" - Host URL: %s", payload.URL)) } else if payload.Host != "" { details = append(details, fmt.Sprintf(" - Host: %s", payload.Host)) } // Add service type if payload.ServiceType != "" { details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType))) } // Add port if available if payload.Port > 0 { details = append(details, fmt.Sprintf(" - Port: %d", payload.Port)) } // Add domain if available if payload.Domain != "" { details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain)) } // Add response time if payload.ResponseTime > 0 { details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime)) } else { details = append(details, " - Response time: N/A") } // Add region info if payload.RegionName != "" { details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName)) } // Add agent info if payload.AgentID != "" { details = append(details, fmt.Sprintf(" - Agent: %s", payload.AgentID)) } // Add uptime if available if payload.Uptime > 0 { details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime)) } // Add timestamp details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05"))) // Combine message with details if len(details) > 0 { message += "\n" + strings.Join(details, "\n") } return message } // generateDefaultServerMessage creates a default server message func (ws *WebhookService) generateDefaultServerMessage(payload *NotificationPayload, resourceType string) string { statusEmoji := "🔵" switch strings.ToLower(payload.Status) { case "up": statusEmoji = "🟢" case "down": statusEmoji = "🔴" case "warning": statusEmoji = "🟡" } return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status)) }