Files
checkcle/server/service-operation/notification/matrix.go
T
MrSchneemann 91ceddc489 feat: add Matrix chat notification support (#211)
Implements native Matrix notification channel support, allowing users
to receive service alerts in any Matrix room via the Client-Server API.

Changes:
- server/service-operation/notification/matrix.go: new Matrix service
  implementing the NotificationService interface (PUT /_matrix/client/v3/...)
- server/service-operation/notification/types.go: add MatrixHomeserver,
  MatrixRoomID, MatrixAccessToken fields to AlertConfiguration
- server/service-operation/notification/manager.go: register MatrixService
- server/pb_migrations/1772200000_updated_alert_configurations_matrix.js:
  PocketBase migration adding matrix fields and notification_type value
- application: Matrix option in channel dialog with homeserver/room/token
  fields, tab filter, list display, type definitions and translations (en, km)
- application/public/upload/notification/matrix.png: Matrix channel icon
2026-03-01 10:48:01 +01:00

302 lines
10 KiB
Go

package notification
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// MatrixService handles Matrix chat notifications
type MatrixService struct{}
// NewMatrixService creates a new Matrix notification service
func NewMatrixService() *MatrixService {
return &MatrixService{}
}
// matrixMessagePayload represents the payload for the Matrix Client-Server API
type matrixMessagePayload struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
Format string `json:"format,omitempty"`
FormattedBody string `json:"formatted_body,omitempty"`
}
// SendNotification sends a plain message to a Matrix room
func (ms *MatrixService) SendNotification(config *AlertConfiguration, message string) error {
if config.MatrixHomeserver == "" || config.MatrixRoomID == "" || config.MatrixAccessToken == "" {
return fmt.Errorf("matrix homeserver, room ID and access token are required")
}
// Encode the room ID for use in the URL path
encodedRoomID := url.PathEscape(config.MatrixRoomID)
// Use Unix nanoseconds as a unique transaction ID to prevent duplicate messages
txnID := fmt.Sprintf("%d", time.Now().UnixNano())
apiURL := fmt.Sprintf("%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s",
strings.TrimRight(config.MatrixHomeserver, "/"),
encodedRoomID,
txnID,
)
payload := matrixMessagePayload{
MsgType: "m.text",
Body: message,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("matrix: failed to marshal payload: %w", err)
}
req, err := http.NewRequest(http.MethodPut, apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("matrix: failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+config.MatrixAccessToken)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("matrix: HTTP request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("matrix: API returned status %d", resp.StatusCode)
}
return nil
}
// SendServerNotification sends a server-specific notification via Matrix
func (ms *MatrixService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
message := ms.generateServerMessage(payload, template, resourceType)
return ms.SendNotification(config, message)
}
// SendServiceNotification sends a service-specific notification via Matrix
func (ms *MatrixService) SendServiceNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServiceNotificationTemplate) error {
message := ms.generateServiceMessage(payload, template)
return ms.SendNotification(config, message)
}
func (ms *MatrixService) generateServerMessage(payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) string {
var templateMessage string
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:
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 templateMessage == "" {
templateMessage = ms.generateDefaultServerMessage(payload, resourceType)
}
return ms.replacePlaceholders(templateMessage, payload)
}
func (ms *MatrixService) generateServiceMessage(payload *NotificationPayload, template *ServiceNotificationTemplate) string {
var templateMessage string
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 templateMessage == "" {
templateMessage = ms.generateDefaultUptimeMessage(payload)
}
return ms.replacePlaceholders(templateMessage, payload)
}
func (ms *MatrixService) replacePlaceholders(message string, payload *NotificationPayload) string {
message = strings.ReplaceAll(message, "${service_name}", payload.ServiceName)
message = strings.ReplaceAll(message, "${status}", strings.ToUpper(payload.Status))
message = strings.ReplaceAll(message, "${host}", ms.safeString(payload.Host))
message = strings.ReplaceAll(message, "${hostname}", ms.safeString(payload.Hostname))
u := ms.safeString(payload.URL)
if u == "N/A" && payload.Host != "" {
u = payload.Host
}
message = strings.ReplaceAll(message, "${url}", u)
message = strings.ReplaceAll(message, "${domain}", ms.safeString(payload.Domain))
if payload.ServiceType != "" {
message = strings.ReplaceAll(message, "${service_type}", strings.ToUpper(payload.ServiceType))
} else {
message = strings.ReplaceAll(message, "${service_type}", "N/A")
}
message = strings.ReplaceAll(message, "${region_name}", ms.safeString(payload.RegionName))
message = strings.ReplaceAll(message, "${agent_id}", ms.safeString(payload.AgentID))
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")
}
message = strings.ReplaceAll(message, "${cpu_usage}", ms.safeString(payload.CPUUsage))
message = strings.ReplaceAll(message, "${ram_usage}", ms.safeString(payload.RAMUsage))
message = strings.ReplaceAll(message, "${disk_usage}", ms.safeString(payload.DiskUsage))
message = strings.ReplaceAll(message, "${network_usage}", ms.safeString(payload.NetworkUsage))
message = strings.ReplaceAll(message, "${cpu_temp}", ms.safeString(payload.CPUTemp))
message = strings.ReplaceAll(message, "${disk_io}", ms.safeString(payload.DiskIO))
message = strings.ReplaceAll(message, "${threshold}", ms.safeString(payload.Threshold))
message = strings.ReplaceAll(message, "${error_message}", ms.safeString(payload.ErrorMessage))
message = strings.ReplaceAll(message, "${error}", ms.safeString(payload.ErrorMessage))
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
}
func (ms *MatrixService) safeString(value string) string {
if value == "" {
return "N/A"
}
return value
}
func (ms *MatrixService) generateDefaultUptimeMessage(payload *NotificationPayload) string {
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))
details := []string{}
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))
}
if payload.ServiceType != "" {
details = append(details, fmt.Sprintf(" - Type: %s", strings.ToUpper(payload.ServiceType)))
}
if payload.Port > 0 {
details = append(details, fmt.Sprintf(" - Port: %d", payload.Port))
}
if payload.Domain != "" {
details = append(details, fmt.Sprintf(" - Domain: %s", payload.Domain))
}
if payload.ResponseTime > 0 {
details = append(details, fmt.Sprintf(" - Response time: %dms", payload.ResponseTime))
} else {
details = append(details, " - Response time: N/A")
}
if payload.RegionName != "" {
details = append(details, fmt.Sprintf(" - Region: %s", payload.RegionName))
}
if payload.Uptime > 0 {
details = append(details, fmt.Sprintf(" - Uptime: %d%%", payload.Uptime))
}
details = append(details, fmt.Sprintf(" - Time: %s", payload.Timestamp.Format("2006-01-02 15:04:05")))
if len(details) > 0 {
message += "\n" + strings.Join(details, "\n")
}
return message
}
func (ms *MatrixService) 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))
}