This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-9p/elevate_notify.go

168 lines
3.8 KiB
Go

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("<b>%s</b>\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(&notifID)
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("&amp;")...)
case '<':
out = append(out, []byte("&lt;")...)
case '>':
out = append(out, []byte("&gt;")...)
default:
out = append(out, c)
}
}
return string(out)
}