diff --git a/bus/bus.go b/bus/bus.go new file mode 100644 index 0000000..7610c98 --- /dev/null +++ b/bus/bus.go @@ -0,0 +1,76 @@ +package bus + +import ( + "time" + + "github.com/godbus/dbus/v5" +) + +type NotifyBus struct { + conn *dbus.Conn +} + +type BusNotification struct { + ID uint32 + Summary string + Body string + Timeout time.Duration +} + +type NotifyServerInfo struct { + Name string + Vendor string + Version string + SpecVersion string +} + +func NewNotifyBus() (*NotifyBus, error) { + conn, err := dbus.ConnectSessionBus() + if err != nil { + return nil, err + } + return &NotifyBus{conn: conn}, nil +} + +func (n *NotifyBus) Close() { + n.conn.Close() +} + +func (n *NotifyBus) ServerInfo() (*NotifyServerInfo, error) { + obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + call := obj.Call( + "org.freedesktop.Notifications.GetServerInformation", // Method + 0, // Flags + ) + if call.Err != nil { + return nil, call.Err + } + + srvInfo := &NotifyServerInfo{} + call.Store(&srvInfo.Name, &srvInfo.Vendor, &srvInfo.Version, &srvInfo.SpecVersion) + return srvInfo, nil +} + +func (n *NotifyBus) Notify(notification BusNotification) (uint32, error) { + obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + var ret uint32 + call := obj.Call( + "org.freedesktop.Notifications.Notify", // Method + 0, // Flags + "alerttonotify", // App name + notification.ID, // Notification ID + "", // Icon + notification.Summary, // Summary + notification.Body, // Body + []string{}, // Actions + map[string]dbus.Variant{}, // Hints + int32(notification.Timeout.Milliseconds()), // Timeout + ) + if call.Err != nil { + return ret, call.Err + } + + call.Store(&ret) + + return ret, nil +} diff --git a/flake.nix b/flake.nix index e575015..aa96fb5 100644 --- a/flake.nix +++ b/flake.nix @@ -48,7 +48,7 @@ version = version; pname = "alerttonotify"; src = src; - vendorHash = pkgs.lib.fakeHash; + vendorHash = "sha256-1ejnJykXY+j/xddFupLII2SXGsbxBSUaAe20YpcMzgQ="; }; } ); diff --git a/go.mod b/go.mod index 8cce481..d496354 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.t-juice.club/torjus/alerttonotify go 1.23.3 + +require github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..024b269 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/main.go b/main.go index 814c753..f5f5e01 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,59 @@ package main -import "fmt" +import ( + "context" + "log/slog" + "net/http" + "os" + "os/signal" + + "git.t-juice.club/torjus/alerttonotify/bus" + "git.t-juice.club/torjus/alerttonotify/server" +) const Version = "v0.1.0" func main() { - fmt.Println("Hello!") + // Setup logging + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + logger.Info("Starting alerttonotify", "version", Version) + + // Setup dbus connection + nbus, err := bus.NewNotifyBus() + if err != nil { + logger.Error("Failed to create notify bus", "error", err) + os.Exit(1) + } + defer nbus.Close() + + // Verify connection and server + info, err := nbus.ServerInfo() + if err != nil { + logger.Error("Failed to get notification server info", "error", err) + os.Exit(1) + return + } + logger.Info("Connected to notification daemon", "server", info.Name, "version", info.Version) + + shutdownCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + // Setup http server + srv := server.NewServer(nbus, logger) + srv.Addr = ":5001" + + // Listen for shutdown signal + go func() { + <-shutdownCtx.Done() + srv.Shutdown(context.Background()) + }() + + logger.Info("Starting http server", "addr", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error("Failed to start http server", "error", err) + os.Exit(1) + } + logger.Info("Shutting down") } diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..2456060 --- /dev/null +++ b/server/server.go @@ -0,0 +1,91 @@ +package server + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "git.t-juice.club/torjus/alerttonotify/bus" +) + +type Server struct { + logger *slog.Logger + nbus *bus.NotifyBus + http.Server +} + +type Alert struct { + Status string `json:"status"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + GeneratorURL string `json:"generatorURL"` + Fingerprint string `json:"fingerprint"` +} +type AlertMessage struct { + Version string `json:"version"` + GroupKey string `json:"groupKey"` + TruncatedAlerts int `json:"truncatedAlerts"` + Status string `json:"status"` + Receiver string `json:"receiver"` + GroupLabels map[string]string `json:"groupLabels"` + CommonLabels map[string]string `json:"commonLabels"` + CommonAnnotations map[string]string `json:"commonAnnotations"` + ExternalURL string `json:"externalURL"` + Alerts []Alert `json:"alerts"` +} + +func NewServer(nbus *bus.NotifyBus, logger *slog.Logger) *Server { + srv := &Server{ + nbus: nbus, + logger: logger, + } + + mux := http.NewServeMux() + mux.HandleFunc("GET /", srv.handleIndex) + mux.HandleFunc("POST /alert", srv.handleAlert) + + srv.Handler = mux + + return srv +} + +func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { + s.logger.Info("index page") + w.Write([]byte("Hello!")) +} + +func (s *Server) handleAlert(w http.ResponseWriter, r *http.Request) { + s.logger.Debug("Got new alert", "remoteAddr", r.RemoteAddr) + + decoder := json.NewDecoder(r.Body) + var alertMessage AlertMessage + if err := decoder.Decode(&alertMessage); err != nil { + s.logger.Error("Failed to decode alert message", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + s.logger.Debug("Decoded alert message", "alert", alertMessage) + + s.logger.Info("Incoming message", "status", alertMessage.Status) + var sb strings.Builder + for _, alert := range alertMessage.Alerts { + s.logger.Debug("Incoming alert", "status", alert.Status, "fingerprint", alert.Fingerprint) + if summary, ok := alert.Annotations["summary"]; ok { + sb.WriteString(fmt.Sprintf("[%s] %s\n", strings.ToUpper(alert.Status), summary)) + } else { + sb.WriteString(fmt.Sprintf("[%s]: %s\n", strings.ToUpper(alert.Status), alert.Fingerprint)) + } + } + + notification := bus.BusNotification{ + Summary: fmt.Sprintf("%d alerts %s", len(alertMessage.Alerts), alertMessage.Status), + Body: sb.String(), + } + s.logger.Debug("Sending notification", "notification", notification) + s.nbus.Notify(notification) +}