168 lines
3.8 KiB
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(¬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)
|
|
}
|