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  }