4 ## Copyright (C) 2005 Sebastian Estienne
5 ## Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com>
6 ## Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2005-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
9 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10 ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
11 ## Stephan Erb <steve-e AT h3c.de>
12 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
13 ## Jonathan Schleifer <js-gajim AT webkeks.org>
15 ## This file is part of Gajim.
17 ## Gajim is free software; you can redistribute it and/or modify
18 ## it under the terms of the GNU General Public License as published
19 ## by the Free Software Foundation; version 3 only.
21 ## Gajim is distributed in the hope that it will be useful,
22 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ## GNU General Public License for more details.
26 ## You should have received a copy of the GNU General Public License
27 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
32 from dialogs
import PopupNotificationWindow
37 from common
import gajim
38 from common
import helpers
39 from common
import ged
41 from common
import dbus_support
42 if dbus_support
.supported
:
47 USER_HAS_PYNOTIFY
= True # user has pynotify module
50 pynotify
.init('Gajim Notification')
52 USER_HAS_PYNOTIFY
= False
54 def get_show_in_roster(event
, account
, contact
, session
=None):
56 Return True if this event must be shown in roster, else False
58 if event
== 'gc_message_received':
60 if event
== 'message_received':
61 if session
and session
.control
:
65 def get_show_in_systray(event
, account
, contact
, type_
=None):
67 Return True if this event must be shown in systray, else False
69 if type_
== 'printed_gc_msg' and not gajim
.config
.get(
70 'notify_on_all_muc_messages'):
71 # it's not an highlighted message, don't show in systray
73 return gajim
.config
.get('trayicon_notification_on_events')
75 def popup(event_type
, jid
, account
, msg_type
='', path_to_image
=None, title
=None,
78 Notify a user of an event. It first tries to a valid implementation of
79 the Desktop Notification Specification. If that fails, then we fall back to
80 the older style PopupNotificationWindow method
84 path_to_image
= gtkgui_helpers
.get_icon_path('gajim-chat_msg_recv', 48)
86 # Try to show our popup via D-Bus and notification daemon
87 if gajim
.config
.get('use_notif_daemon') and dbus_support
.supported
:
89 DesktopNotification(event_type
, jid
, account
, msg_type
,
90 path_to_image
, title
, gobject
.markup_escape_text(text
))
91 return # sucessfully did D-Bus Notification procedure!
92 except dbus
.DBusException
, e
:
93 # Connection to D-Bus failed
94 gajim
.log
.debug(str(e
))
96 # This means that we sent the message incorrectly
97 gajim
.log
.debug(str(e
))
99 # Ok, that failed. Let's try pynotify, which also uses notification daemon
100 if gajim
.config
.get('use_notif_daemon') and USER_HAS_PYNOTIFY
:
101 if not text
and event_type
== 'new_message':
102 # empty text for new_message means do_preview = False
103 # -> default value for text
104 _text
= gobject
.markup_escape_text(
105 gajim
.get_name_from_jid(account
, jid
))
107 _text
= gobject
.markup_escape_text(text
)
114 notification
= pynotify
.Notification(_title
, _text
)
115 timeout
= gajim
.config
.get('notification_timeout') * 1000 # make it ms
116 notification
.set_timeout(timeout
)
118 notification
.set_category(event_type
)
119 notification
.set_data('event_type', event_type
)
120 notification
.set_data('jid', jid
)
121 notification
.set_data('account', account
)
122 notification
.set_data('msg_type', msg_type
)
123 notification
.set_property('icon-name', path_to_image
)
124 if 'actions' in pynotify
.get_server_caps():
125 notification
.add_action('default', 'Default Action',
126 on_pynotify_notification_clicked
)
131 except gobject
.GError
, e
:
132 # Connection to notification-daemon failed, see #2893
133 gajim
.log
.debug(str(e
))
135 # Either nothing succeeded or the user wants old-style notifications
136 instance
= PopupNotificationWindow(event_type
, jid
, account
, msg_type
,
137 path_to_image
, title
, text
)
138 gajim
.interface
.roster
.popup_notification_windows
.append(instance
)
140 def on_pynotify_notification_clicked(notification
, action
):
141 jid
= notification
.get_data('jid')
142 account
= notification
.get_data('account')
143 msg_type
= notification
.get_data('msg_type')
146 gajim
.interface
.handle_event(account
, jid
, msg_type
)
153 gajim
.ged
.register_event_handler('notification', ged
.GUI2
,
154 self
._nec
_notification
)
156 def _nec_notification(self
, obj
):
158 popup(obj
.popup_event_type
, obj
.jid
, obj
.conn
.name
,
159 obj
.popup_msg_type
, path_to_image
=obj
.popup_image
,
160 title
=obj
.popup_title
, text
=obj
.popup_text
)
164 helpers
.play_sound_file(obj
.sound_file
)
165 elif obj
.sound_event
:
166 helpers
.play_sound(obj
.sound_event
)
170 helpers
.exec_command(obj
.command
)
174 class NotificationResponseManager
:
176 Collect references to pending DesktopNotifications and manages there
177 signalling. This is necessary due to a bug in DBus where you can't remove a
178 signal from an interface once it's connected
184 self
.interface
= None
186 def attach_to_interface(self
):
187 if self
.interface
is not None:
189 self
.interface
= dbus_support
.get_notifications_interface()
190 self
.interface
.connect_to_signal('ActionInvoked',
191 self
.on_action_invoked
)
192 self
.interface
.connect_to_signal('NotificationClosed', self
.on_closed
)
194 def on_action_invoked(self
, id_
, reason
):
195 self
.received
.append((id_
, time
.time(), reason
))
196 if id_
in self
.pending
:
197 notification
= self
.pending
[id_
]
198 notification
.on_action_invoked(id_
, reason
)
199 del self
.pending
[id_
]
200 if len(self
.received
) > 20:
202 for rec
in self
.received
:
205 self
.received
.remove(rec
)
207 def on_closed(self
, id_
, reason
=None):
208 if id_
in self
.pending
:
209 del self
.pending
[id_
]
211 def add_pending(self
, id_
, object_
):
212 # Check to make sure that we handle an event immediately if we're adding
213 # an id that's already been triggered
214 for rec
in self
.received
:
216 object_
.on_action_invoked(id_
, rec
[2])
217 self
.received
.remove(rec
)
219 if id_
not in self
.pending
:
221 self
.pending
[id_
] = object_
223 # We've triggered an event that has a duplicate ID!
224 gajim
.log
.debug('Duplicate ID of notification. Can\'t handle this.')
226 notification_response_manager
= NotificationResponseManager()
228 class DesktopNotification
:
230 A DesktopNotification that interfaces with D-Bus via the Desktop
231 Notification Specification
234 def __init__(self
, event_type
, jid
, account
, msg_type
='',
235 path_to_image
=None, title
=None, text
=None):
236 self
.path_to_image
= path_to_image
237 self
.event_type
= event_type
240 # 0.3.1 is the only version of notification daemon that has no way
241 # to determine which version it is. If no method exists, it means
242 # they're using that one.
243 self
.default_version
= [0, 3, 1]
244 self
.account
= account
246 self
.msg_type
= msg_type
248 # default value of text
249 if not text
and event_type
== 'new_message':
250 # empty text for new_message means do_preview = False
251 self
.text
= gajim
.get_name_from_jid(account
, jid
)
254 self
.title
= event_type
# default value
256 if event_type
== _('Contact Signed In'):
257 ntype
= 'presence.online'
258 elif event_type
== _('Contact Signed Out'):
259 ntype
= 'presence.offline'
260 elif event_type
in (_('New Message'), _('New Single Message'),
261 _('New Private Message')):
262 ntype
= 'im.received'
263 elif event_type
== _('File Transfer Request'):
265 elif event_type
== _('File Transfer Error'):
266 ntype
= 'transfer.error'
267 elif event_type
in (_('File Transfer Completed'),
268 _('File Transfer Stopped')):
269 ntype
= 'transfer.complete'
270 elif event_type
== _('New E-mail'):
271 ntype
= 'email.arrived'
272 elif event_type
== _('Groupchat Invitation'):
273 ntype
= 'im.invitation'
274 elif event_type
== _('Contact Changed Status'):
275 ntype
= 'presence.status'
276 elif event_type
== _('Connection Failed'):
277 ntype
= 'connection.failed'
278 elif event_type
== _('Subscription request'):
279 ntype
= 'subscription.request'
280 elif event_type
== _('Unsubscribed'):
281 ntype
= 'unsubscribed'
283 # default failsafe values
284 self
.path_to_image
= gtkgui_helpers
.get_icon_path(
285 'gajim-chat_msg_recv', 48)
286 ntype
= 'im' # Notification Type
288 self
.notif
= dbus_support
.get_notifications_interface(self
)
289 if self
.notif
is None:
290 raise dbus
.DBusException('unable to get notifications interface')
293 if self
.kde_notifications
:
294 self
.attempt_notify()
296 self
.capabilities
= self
.notif
.GetCapabilities()
297 if self
.capabilities
is None:
298 self
.capabilities
= ['actions']
301 def attempt_notify(self
):
302 timeout
= gajim
.config
.get('notification_timeout') # in seconds
304 if self
.kde_notifications
:
305 notification_text
= ('<html><img src="%(image)s" align=left />' \
306 '%(title)s<br/>%(text)s</html>') % {'title': self
.title
,
307 'text': self
.text
, 'image': self
.path_to_image
}
308 gajim_icon
= gtkgui_helpers
.get_icon_path('gajim', 48)
311 dbus
.String(_('Gajim')), # app_name (string)
312 dbus
.UInt32(0), # replaces_id (uint)
313 ntype
, # event_id (string)
314 dbus
.String(gajim_icon
), # app_icon (string)
315 dbus
.String(''), # summary (string)
316 dbus
.String(notification_text
), # body (string)
317 # actions (stringlist)
318 (dbus
.String('default'), dbus
.String(self
.event_type
),
319 dbus
.String('ignore'), dbus
.String(_('Ignore'))),
320 [], # hints (not used in KDE yet)
321 dbus
.UInt32(timeout
*1000), # timeout (int), in ms
322 reply_handler
=self
.attach_by_id
,
323 error_handler
=self
.notify_another_way
)
327 version
= self
.version
328 if version
[:2] == [0, 2]:
330 if 'actions' in self
.capabilities
:
331 actions
= {'default': 0}
334 dbus
.String(_('Gajim')),
335 dbus
.String(self
.path_to_image
),
339 dbus
.String(self
.title
),
340 dbus
.String(self
.text
),
341 [dbus
.String(self
.path_to_image
)],
345 dbus
.UInt32(timeout
),
346 reply_handler
=self
.attach_by_id
,
347 error_handler
=self
.notify_another_way
)
348 except AttributeError:
349 # we're actually dealing with the newer version
352 if gajim
.interface
.systray_enabled
and \
353 gajim
.config
.get('attach_notifications_to_systray'):
354 status_icon
= gajim
.interface
.systray
.status_icon
355 x
, y
, width
, height
= status_icon
.get_geometry()[1]
356 pos_x
= x
+ (width
/ 2)
357 pos_y
= y
+ (height
/ 2)
358 hints
= {'x': pos_x
, 'y': pos_y
}
361 if version
>= [0, 3, 2]:
362 hints
['urgency'] = dbus
.Byte(0) # Low Urgency
363 hints
['category'] = dbus
.String(ntype
)
364 # it seems notification-daemon doesn't like empty text
369 if os
.environ
.get('KDE_FULL_SESSION') == 'true':
370 self
.path_to_image
= os
.path
.abspath(self
.path_to_image
)
371 text
= '<table style=\'padding: 3px\'><tr><td>' \
372 '<img src=\"%s\"></td><td width=20> </td>' \
373 '<td>%s</td></tr></table>' % (self
.path_to_image
,
375 self
.path_to_image
= os
.path
.abspath(
376 gtkgui_helpers
.get_icon_path('gajim', 48))
378 if 'actions' in self
.capabilities
:
379 actions
= (dbus
.String('default'), dbus
.String(
383 dbus
.String(_('Gajim')),
384 # this notification does not replace other
386 dbus
.String(self
.path_to_image
),
387 dbus
.String(self
.title
),
391 dbus
.UInt32(timeout
*1000),
392 reply_handler
=self
.attach_by_id
,
393 error_handler
=self
.notify_another_way
)
395 self
.notify_another_way(e
)
399 dbus
.String(_('Gajim')),
400 dbus
.String(self
.path_to_image
),
402 dbus
.String(self
.title
),
403 dbus
.String(self
.text
),
406 dbus
.UInt32(timeout
*1000),
407 reply_handler
=self
.attach_by_id
,
408 error_handler
=self
.notify_another_way
)
410 self
.notify_another_way(e
)
412 def attach_by_id(self
, id_
):
414 notification_response_manager
.attach_to_interface()
415 notification_response_manager
.add_pending(self
.id, self
)
417 def notify_another_way(self
, e
):
418 gajim
.log
.debug('Error when trying to use notification daemon: %s' % \
420 instance
= PopupNotificationWindow(self
.event_type
, self
.jid
,
421 self
.account
, self
.msg_type
, self
.path_to_image
, self
.title
,
423 gajim
.interface
.roster
.popup_notification_windows
.append(instance
)
425 def on_action_invoked(self
, id_
, reason
):
426 if self
.notif
is None:
428 self
.notif
.CloseNotification(dbus
.UInt32(id_
))
431 if reason
== 'ignore':
434 gajim
.interface
.handle_event(self
.account
, self
.jid
, self
.msg_type
)
436 def version_reply_handler(self
, name
, vendor
, version
, spec_version
=None):
438 version
= spec_version
439 elif vendor
== 'Xfce' and version
.startswith('0.1.0'):
441 version_list
= version
.split('.')
444 while len(version_list
):
445 self
.version
.append(int(version_list
.pop(0)))
447 self
.version_error_handler_3_x_try(None)
448 self
.attempt_notify()
450 def get_version(self
):
451 self
.notif
.GetServerInfo(
452 reply_handler
=self
.version_reply_handler
,
453 error_handler
=self
.version_error_handler_2_x_try
)
455 def version_error_handler_2_x_try(self
, e
):
456 self
.notif
.GetServerInformation(
457 reply_handler
=self
.version_reply_handler
,
458 error_handler
=self
.version_error_handler_3_x_try
)
460 def version_error_handler_3_x_try(self
, e
):
461 self
.version
= self
.default_version
462 self
.attempt_notify()