github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/desktop/notification/fdo.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package notification 21 22 import ( 23 "context" 24 "fmt" 25 26 "github.com/godbus/dbus" 27 28 "github.com/snapcore/snapd/logger" 29 ) 30 31 const ( 32 dBusName = "org.freedesktop.Notifications" 33 dBusObjectPath = "/org/freedesktop/Notifications" 34 dBusInterfaceName = "org.freedesktop.Notifications" 35 ) 36 37 // Server holds a connection to a notification server interactions. 38 type Server struct { 39 conn *dbus.Conn 40 obj dbus.BusObject 41 } 42 43 // New returns new connection to a freedesktop.org message notification server. 44 // 45 // Each server offers specific capabilities. It is advised to provide graceful 46 // degradation of functionality, depending on the supported capabilities, so 47 // that the notification messages are useful on a wide range of desktop 48 // environments. 49 func New(conn *dbus.Conn) *Server { 50 return &Server{ 51 conn: conn, 52 obj: conn.Object(dBusName, dBusObjectPath), 53 } 54 } 55 56 // ServerInformation returns the information about the notification server. 57 func (srv *Server) ServerInformation() (name, vendor, version, specVersion string, err error) { 58 call := srv.obj.Call(dBusInterfaceName+".GetServerInformation", 0) 59 if err := call.Store(&name, &vendor, &version, &specVersion); err != nil { 60 return "", "", "", "", err 61 } 62 return name, vendor, version, specVersion, nil 63 } 64 65 // ServerCapabilities returns the list of notification capabilities provided by the session. 66 func (srv *Server) ServerCapabilities() ([]ServerCapability, error) { 67 call := srv.obj.Call(dBusInterfaceName+".GetCapabilities", 0) 68 var caps []ServerCapability 69 if err := call.Store(&caps); err != nil { 70 return nil, err 71 } 72 return caps, nil 73 } 74 75 // SendNotification sends a new notification or updates an existing 76 // notification. In both cases the ID of the notification, as assigned by the 77 // server, is returned. The ID can be used to cancel a notification, update it 78 // or react to invoked user actions. 79 func (srv *Server) SendNotification(msg *Message) (ID, error) { 80 call := srv.obj.Call(dBusInterfaceName+".Notify", 0, 81 msg.AppName, msg.ReplacesID, msg.Icon, msg.Summary, msg.Body, 82 flattenActions(msg.Actions), mapHints(msg.Hints), 83 int32(msg.ExpireTimeout.Nanoseconds()/1e6)) 84 var id ID 85 if err := call.Store(&id); err != nil { 86 return 0, err 87 } 88 return id, nil 89 } 90 91 func flattenActions(actions []Action) []string { 92 result := make([]string, len(actions)*2) 93 for i, action := range actions { 94 result[i*2] = action.ActionKey 95 result[i*2+1] = action.LocalizedText 96 } 97 return result 98 } 99 100 func mapHints(hints []Hint) map[string]dbus.Variant { 101 result := make(map[string]dbus.Variant, len(hints)) 102 for _, hint := range hints { 103 result[hint.Name] = dbus.MakeVariant(hint.Value) 104 } 105 return result 106 } 107 108 // CloseNotification closes a notification message with the given ID. 109 func (srv *Server) CloseNotification(id ID) error { 110 call := srv.obj.Call(dBusInterfaceName+".CloseNotification", 0, id) 111 return call.Store() 112 } 113 114 // ObserveNotifications blocks and processes message notification signals. 115 // 116 // The bus connection is configured to deliver signals from the notification 117 // server. All received signals are dispatched to the provided observer. This 118 // process continues until stopped by the context, or if an error occurs. 119 func (srv *Server) ObserveNotifications(ctx context.Context, observer Observer) (err error) { 120 // TODO: upgrade godbus and use un-buffered channel. 121 ch := make(chan *dbus.Signal, 10) 122 defer close(ch) 123 124 srv.conn.Signal(ch) 125 defer srv.conn.RemoveSignal(ch) 126 127 matchRules := []dbus.MatchOption{ 128 dbus.WithMatchSender(dBusName), 129 dbus.WithMatchObjectPath(dBusObjectPath), 130 dbus.WithMatchInterface(dBusInterfaceName), 131 } 132 if err := srv.conn.AddMatchSignal(matchRules...); err != nil { 133 return err 134 } 135 defer func() { 136 if err := srv.conn.RemoveMatchSignal(matchRules...); err != nil { 137 // XXX: this should not fail for us in practice but we don't want 138 // to clobber the actual error being returned from the function in 139 // general, so ignore RemoveMatchSignal errors and just log them 140 // instead. 141 logger.Noticef("Cannot remove D-Bus signal matcher: %v", err) 142 } 143 }() 144 145 for { 146 select { 147 case <-ctx.Done(): 148 return ctx.Err() 149 case sig := <-ch: 150 if err := processSignal(sig, observer); err != nil { 151 return err 152 } 153 } 154 } 155 } 156 157 func processSignal(sig *dbus.Signal, observer Observer) error { 158 switch sig.Name { 159 case dBusInterfaceName + ".NotificationClosed": 160 if err := processNotificationClosed(sig, observer); err != nil { 161 return fmt.Errorf("cannot process NotificationClosed signal: %v", err) 162 } 163 case dBusInterfaceName + ".ActionInvoked": 164 if err := processActionInvoked(sig, observer); err != nil { 165 return fmt.Errorf("cannot process ActionInvoked signal: %v", err) 166 } 167 } 168 return nil 169 } 170 171 func processNotificationClosed(sig *dbus.Signal, observer Observer) error { 172 if len(sig.Body) != 2 { 173 return fmt.Errorf("unexpected number of body elements: %d", len(sig.Body)) 174 } 175 id, ok := sig.Body[0].(uint32) 176 if !ok { 177 return fmt.Errorf("expected first body element to be uint32, got %T", sig.Body[0]) 178 } 179 reason, ok := sig.Body[1].(uint32) 180 if !ok { 181 return fmt.Errorf("expected second body element to be uint32, got %T", sig.Body[1]) 182 } 183 return observer.NotificationClosed(ID(id), CloseReason(reason)) 184 } 185 186 func processActionInvoked(sig *dbus.Signal, observer Observer) error { 187 if len(sig.Body) != 2 { 188 return fmt.Errorf("unexpected number of body elements: %d", len(sig.Body)) 189 } 190 id, ok := sig.Body[0].(uint32) 191 if !ok { 192 return fmt.Errorf("expected first body element to be uint32, got %T", sig.Body[0]) 193 } 194 actionKey, ok := sig.Body[1].(string) 195 if !ok { 196 return fmt.Errorf("expected second body element to be string, got %T", sig.Body[1]) 197 } 198 return observer.ActionInvoked(ID(id), actionKey) 199 }