package main import ( "fmt" "sync" "github.com/godbus/dbus/v5" "ollie/elevate" ) // elevateNotifier handles desktop notifications for elevation requests // and maps notification action callbacks back to the broker. type elevateNotifier struct { conn *dbus.Conn broker *elevate.Broker mu sync.Mutex notifID map[uint32]string // notification ID -> request ID } var notifier *elevateNotifier // initElevateNotifier sets up the notification action listener. func initElevateNotifier(conn *dbus.Conn, broker *elevate.Broker) { if conn == nil || broker == nil { return } notifier = &elevateNotifier{ conn: conn, broker: broker, notifID: make(map[uint32]string), } // Listen for ActionInvoked signals from the notification daemon conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.Notifications',member='ActionInvoked'") //nolint:errcheck conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.Notifications',member='NotificationClosed'") //nolint:errcheck ch := make(chan *dbus.Signal, 32) conn.Signal(ch) go notifier.listenSignals(ch) } // NotifyElevation is called by the DBusAdapter to show a notification. func (a *DBusAdapter) NotifyElevation(req *elevate.Request) { if notifier == nil { return } notifier.sendNotification(req) } func (n *elevateNotifier) sendNotification(req *elevate.Request) { obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") summary := "Elevation Request" body := fmt.Sprintf("%s\ncwd: %s", escapeMarkup(req.Cmd), escapeMarkup(req.Cwd)) if req.SessionID != "" { body = fmt.Sprintf("session: %s\n%s", escapeMarkup(req.SessionID), body) } actions := []string{ "approve", "Approve", "deny", "Deny", "persist", "Persist", } hints := map[string]dbus.Variant{ "urgency": dbus.MakeVariant(byte(2)), // critical } call := obj.Call("org.freedesktop.Notifications.Notify", 0, "ollie", // app_name uint32(0), // replaces_id "dialog-warning", // icon summary, body, actions, hints, int32(300000), // timeout ms (matches TTL) ) if call.Err != nil { return } var notifID uint32 call.Store(¬ifID) n.mu.Lock() n.notifID[notifID] = req.ID n.mu.Unlock() } func (n *elevateNotifier) listenSignals(ch chan *dbus.Signal) { for sig := range ch { switch sig.Name { case "org.freedesktop.Notifications.ActionInvoked": if len(sig.Body) < 2 { continue } nid, _ := sig.Body[0].(uint32) action, _ := sig.Body[1].(string) n.mu.Lock() reqID, ok := n.notifID[nid] delete(n.notifID, nid) n.mu.Unlock() if !ok { continue } var res elevate.Resolution switch action { case "approve": res = elevate.ResolveApprove case "deny": res = elevate.ResolveDeny case "persist": res = elevate.ResolvePersist default: res = elevate.ResolveDeny } n.broker.Resolve(reqID, res) case "org.freedesktop.Notifications.NotificationClosed": if len(sig.Body) < 2 { continue } nid, _ := sig.Body[0].(uint32) reason, _ := sig.Body[1].(uint32) n.mu.Lock() reqID, ok := n.notifID[nid] delete(n.notifID, nid) n.mu.Unlock() if !ok { continue } // reason 2 = dismissed by user, 1 = expired, 3 = closed by call, 4 = undefined // Treat dismiss as deny if reason == 2 { n.broker.Resolve(reqID, elevate.ResolveDeny) } } } } func escapeMarkup(s string) string { // Basic XML/HTML entity escaping for notification body var out []byte for _, c := range []byte(s) { switch c { case '&': out = append(out, []byte("&")...) case '<': out = append(out, []byte("<")...) case '>': out = append(out, []byte(">")...) default: out = append(out, c) } } return string(out) }