From 083e3017abf7fe108efbc7750ae6077047e8934e Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 30 Aug 2017 20:43:50 +0200 Subject: [PATCH] Use GNotification instead of pynotify or dbus --- gajim/app_actions.py | 6 + gajim/common/config.py | 1 - gajim/common/dbus_support.py | 21 -- gajim/features_window.py | 16 -- gajim/gajim.py | 5 +- gajim/notify.py | 415 ++++------------------------------- 6 files changed, 50 insertions(+), 414 deletions(-) diff --git a/gajim/app_actions.py b/gajim/app_actions.py index a88a75bb7..7f58893ed 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -228,3 +228,9 @@ class AppActions(): else: interface.instances['logs'] = history_window.\ HistoryWindow() + + def on_open_event(self, action, param): + dict_ = param.unpack() + app.interface.handle_event(dict_['account'], dict_['jid'], + dict_['type_']) + diff --git a/gajim/common/config.py b/gajim/common/config.py index 6fbf8570d..48f662d90 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -74,7 +74,6 @@ class Config: 'autopopupaway': [ opt_bool, False ], 'autopopup_chat_opened': [ opt_bool, False, _('Show desktop notification even when a chat window is opened for this contact and does not have focus') ], 'sounddnd': [ opt_bool, False, _('Play sound when user is busy')], - 'use_notif_daemon': [ opt_bool, True, _('Use D-Bus and Notification-Daemon to show notifications') ], 'showoffline': [ opt_bool, False ], 'show_only_chat_and_online': [ opt_bool, False, _('Show only online and free for chat contacts in roster.')], 'show_transports_group': [ opt_bool, True ], diff --git a/gajim/common/dbus_support.py b/gajim/common/dbus_support.py index c8476551b..77b45e753 100644 --- a/gajim/common/dbus_support.py +++ b/gajim/common/dbus_support.py @@ -159,27 +159,6 @@ def get_interface(interface, path, start_service=True): return None -def get_notifications_interface(notif=None): - """ - Get the notifications interface - - :param notif: DesktopNotification instance - """ - # try to see if KDE notifications are available - iface = get_interface('org.kde.VisualNotifications', '/VisualNotifications', - start_service=False) - if iface != None: - if notif != None: - notif.kde_notifications = True - return iface - # KDE notifications don't seem to be available, falling back to - # notification-daemon - else: - if notif != None: - notif.kde_notifications = False - return get_interface('org.freedesktop.Notifications', - '/org/freedesktop/Notifications') - if supported: class MissingArgument(dbus.DBusException): _dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument' diff --git a/gajim/features_window.py b/gajim/features_window.py index 50ce25bdb..496e15de9 100644 --- a/gajim/features_window.py +++ b/gajim/features_window.py @@ -73,10 +73,6 @@ class FeaturesWindow: _('Spellchecking of composed messages.'), _('Requires libgtkspell.'), _('Requires libgtkspell and libenchant.')), - _('Notification'): (self.notification_available, - _('Passive popups notifying for new events.'), - _('Requires python-notify or instead python-dbus in conjunction with notification-daemon.'), - _('Feature not available under Windows.')), _('Automatic status'): (self.idle_available, _('Ability to measure idle time, in order to set auto status.'), _('Requires libxss library.'), @@ -199,18 +195,6 @@ class FeaturesWindow: return False return True - def notification_available(self): - if os.name == 'nt': - return False - from gajim.common import dbus_support - if self.dbus_available() and dbus_support.get_notifications_interface(): - return True - try: - __import__('pynotify') - except Exception: - return False - return True - def idle_available(self): from gajim.common import sleepy return sleepy.SUPPORTED diff --git a/gajim/gajim.py b/gajim/gajim.py index 269c1c8af..a47817539 100644 --- a/gajim/gajim.py +++ b/gajim/gajim.py @@ -339,7 +339,8 @@ class GajimApplication(Gtk.Application): ('-update-motd', action.on_update_motd, 'online', 's'), ('-delete-motd', action.on_delete_motd, 'online', 's'), ('-activate-bookmark', - action.on_activate_bookmark, 'online', 'a{sv}') + action.on_activate_bookmark, 'online', 'a{sv}'), + ('-open-event', action.on_open_event, 'always', 'a{sv}'), ] self.general_actions = [ @@ -355,7 +356,7 @@ class GajimApplication(Gtk.Application): ('features', action.on_features), ('content', action.on_contents), ('about', action.on_about), - ('faq', action.on_faq) + ('faq', action.on_faq), ] for action in self.general_actions: diff --git a/gajim/notify.py b/gajim/notify.py index 6322e2898..8be89082f 100644 --- a/gajim/notify.py +++ b/gajim/notify.py @@ -27,31 +27,16 @@ ## along with Gajim. If not, see . ## -import os -import time +import sys from gajim.dialogs import PopupNotificationWindow -from gi.repository import GObject from gi.repository import GLib +from gi.repository import Gio from gajim import gtkgui_helpers from gajim.common import app from gajim.common import helpers from gajim.common import ged -from gajim.common import dbus_support -if dbus_support.supported: - import dbus - - -USER_HAS_PYNOTIFY = True # user has pynotify module -try: - import gi - gi.require_version('Notify', '0.7') - from gi.repository import Notify - Notify.init('Gajim Notification') -except ValueError: - USER_HAS_PYNOTIFY = False - def get_show_in_roster(event, account, contact, session=None): """ Return True if this event must be shown in roster, else False @@ -74,12 +59,11 @@ def get_show_in_systray(event, account, contact, type_=None): return False return app.config.get('trayicon_notification_on_events') -def popup(event_type, jid, account, msg_type='', path_to_image=None, title=None, +def popup(event_type, jid, account, type_='', path_to_image=None, title=None, text=None, timeout=-1): """ - Notify a user of an event. It first tries to a valid implementation of - the Desktop Notification Specification. If that fails, then we fall back to - the older style PopupNotificationWindow method + Notify a user of an event using GNotification and GApplication under linux, + the older style PopupNotificationWindow method under windows """ # default image if not path_to_image: @@ -88,67 +72,44 @@ text=None, timeout=-1): if timeout < 0: timeout = app.config.get('notification_timeout') - # Try to show our popup via D-Bus and notification daemon - if app.config.get('use_notif_daemon') and dbus_support.supported: - try: - DesktopNotification(event_type, jid, account, msg_type, - path_to_image, title, GLib.markup_escape_text(text), timeout) - return # sucessfully did D-Bus Notification procedure! - except dbus.DBusException as e: - # Connection to D-Bus failed - app.log.debug(str(e)) - except TypeError as e: - # This means that we sent the message incorrectly - app.log.debug(str(e)) + if sys.platform == 'win32': + instance = PopupNotificationWindow(event_type, jid, account, type_, + path_to_image, title, text, timeout) + app.interface.roster.popup_notification_windows.append(instance) + return - # Ok, that failed. Let's try pynotify, which also uses notification daemon - if app.config.get('use_notif_daemon') and USER_HAS_PYNOTIFY: - if not text and event_type == 'new_message': - # empty text for new_message means do_preview = False - # -> default value for text - _text = GLib.markup_escape_text(app.get_name_from_jid(account, - jid)) - else: - _text = GLib.markup_escape_text(text) + # use GNotification + # TODO: Move to standard GTK+ icons here. + icon = Gio.FileIcon.new(Gio.File.new_for_path(path_to_image)) + notification = Gio.Notification() + if title is not None: + notification.set_title(title) + if text is not None: + notification.set_body(text) + notification.set_icon(icon) + notif_id = None + if event_type in (_('Contact Signed In'), _('Contact Signed Out'), + _('New Message'), _('New Single Message'), _('New Private Message'), + _('Contact Changed Status'), _('File Transfer Request'), + _('File Transfer Error'), _('File Transfer Completed'), + _('File Transfer Stopped'), _('Groupchat Invitation'), + _('Connection Failed'), _('Subscription request'), _('Unsubscribed')): + # Create Variant Dict + dict_ = {'account': GLib.Variant('s', account), + 'jid': GLib.Variant('s', jid), + 'type_': GLib.Variant('s', type_)} + variant_dict = GLib.Variant('a{sv}', dict_) + action = 'app.{}-open-event'.format(account) + notification.add_button_with_target('Open', action, variant_dict) + notification.set_default_action_and_target(action, variant_dict) + if event_type in (_('New Message'), _('New Single Message'), + _('New Private Message')): + # Only one notification per JID + notif_id = jid + notification.set_priority(Gio.NotificationPriority.NORMAL) + notification.set_urgent(False) + app.app.send_notification(notif_id, notification) - if not title: - _title = '' - else: - _title = title - - notification = Notify.Notification.new(_title, _text) - notification.set_timeout(timeout*1000) - - notification.set_category(event_type) - notification._data = {} - notification._data["event_type"] = event_type - notification._data["jid"] = jid - notification._data["account"] = account - notification._data["msg_type"] = msg_type - notification.set_property('icon-name', path_to_image) - if 'actions' in Notify.get_server_caps(): - notification.add_action('default', 'Default Action', - on_pynotify_notification_clicked) - - try: - notification.show() - return - except GObject.GError as e: - # Connection to notification-daemon failed, see #2893 - app.log.debug(str(e)) - - # Either nothing succeeded or the user wants old-style notifications - instance = PopupNotificationWindow(event_type, jid, account, msg_type, - path_to_image, title, text, timeout) - app.interface.roster.popup_notification_windows.append(instance) - -def on_pynotify_notification_clicked(notification, action): - jid = notification._data.jid - account = notification._data.account - msg_type = notification._data.msg_type - - notification.close() - app.interface.handle_event(account, jid, msg_type) class Notification: """ @@ -184,297 +145,3 @@ class Notification: helpers.exec_command(obj.command, use_shell=True) except Exception: pass - -class NotificationResponseManager: - """ - Collect references to pending DesktopNotifications and manages there - signalling. This is necessary due to a bug in DBus where you can't remove a - signal from an interface once it's connected - """ - - def __init__(self): - self.pending = {} - self.received = [] - self.interface = None - - def attach_to_interface(self): - if self.interface is not None: - return - self.interface = dbus_support.get_notifications_interface() - self.interface.connect_to_signal('ActionInvoked', - self.on_action_invoked) - self.interface.connect_to_signal('NotificationClosed', self.on_closed) - - def on_action_invoked(self, id_, reason): - if id_ in self.pending: - notification = self.pending[id_] - notification.on_action_invoked(id_, reason) - del self.pending[id_] - return - # got an action on popup that isn't handled yet? Maybe user clicked too - # fast. Remember it. - self.received.append((id_, time.time(), reason)) - if len(self.received) > 20: - curt = time.time() - for rec in self.received: - diff = curt - rec[1] - if diff > 10: - self.received.remove(rec) - - def on_closed(self, id_, reason=None): - if id_ in self.pending: - del self.pending[id_] - - def add_pending(self, id_, object_): - # Check to make sure that we handle an event immediately if we're adding - # an id that's already been triggered - for rec in self.received: - if rec[0] == id_: - object_.on_action_invoked(id_, rec[2]) - self.received.remove(rec) - return - if id_ not in self.pending: - # Add it - self.pending[id_] = object_ - else: - # We've triggered an event that has a duplicate ID! - app.log.debug('Duplicate ID of notification. Can\'t handle this.') - -notification_response_manager = NotificationResponseManager() - -class DesktopNotification: - """ - A DesktopNotification that interfaces with D-Bus via the Desktop - Notification Specification - """ - - def __init__(self, event_type, jid, account, msg_type='', - path_to_image=None, title=None, text=None, timeout=-1): - self.path_to_image = os.path.abspath(path_to_image) - self.event_type = event_type - self.title = title - self.text = text - self.timeout = timeout - # 0.3.1 is the only version of notification daemon that has no way - # to determine which version it is. If no method exists, it means - # they're using that one. - self.default_version = [0, 3, 1] - self.account = account - self.jid = jid - self.msg_type = msg_type - - # default value of text - if not text and event_type == 'new_message': - # empty text for new_message means do_preview = False - self.text = app.get_name_from_jid(account, jid) - - if not title: - self.title = event_type # default value - - if event_type == _('Contact Signed In'): - ntype = 'presence.online' - elif event_type == _('Contact Signed Out'): - ntype = 'presence.offline' - elif event_type in (_('New Message'), _('New Single Message'), - _('New Private Message')): - ntype = 'im.received' - elif event_type == _('File Transfer Request'): - ntype = 'transfer' - elif event_type == _('File Transfer Error'): - ntype = 'transfer.error' - elif event_type in (_('File Transfer Completed'), - _('File Transfer Stopped')): - ntype = 'transfer.complete' - elif event_type == _('New E-mail'): - ntype = 'email.arrived' - elif event_type == _('Groupchat Invitation'): - ntype = 'im.invitation' - elif event_type == _('Contact Changed Status'): - ntype = 'presence.status' - elif event_type == _('Connection Failed'): - ntype = 'connection.failed' - elif event_type == _('Subscription request'): - ntype = 'subscription.request' - elif event_type == _('Unsubscribed'): - ntype = 'unsubscribed' - else: - # default failsafe values - self.path_to_image = gtkgui_helpers.get_icon_path( - 'gajim-chat_msg_recv', 48) - ntype = 'im' # Notification Type - - self.notif = dbus_support.get_notifications_interface(self) - if self.notif is None: - raise dbus.DBusException('unable to get notifications interface') - self.ntype = ntype - - if self.kde_notifications: - self.attempt_notify() - else: - self.capabilities = self.notif.GetCapabilities() - if self.capabilities is None: - self.capabilities = ['actions'] - self.get_version() - - def attempt_notify(self): - ntype = self.ntype - if self.kde_notifications: - notification_text = ('' \ - '%(title)s
%(text)s') % {'title': self.title, - 'text': self.text, 'image': self.path_to_image} - gajim_icon = gtkgui_helpers.get_icon_path('org.gajim.Gajim', 48) - try: - self.notif.Notify( - dbus.String(_('Gajim')), # app_name (string) - dbus.UInt32(0), # replaces_id (uint) - ntype, # event_id (string) - dbus.String(gajim_icon), # app_icon (string) - dbus.String(''), # summary (string) - dbus.String(notification_text), # body (string) - # actions (stringlist) - (dbus.String('default'), dbus.String(self.event_type), - dbus.String('ignore'), dbus.String(_('Ignore'))), - [], # hints (not used in KDE yet) - dbus.UInt32(self.timeout*1000), # timeout (int), in ms - reply_handler=self.attach_by_id, - error_handler=self.notify_another_way) - return - except Exception: - pass - version = self.version - if version[:2] == [0, 2]: - actions = {} - if 'actions' in self.capabilities and self.msg_type: - actions = {'default': 0} - try: - self.notif.Notify( - dbus.String(_('Gajim')), - dbus.String(self.path_to_image), - dbus.UInt32(0), - ntype, - dbus.Byte(0), - dbus.String(self.title), - dbus.String(self.text), - [dbus.String(self.path_to_image)], - actions, - [''], - True, - dbus.UInt32(self.timeout), - reply_handler=self.attach_by_id, - error_handler=self.notify_another_way) - except AttributeError: - # we're actually dealing with the newer version - version = [0, 3, 1] - if version > [0, 3]: - if app.interface.systray_enabled and \ - app.config.get('attach_notifications_to_systray'): - status_icon = app.interface.systray.status_icon - rect = status_icon.get_geometry()[2] - x, y, width, height = rect.x, rect.y, rect.width, rect.height - pos_x = x + (width / 2) - pos_y = y + (height / 2) - hints = {'x': pos_x, 'y': pos_y} - else: - hints = {} - if version >= [0, 3, 2]: - hints['urgency'] = dbus.Byte(0) # Low Urgency - hints['category'] = dbus.String(ntype) - # it seems notification-daemon doesn't like empty text - if self.text: - text = self.text - if len(self.text) > 200: - text = '%s\n…' % self.text[:200] - else: - text = ' ' - if os.environ.get('KDE_FULL_SESSION') == 'true': - text = '' \ - '
' \ - ' %s
' % (self.path_to_image, - text) - self.path_to_image = os.path.abspath( - gtkgui_helpers.get_icon_path('org.gajim.Gajim', 48)) - actions = () - if 'actions' in self.capabilities and self.msg_type: - actions = (dbus.String('default'), dbus.String( - self.event_type)) - try: - self.notif.Notify( - dbus.String(_('Gajim')), - # this notification does not replace other - dbus.UInt32(0), - dbus.String(self.path_to_image), - dbus.String(self.title), - dbus.String(text), - actions, - hints, - dbus.UInt32(self.timeout*1000), - reply_handler=self.attach_by_id, - error_handler=self.notify_another_way) - except Exception as e: - self.notify_another_way(e) - else: - try: - self.notif.Notify( - dbus.String(_('Gajim')), - dbus.String(self.path_to_image), - dbus.UInt32(0), - dbus.String(self.title), - dbus.String(self.text), - dbus.String(''), - hints, - dbus.UInt32(self.timeout*1000), - reply_handler=self.attach_by_id, - error_handler=self.notify_another_way) - except Exception as e: - self.notify_another_way(e) - - def attach_by_id(self, id_): - notification_response_manager.attach_to_interface() - notification_response_manager.add_pending(id_, self) - - def notify_another_way(self, e): - app.log.debug('Error when trying to use notification daemon: %s' % \ - str(e)) - instance = PopupNotificationWindow(self.event_type, self.jid, - self.account, self.msg_type, self.path_to_image, self.title, - self.text, self.timeout) - app.interface.roster.popup_notification_windows.append(instance) - - def on_action_invoked(self, id_, reason): - if self.notif is None: - return - self.notif.CloseNotification(dbus.UInt32(id_)) - self.notif = None - - if reason == 'ignore': - return - - app.interface.handle_event(self.account, self.jid, self.msg_type) - - def version_reply_handler(self, name, vendor, version, spec_version=None): - if spec_version: - version = spec_version - elif vendor == 'Xfce' and version.startswith('0.1.0'): - version = '0.9' - version_list = version.split('.') - self.version = [] - try: - while len(version_list): - self.version.append(int(version_list.pop(0))) - except ValueError: - self.version_error_handler_3_x_try(None) - self.attempt_notify() - - def get_version(self): - self.notif.GetServerInfo( - reply_handler=self.version_reply_handler, - error_handler=self.version_error_handler_2_x_try) - - def version_error_handler_2_x_try(self, e): - self.notif.GetServerInformation( - reply_handler=self.version_reply_handler, - error_handler=self.version_error_handler_3_x_try) - - def version_error_handler_3_x_try(self, e): - self.version = self.default_version - self.attempt_notify()