Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions server/service-operation/notification/feishu.go
Original file line number Diff line number Diff line change
@@ -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))
}
6 changes: 3 additions & 3 deletions server/service-operation/notification/manager.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package notification

import (
Expand All @@ -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()
Expand All @@ -31,6 +30,7 @@ func NewNotificationManager(pbClient *pocketbase.PocketBaseClient) *Notification
services["webhook"] = NewWebhookService()
services["ntfy"] = NewNtfyService()
services["pushover"] = NewPushoverService()
services["feishu"] = NewFeishuService()
services["notifiarr"] = NewNotifiarrService()
services["gotify"] = NewGotifyService()

Expand Down Expand Up @@ -68,4 +68,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)
}
}
Loading