Use GNotification instead of pynotify or dbus

This commit is contained in:
Yann Leboulanger 2017-08-30 20:43:50 +02:00
parent 8bc2ab096e
commit 083e3017ab
6 changed files with 50 additions and 414 deletions

View File

@ -228,3 +228,9 @@ class AppActions():
else: else:
interface.instances['logs'] = history_window.\ interface.instances['logs'] = history_window.\
HistoryWindow() HistoryWindow()
def on_open_event(self, action, param):
dict_ = param.unpack()
app.interface.handle_event(dict_['account'], dict_['jid'],
dict_['type_'])

View File

@ -74,7 +74,6 @@ class Config:
'autopopupaway': [ opt_bool, False ], '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') ], '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')], '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 ], 'showoffline': [ opt_bool, False ],
'show_only_chat_and_online': [ opt_bool, False, _('Show only online and free for chat contacts in roster.')], 'show_only_chat_and_online': [ opt_bool, False, _('Show only online and free for chat contacts in roster.')],
'show_transports_group': [ opt_bool, True ], 'show_transports_group': [ opt_bool, True ],

View File

@ -159,27 +159,6 @@ def get_interface(interface, path, start_service=True):
return None 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: if supported:
class MissingArgument(dbus.DBusException): class MissingArgument(dbus.DBusException):
_dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument' _dbus_error_name = _GAJIM_ERROR_IFACE + '.MissingArgument'

View File

@ -73,10 +73,6 @@ class FeaturesWindow:
_('Spellchecking of composed messages.'), _('Spellchecking of composed messages.'),
_('Requires libgtkspell.'), _('Requires libgtkspell.'),
_('Requires libgtkspell and libenchant.')), _('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, _('Automatic status'): (self.idle_available,
_('Ability to measure idle time, in order to set auto status.'), _('Ability to measure idle time, in order to set auto status.'),
_('Requires libxss library.'), _('Requires libxss library.'),
@ -199,18 +195,6 @@ class FeaturesWindow:
return False return False
return True 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): def idle_available(self):
from gajim.common import sleepy from gajim.common import sleepy
return sleepy.SUPPORTED return sleepy.SUPPORTED

View File

@ -339,7 +339,8 @@ class GajimApplication(Gtk.Application):
('-update-motd', action.on_update_motd, 'online', 's'), ('-update-motd', action.on_update_motd, 'online', 's'),
('-delete-motd', action.on_delete_motd, 'online', 's'), ('-delete-motd', action.on_delete_motd, 'online', 's'),
('-activate-bookmark', ('-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 = [ self.general_actions = [
@ -355,7 +356,7 @@ class GajimApplication(Gtk.Application):
('features', action.on_features), ('features', action.on_features),
('content', action.on_contents), ('content', action.on_contents),
('about', action.on_about), ('about', action.on_about),
('faq', action.on_faq) ('faq', action.on_faq),
] ]
for action in self.general_actions: for action in self.general_actions:

View File

@ -27,31 +27,16 @@
## along with Gajim. If not, see <http://www.gnu.org/licenses/>. ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
## ##
import os import sys
import time
from gajim.dialogs import PopupNotificationWindow from gajim.dialogs import PopupNotificationWindow
from gi.repository import GObject
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gio
from gajim import gtkgui_helpers from gajim import gtkgui_helpers
from gajim.common import app from gajim.common import app
from gajim.common import helpers from gajim.common import helpers
from gajim.common import ged 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): def get_show_in_roster(event, account, contact, session=None):
""" """
Return True if this event must be shown in roster, else False 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 False
return app.config.get('trayicon_notification_on_events') 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): text=None, timeout=-1):
""" """
Notify a user of an event. It first tries to a valid implementation of Notify a user of an event using GNotification and GApplication under linux,
the Desktop Notification Specification. If that fails, then we fall back to the older style PopupNotificationWindow method under windows
the older style PopupNotificationWindow method
""" """
# default image # default image
if not path_to_image: if not path_to_image:
@ -88,67 +72,44 @@ text=None, timeout=-1):
if timeout < 0: if timeout < 0:
timeout = app.config.get('notification_timeout') timeout = app.config.get('notification_timeout')
# Try to show our popup via D-Bus and notification daemon if sys.platform == 'win32':
if app.config.get('use_notif_daemon') and dbus_support.supported: instance = PopupNotificationWindow(event_type, jid, account, type_,
try: path_to_image, title, text, timeout)
DesktopNotification(event_type, jid, account, msg_type, app.interface.roster.popup_notification_windows.append(instance)
path_to_image, title, GLib.markup_escape_text(text), timeout) return
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))
# Ok, that failed. Let's try pynotify, which also uses notification daemon # use GNotification
if app.config.get('use_notif_daemon') and USER_HAS_PYNOTIFY: # TODO: Move to standard GTK+ icons here.
if not text and event_type == 'new_message': icon = Gio.FileIcon.new(Gio.File.new_for_path(path_to_image))
# empty text for new_message means do_preview = False notification = Gio.Notification()
# -> default value for text if title is not None:
_text = GLib.markup_escape_text(app.get_name_from_jid(account, notification.set_title(title)
jid)) if text is not None:
else: notification.set_body(text)
_text = GLib.markup_escape_text(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: class Notification:
""" """
@ -184,297 +145,3 @@ class Notification:
helpers.exec_command(obj.command, use_shell=True) helpers.exec_command(obj.command, use_shell=True)
except Exception: except Exception:
pass 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 = ('<html><img src="%(image)s" align=left />' \
'%(title)s<br/>%(text)s</html>') % {'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 = '<table style=\'padding: 3px\'><tr><td>' \
'<img src=\"%s\"></td><td width=20> </td>' \
'<td>%s</td></tr></table>' % (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()