From 18bee3c50b8aa4ba2b72cdd74543e1b1c8459373 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 2 Sep 2025 11:54:00 +0800 Subject: [PATCH] add feishu notification --- .../service-operation/notification/feishu.go | 339 ++++++++++++++++++ .../service-operation/notification/manager.go | 6 +- .../service-operation/notification/types.go | 136 +++---- 3 files changed, 410 insertions(+), 71 deletions(-) create mode 100644 server/service-operation/notification/feishu.go diff --git a/server/service-operation/notification/feishu.go b/server/service-operation/notification/feishu.go new file mode 100644 index 0000000..492ea9b --- /dev/null +++ b/server/service-operation/notification/feishu.go @@ -0,0 +1,339 @@ +package notification + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// FeishuService handles Feishu notifications +type FeishuService struct{} + +// NewFeishuService creates a new feishu webhook notification service +func NewFeishuService() *FeishuService { + return &FeishuService{} +} + +// FeishuPayload represents the payload for Feishu webhook +type FeishuPayload struct { + MsgType string `json:"msg_type"` + Content struct { + Text string `json:"text"` + } `json:"content"` +} + +// SendNotification sends a notification via feishu webhook webhook +func (gcs *FeishuService) SendNotification(config *AlertConfiguration, message string) error { + // fmt.Printf("💬 [FEISHU_WEBHOOK] Attempting to send notification...\n") + // fmt.Printf("💬 [FEISHU_WEBHOOK] Config - Webhook URL present: %v\n", config.FeishuWebhookURL != "") + // fmt.Printf("💬 [FEISHU_WEBHOOK] Message: %s\n", message) + + if config.FeishuWebhookURL == "" { + return fmt.Errorf("feishu webhook webhook URL is required") + } + + payload := FeishuPayload{ + MsgType: "text", + } + payload.Content.Text = message + + jsonData, err := json.Marshal(payload) + if err != nil { + // fmt.Printf("❌ [FEISHU_WEBHOOK] JSON marshal error: %v\n", err) + return err + } + + // fmt.Printf("💬 [FEISHU_WEBHOOK] Sending POST request to webhook...\n") + resp, err := http.Post(config.FeishuWebhookURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + // fmt.Printf("❌ [FEISHU_WEBHOOK] HTTP error: %v\n", err) + return err + } + defer resp.Body.Close() + + // fmt.Printf("💬 [FEISHU_WEBHOOK] Response status: %d\n", resp.StatusCode) + + if resp.StatusCode != http.StatusOK { + // fmt.Printf("❌ [FEISHU_WEBHOOK] Webhook error, status: %d\n", resp.StatusCode) + return fmt.Errorf("feishu webhook webhook error, status: %d", resp.StatusCode) + } + + // fmt.Printf("✅ [FEISHU_WEBHOOK] Message sent successfully!\n") + return nil +} + +// SendServerNotification sends a server-specific notification via feishu webhook +func (gcs *FeishuService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error { + message := gcs.generateServerMessage(payload, template, resourceType) + return gcs.SendNotification(config, message) +} + +// SendServiceNotification sends a service-specific notification via feishu webhook +func (gcs *FeishuService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error { + message := gcs.generateServiceMessage(payload, template) + return gcs.SendNotification(config, message) +} + +// generateServerMessage creates a message for server notifications using server template +func (gcs *FeishuService) 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 = gcs.generateDefaultServerMessage(payload, resourceType) + } + + return gcs.replacePlaceholders(templateMessage, payload) +} + +// generateServiceMessage creates a message for service notifications using service template +func (gcs *FeishuService) 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 = gcs.generateDefaultUptimeMessage(payload) + } + + return gcs.replacePlaceholders(templateMessage, payload) +} + +// replacePlaceholders replaces all placeholders in the message with actual values +func (gcs *FeishuService) replacePlaceholders(message string, payload *NotificationPayload) string { + // Replace basic placeholders + message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName) + message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status)) + message = strings.ReplaceAll(message, "${host}", gcs.safeString(payload.Host)) + message = strings.ReplaceAll(message, "${hostname}", gcs.safeString(payload.Hostname)) + + // Replace URL with fallback to host + url := gcs.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}", gcs.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}", gcs.safeString(payload.RegionName)) + message = strings.ReplaceAll(message, "${agent_id}", gcs.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}", gcs.safeString(payload.CPUUsage)) + message = strings.ReplaceAll(message, "${ram_usage}", gcs.safeString(payload.RAMUsage)) + message = strings.ReplaceAll(message, "${disk_usage}", gcs.safeString(payload.DiskUsage)) + message = strings.ReplaceAll(message, "${network_usage}", gcs.safeString(payload.NetworkUsage)) + message = strings.ReplaceAll(message, "${cpu_temp}", gcs.safeString(payload.CPUTemp)) + message = strings.ReplaceAll(message, "${disk_io}", gcs.safeString(payload.DiskIO)) + message = strings.ReplaceAll(message, "${threshold}", gcs.safeString(payload.Threshold)) + + // Replace error message - important for uptime services + message = strings.ReplaceAll(message, "${error_message}", gcs.safeString(payload.ErrorMessage)) + message = strings.ReplaceAll(message, "${error}", gcs.safeString(payload.ErrorMessage)) + + // 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 (gcs *FeishuService) safeString(value string) string { + if value == "" { + return "N/A" + } + return value +} + +// generateDefaultUptimeMessage creates a default uptime message with proper formatting +func (gcs *FeishuService) generateDefaultUptimeMessage(payload *NotificationPayload) string { + // Status emoji mapping for feishu webhook + 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 (gcs *FeishuService) 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)) +} diff --git a/server/service-operation/notification/manager.go b/server/service-operation/notification/manager.go index 39d5d69..3be181d 100644 --- a/server/service-operation/notification/manager.go +++ b/server/service-operation/notification/manager.go @@ -1,4 +1,3 @@ - package notification import ( @@ -20,7 +19,7 @@ type NotificationManager struct { func NewNotificationManager(pbClient *pocketbase.PocketBaseClient) *NotificationManager { // Initialize notification services services := make(map[string]NotificationService) - + // log.Printf("🔧 Initializing notification services...") services["telegram"] = NewTelegramService() services["signal"] = NewSignalService() @@ -31,6 +30,7 @@ func NewNotificationManager(pbClient *pocketbase.PocketBaseClient) *Notification services["webhook"] = NewWebhookService() services["ntfy"] = NewNtfyService() services["pushover"] = NewPushoverService() + services["feishu"] = NewFeishuService() // log.Printf("✅ Notification services initialized: %v", getKeys(services)) @@ -66,4 +66,4 @@ func (nm *NotificationManager) SendUptimeServiceNotification(payload *Notificati // SendSSLNotification sends notification for SSL certificates using SSL templates func (nm *NotificationManager) SendSSLNotification(payload *NotificationPayload, notificationID, templateID string) error { return nm.sslManager.SendSSLNotification(payload, notificationID, templateID) -} \ No newline at end of file +} diff --git a/server/service-operation/notification/types.go b/server/service-operation/notification/types.go index 3a8a5d2..8a20804 100644 --- a/server/service-operation/notification/types.go +++ b/server/service-operation/notification/types.go @@ -1,71 +1,71 @@ - package notification import "time" // NotificationPayload represents the data sent in notifications type NotificationPayload struct { - ServiceName string `json:"service_name"` - Status string `json:"status"` - Host string `json:"host"` - Hostname string `json:"hostname"` - Port int `json:"port"` - ServiceType string `json:"service_type"` - ResponseTime int64 `json:"response_time"` - Timestamp time.Time `json:"timestamp"` - Message string `json:"message"` - ErrorMessage string `json:"error_message,omitempty"` - + ServiceName string `json:"service_name"` + Status string `json:"status"` + Host string `json:"host"` + Hostname string `json:"hostname"` + Port int `json:"port"` + ServiceType string `json:"service_type"` + ResponseTime int64 `json:"response_time"` + Timestamp time.Time `json:"timestamp"` + Message string `json:"message"` + ErrorMessage string `json:"error_message,omitempty"` + // Service-specific fields - URL string `json:"url,omitempty"` - Domain string `json:"domain,omitempty"` - RegionName string `json:"region_name,omitempty"` - AgentID string `json:"agent_id,omitempty"` - Uptime int `json:"uptime,omitempty"` - + URL string `json:"url,omitempty"` + Domain string `json:"domain,omitempty"` + RegionName string `json:"region_name,omitempty"` + AgentID string `json:"agent_id,omitempty"` + Uptime int `json:"uptime,omitempty"` + // Server monitoring specific 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"` - + 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 Certificate specific 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"` - SerialNumber string `json:"serial_number,omitempty"` + CertificateName string `json:"certificate_name,omitempty"` + ExpiryDate string `json:"expiry_date,omitempty"` + DaysLeft string `json:"days_left,omitempty"` + IssuerCN string `json:"issuer_cn,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` } // AlertConfiguration represents an alert configuration from PocketBase type AlertConfiguration struct { - ID string `json:"id"` - NotificationType string `json:"notification_type"` - TelegramChatID string `json:"telegram_chat_id"` - DiscordWebhookURL string `json:"discord_webhook_url"` - SignalNumber string `json:"signal_number"` - SignalAPIEndpoint string `json:"signal_api_endpoint"` - NotifyName string `json:"notify_name"` - BotToken string `json:"bot_token"` - TemplateID string `json:"template_id"` - SlackWebhookURL string `json:"slack_webhook_url"` - GoogleChatWebhookURL string `json:"google_chat_webhook_url"` - Enabled string `json:"enabled"` // String because PocketBase returns it as string - EmailAddress string `json:"email_address"` - EmailSenderName string `json:"email_sender_name"` - SMTPServer string `json:"smtp_server"` - SMTPPassword string `json:"smtp_password,omitempty"` - SMTPPort string `json:"smtp_port"` - WebhookID string `json:"webhook_id"` - ChannelID string `json:"channel_id"` - WebhookURL string `json:"webhook_url"` + ID string `json:"id"` + NotificationType string `json:"notification_type"` + TelegramChatID string `json:"telegram_chat_id"` + DiscordWebhookURL string `json:"discord_webhook_url"` + SignalNumber string `json:"signal_number"` + SignalAPIEndpoint string `json:"signal_api_endpoint"` + NotifyName string `json:"notify_name"` + BotToken string `json:"bot_token"` + TemplateID string `json:"template_id"` + SlackWebhookURL string `json:"slack_webhook_url"` + GoogleChatWebhookURL string `json:"google_chat_webhook_url"` + FeishuWebhookURL string `json:"feishu_webhook_url"` + Enabled string `json:"enabled"` // String because PocketBase returns it as string + EmailAddress string `json:"email_address"` + EmailSenderName string `json:"email_sender_name"` + SMTPServer string `json:"smtp_server"` + SMTPPassword string `json:"smtp_password,omitempty"` + SMTPPort string `json:"smtp_port"` + WebhookID string `json:"webhook_id"` + ChannelID string `json:"channel_id"` + WebhookURL string `json:"webhook_url"` WebhookPayloadTemplate string `json:"webhook_payload_template"` - NtfyEndpoint string `json:"ntfy_endpoint"` - APIToken string `json:"api_token"` - UserKey string `json:"user_key"` + NtfyEndpoint string `json:"ntfy_endpoint"` + APIToken string `json:"api_token"` + UserKey string `json:"user_key"` } // ServerNotificationTemplate represents a server notification template @@ -90,31 +90,31 @@ type ServerNotificationTemplate struct { RestoreNetworkMessage string `json:"restore_network_message"` RestoreCPUTempMessage string `json:"restore_cpu_temp_message"` RestoreDiskIOMessage string `json:"restore_disk_io_message"` - Placeholder string `json:"placeholder"` + Placeholder string `json:"placeholder"` } // ServiceNotificationTemplate represents a service notification template type ServiceNotificationTemplate struct { - ID string `json:"id"` - Name string `json:"name"` - UpMessage string `json:"up_message"` - DownMessage string `json:"down_message"` - MaintenanceMessage string `json:"maintenance_message"` - IncidentMessage string `json:"incident_message"` - ResolvedMessage string `json:"resolved_message"` - WarningMessage string `json:"warning_message"` - Placeholder string `json:"placeholder"` + ID string `json:"id"` + Name string `json:"name"` + UpMessage string `json:"up_message"` + DownMessage string `json:"down_message"` + MaintenanceMessage string `json:"maintenance_message"` + IncidentMessage string `json:"incident_message"` + ResolvedMessage string `json:"resolved_message"` + WarningMessage string `json:"warning_message"` + Placeholder string `json:"placeholder"` } // NotificationResponse represents the response from notification APIs (like Telegram) type NotificationResponse struct { - OK bool `json:"ok"` - Description string `json:"description,omitempty"` - ErrorCode int `json:"error_code,omitempty"` + OK bool `json:"ok"` + Description string `json:"description,omitempty"` + ErrorCode int `json:"error_code,omitempty"` Result interface{} `json:"result,omitempty"` } // NotificationService interface for different notification services type NotificationService interface { SendNotification(config *AlertConfiguration, message string) error -} \ No newline at end of file +}