root/branches/gajim_0.11.1/src/notify.py

Revision 8662, 18.4 kB (checked in by asterix, 12 months ago)

Use good funtion so we can see icon for pynotify notifs

Line 
1##      notify.py
2##
3## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
4## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
5## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com>
6##
7## Notification daemon connection via D-Bus code:
8## Copyright (C) 2005 by Sebastian Estienne
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20import os
21import time
22import dialogs
23import gtkgui_helpers
24
25from common import gajim
26from common import helpers
27
28from common import dbus_support
29if dbus_support.supported:
30        import dbus
31        import dbus.glib
32        import dbus.service
33
34
35USER_HAS_PYNOTIFY = True # user has pynotify module
36try:
37        import pynotify
38        pynotify.init('Gajim Notification')
39except ImportError:
40        USER_HAS_PYNOTIFY = False
41
42def get_show_in_roster(event, account, contact):
43        '''Return True if this event must be shown in roster, else False'''
44        num = get_advanced_notification(event, account, contact)
45        if num != None:
46                if gajim.config.get_per('notifications', str(num), 'roster') == 'yes':
47                        return True
48                if gajim.config.get_per('notifications', str(num), 'roster') == 'no':
49                        return False
50        if event == 'message_received':
51                chat_control = helpers.get_chat_control(account, contact)
52                if chat_control:
53                        return False
54        return True
55
56def get_show_in_systray(event, account, contact):
57        '''Return True if this event must be shown in systray, else False'''
58        num = get_advanced_notification(event, account, contact)
59        if num != None:
60                if gajim.config.get_per('notifications', str(num), 'systray') == 'yes':
61                        return True
62                if gajim.config.get_per('notifications', str(num), 'systray') == 'no':
63                        return False
64        return gajim.config.get('trayicon_notification_on_events')
65
66def get_advanced_notification(event, account, contact):
67        '''Returns the number of the first (top most)
68        advanced notification else None'''
69        num = 0
70        notif = gajim.config.get_per('notifications', str(num))
71        while notif:
72                recipient_ok = False
73                status_ok = False
74                tab_opened_ok = False
75                # test event
76                if gajim.config.get_per('notifications', str(num), 'event') == event:
77                        # test recipient
78                        recipient_type = gajim.config.get_per('notifications', str(num),
79                                'recipient_type')
80                        recipients = gajim.config.get_per('notifications', str(num),
81                                'recipients').split()
82                        if recipient_type == 'all':
83                                recipient_ok = True
84                        elif recipient_type == 'contact' and contact.jid in recipients:
85                                recipient_ok = True
86                        elif recipient_type == 'group':
87                                for group in contact.groups:
88                                        if group in contact.groups:
89                                                recipient_ok = True
90                                                break
91                if recipient_ok:
92                        # test status
93                        our_status = gajim.SHOW_LIST[gajim.connections[account].connected]
94                        status = gajim.config.get_per('notifications', str(num), 'status')
95                        if status == 'all' or our_status in status.split():
96                                status_ok = True
97                if status_ok:
98                        # test window_opened
99                        tab_opened = gajim.config.get_per('notifications', str(num),
100                                'tab_opened')
101                        if tab_opened == 'both':
102                                tab_opened_ok = True
103                        else:
104                                chat_control = helpers.get_chat_control(account, contact)
105                                if (chat_control and tab_opened == 'yes') or (not chat_control and \
106                                tab_opened == 'no'):
107                                        tab_opened_ok = True
108                if tab_opened_ok:
109                        return num
110
111                num += 1
112                notif = gajim.config.get_per('notifications', str(num))
113
114def notify(event, jid, account, parameters, advanced_notif_num = None):
115        '''Check what type of notifications we want, depending on basic
116        and the advanced configuration of notifications and do these notifications;
117        advanced_notif_num holds the number of the first (top most) advanced
118        notification'''
119        # First, find what notifications we want
120        do_popup = False
121        do_sound = False
122        do_cmd = False
123        if event == 'status_change':
124                new_show = parameters[0]
125                status_message = parameters[1]
126                # Default: No popup for status change
127        elif event == 'contact_connected':
128                status_message = parameters
129                j = gajim.get_jid_without_resource(jid)
130                server = gajim.get_server_from_jid(j)
131                account_server = account + '/' + server
132                block_transport = False
133                if account_server in gajim.block_signed_in_notifications and \
134                gajim.block_signed_in_notifications[account_server]:
135                        block_transport = True
136                if helpers.allow_showing_notification(account, 'notify_on_signin') and \
137                not gajim.block_signed_in_notifications[account] and not block_transport:
138                        do_popup = True
139                if gajim.config.get_per('soundevents', 'contact_connected',
140                'enabled') and not gajim.block_signed_in_notifications[account] and \
141                not block_transport:
142                        do_sound = True
143        elif event == 'contact_disconnected':
144                status_message = parameters
145                if helpers.allow_showing_notification(account, 'notify_on_signout'):
146                        do_popup = True
147                if gajim.config.get_per('soundevents', 'contact_disconnected',
148                        'enabled'):
149                        do_sound = True
150        elif event == 'new_message':
151                message_type = parameters[0]
152                is_first_message = parameters[1]
153                nickname = parameters[2]
154                message = parameters[3]
155                if helpers.allow_showing_notification(account, 'notify_on_new_message',
156                advanced_notif_num, is_first_message):
157                        do_popup = True
158                if is_first_message and helpers.allow_sound_notification(
159                'first_message_received', advanced_notif_num):
160                        do_sound = True
161                elif not is_first_message and helpers.allow_sound_notification(
162                'next_message_received', advanced_notif_num):
163                        do_sound = True
164        else:
165                print '*Event not implemeted yet*'
166
167        if advanced_notif_num is not None and gajim.config.get_per('notifications',
168        str(advanced_notif_num), 'run_command'):
169                do_cmd = True
170                       
171        # Do the wanted notifications   
172        if do_popup:
173                if event in ('contact_connected', 'contact_disconnected',
174                'status_change'): # Common code for popup for these three events
175                        if event == 'contact_disconnected':
176                                show_image = 'offline.png'
177                                suffix = '_notif_size_bw.png'
178                        else: #Status Change or Connected
179                                # FIXME: for status change,
180                                # we don't always 'online.png', but we
181                                # first need 48x48 for all status
182                                show_image = 'online.png'
183                                suffix = '_notif_size_colored.png'     
184                        transport_name = gajim.get_transport_name_from_jid(jid)
185                        img = None
186                        if transport_name:
187                                img = os.path.join(gajim.DATA_DIR, 'iconsets',
188                                        'transports', transport_name, '48x48', show_image)
189                        if not img or not os.path.isfile(img):
190                                iconset = gajim.config.get('iconset')
191                                img = os.path.join(gajim.DATA_DIR, 'iconsets',
192                                                iconset, '48x48', show_image)
193                        path = gtkgui_helpers.get_path_to_generic_or_avatar(img,
194                                jid = jid, suffix = suffix)
195                        if event == 'status_change':
196                                title = _('%(nick)s Changed Status') % \
197                                        {'nick': gajim.get_name_from_jid(account, jid)}
198                                text = _('%(nick)s is now %(status)s') % \
199                                        {'nick': gajim.get_name_from_jid(account, jid),\
200                                        'status': helpers.get_uf_show(gajim.SHOW_LIST[new_show])}
201                                if status_message:
202                                        text =  text + " : " + status_message
203                                popup(_('Contact Changed Status'), jid, account,
204                                        path_to_image = path, title = title, text = text)
205                        elif event == 'contact_connected':
206                                title = _('%(nickname)s Signed In') % \
207                                        {'nickname': gajim.get_name_from_jid(account, jid)}
208                                text = ''
209                                if status_message:
210                                        text = status_message
211                                popup(_('Contact Signed In'), jid, account,
212                                        path_to_image = path, title = title, text = text)
213                        elif event == 'contact_disconnected':
214                                title = _('%(nickname)s Signed Out') % \
215                                        {'nickname': gajim.get_name_from_jid(account, jid)}
216                                text = ''
217                                if status_message:
218                                        text = status_message
219                                popup(_('Contact Signed Out'), jid, account,
220                                        path_to_image = path, title = title, text = text)
221                elif event == 'new_message':
222                        if message_type == 'normal': # single message
223                                event_type = _('New Single Message')
224                                img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
225                                        'single_msg_recv.png')
226                                title = _('New Single Message from %(nickname)s') % \
227                                        {'nickname': nickname}
228                                text = message
229                        elif message_type == 'pm': # private message
230                                event_type = _('New Private Message')
231                                room_name = gajim.get_nick_from_jid(jid)
232                                img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
233                                        'priv_msg_recv.png')
234                                title = _('New Private Message from group chat %s') % room_name
235                                text = _('%(nickname)s: %(message)s') % {'nickname': nickname,
236                                        'message': message}
237                        else: # chat message
238                                event_type = _('New Message')
239                                img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
240                                        'chat_msg_recv.png')
241                                title = _('New Message from %(nickname)s') % \
242                                        {'nickname': nickname}
243                                text = message
244                        path = gtkgui_helpers.get_path_to_generic_or_avatar(img)
245                        popup(event_type, jid, account, message_type,
246                                path_to_image = path, title = title, text = text)
247
248        if do_sound:
249                snd_file = None
250                snd_event = None # If not snd_file, play the event
251                if event == 'new_message':
252                        if advanced_notif_num is not None and gajim.config.get_per(
253                        'notifications', str(advanced_notif_num), 'sound') == 'yes':
254                                snd_file = gajim.config.get_per('notifications',
255                                        str(advanced_notif_num), 'sound_file')
256                        elif advanced_notif_num is not None and gajim.config.get_per(
257                        'notifications', str(advanced_notif_num), 'sound') == 'no':
258                                pass # do not set snd_event
259                        elif is_first_message:
260                                snd_event = 'first_message_received'
261                        else:
262                                snd_event = 'next_message_received'
263                elif event in ('contact_connected', 'contact_disconnected'):
264                        snd_event = event
265                if snd_file:
266                        helpers.play_sound_file(snd_file)
267                if snd_event:
268                        helpers.play_sound(snd_event)
269
270        if do_cmd:
271                command = gajim.config.get_per('notifications', str(advanced_notif_num),
272                        'command')
273                try:
274                        helpers.exec_command(command)
275                except:
276                        pass
277
278def popup(event_type, jid, account, msg_type = '', path_to_image = None,
279        title = None, text = None):
280        '''Notifies a user of an event. It first tries to a valid implementation of
281        the Desktop Notification Specification. If that fails, then we fall back to
282        the older style PopupNotificationWindow method.'''
283        text = gtkgui_helpers.escape_for_pango_markup(text)
284        title = gtkgui_helpers.escape_for_pango_markup(title)
285
286        if gajim.config.get('use_notif_daemon') and dbus_support.supported:
287                try:
288                        DesktopNotification(event_type, jid, account, msg_type,
289                                path_to_image, title, text)
290                        return  # sucessfully did D-Bus Notification procedure!
291                except dbus.DBusException, e:
292                        # Connection to D-Bus failed
293                        gajim.log.debug(str(e))
294                except TypeError, e:
295                        # This means that we sent the message incorrectly
296                        gajim.log.debug(str(e))
297        # we failed to speak to notification daemon via D-Bus
298        if USER_HAS_PYNOTIFY: # try via libnotify
299                if not text:
300                        text = gajim.get_name_from_jid(account, jid) # default value of text
301                if not title:
302                        title = event_type
303                # default image
304                if not path_to_image:
305                        path_to_image = os.path.abspath(
306                                os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
307                                        'chat_msg_recv.png')) # img to display
308               
309               
310                notification = pynotify.Notification(title, text)
311                timeout = gajim.config.get('notification_timeout') * 1000 # make it ms
312                notification.set_timeout(timeout)
313               
314                notification.set_category(event_type)
315                notification.set_data('event_type', event_type)
316                notification.set_data('jid', jid)
317                notification.set_data('account', account)
318                notification.set_data('msg_type', event_type)
319                notification.set_property('icon-name', path_to_image)
320                notification.add_action('default', 'Default Action',
321                        on_pynotify_notification_clicked)
322
323                try:
324                        notification.show()
325                        return
326                except gobject.GError, e:
327                        # Connection to notification-daemon failed, see #2893
328                        gajim.log.debug(str(e))
329
330        # go old style
331        instance = dialogs.PopupNotificationWindow(event_type, jid, account,
332                msg_type, path_to_image, title, text)
333        gajim.interface.roster.popup_notification_windows.append(instance)
334
335def on_pynotify_notification_clicked(notification, action):
336        jid = notification.get_data('jid')
337        account = notification.get_data('account')
338        msg_type = notification.get_data('msg_type')
339
340        notification.close()
341        gajim.interface.handle_event(account, jid, msg_type)
342
343class NotificationResponseManager:
344        '''Collects references to pending DesktopNotifications and manages there
345        signalling. This is necessary due to a bug in DBus where you can't remove
346        a signal from an interface once it's connected.'''
347        def __init__(self):
348                self.pending = {}
349                self.received = []
350                self.interface = None
351
352        def attach_to_interface(self):
353                if self.interface is not None:
354                        return
355                self.interface = dbus_support.get_notifications_interface()
356                self.interface.connect_to_signal('ActionInvoked', self.on_action_invoked)
357                self.interface.connect_to_signal('NotificationClosed', self.on_closed)
358
359        def on_action_invoked(self, id, reason):
360                self.received.append((id, time.time(), reason))
361                if self.pending.has_key(id):
362                        notification = self.pending[id]
363                        notification.on_action_invoked(id, reason)
364                        del self.pending[id]
365                if len(self.received) > 20:
366                        curt = time.time()
367                        for rec in self.received:
368                                diff = curt - rec[1]
369                                if diff > 10:
370                                        self.received.remove(rec)
371
372        def on_closed(self, id, reason = None):
373                if self.pending.has_key(id):
374                        del self.pending[id]
375
376        def add_pending(self, id, object):
377                # Check to make sure that we handle an event immediately if we're adding
378                # an id that's already been triggered
379                for rec in self.received:
380                        if rec[0] == id:
381                                object.on_action_invoked(id, rec[2])
382                                self.received.remove(rec)
383                                return
384                if id not in self.pending:
385                        # Add it
386                        self.pending[id] = object
387                else:
388                        # We've triggered an event that has a duplicate ID!
389                        gajim.log.debug('Duplicate ID of notification. Can\'t handle this.')
390
391notification_response_manager = NotificationResponseManager()
392
393class DesktopNotification:
394        '''A DesktopNotification that interfaces with D-Bus via the Desktop
395        Notification specification'''
396        def __init__(self, event_type, jid, account, msg_type = '',
397                path_to_image = None, title = None, text = None):
398                self.path_to_image = path_to_image
399                self.event_type = event_type
400                self.title = title
401                self.text = text
402                '''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.'''
403                self.default_version = [0, 3, 1]
404                self.account = account
405                self.jid = jid
406                self.msg_type = msg_type
407
408                if not text:
409                        # default value of text
410                        self.text = gajim.get_name_from_jid(account, jid)
411
412                if not title:
413                        self.title = event_type # default value
414
415                if event_type == _('Contact Signed In'):
416                        ntype = 'presence.online'
417                elif event_type == _('Contact Signed Out'):
418                        ntype = 'presence.offline'
419                elif event_type in (_('New Message'), _('New Single Message'),
420                        _('New Private Message')):
421                        ntype = 'im.received'
422                elif event_type == _('File Transfer Request'):
423                        ntype = 'transfer'
424                elif event_type == _('File Transfer Error'):
425                        ntype = 'transfer.error'
426                elif event_type in (_('File Transfer Completed'), _('File Transfer Stopped')):
427                        ntype = 'transfer.complete'
428                elif event_type == _('New E-mail'):
429                        ntype = 'email.arrived'
430                elif event_type == _('Groupchat Invitation'):
431                        ntype = 'im.invitation'
432                elif event_type == _('Contact Changed Status'):
433                        ntype = 'presence.status'
434                elif event_type == _('Connection Failed'):
435                        ntype = 'connection.failed'
436                else:
437                        # default failsafe values
438                        self.path_to_image = os.path.abspath(
439                                os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
440                                        'chat_msg_recv.png')) # img to display
441                        ntype = 'im' # Notification Type
442
443                self.notif = dbus_support.get_notifications_interface()
444                if self.notif is None:
445                        raise dbus.DBusException('unable to get notifications interface')
446                self.ntype = ntype
447
448                self.get_version()
449
450        def attempt_notify(self):
451                version = self.version
452                timeout = gajim.config.get('notification_timeout') # in seconds
453                ntype = self.ntype
454                if version[:2] == [0, 2]:
455                        try:
456                                self.notif.Notify(
457                                        dbus.String(_('Gajim')),
458                                        dbus.String(self.path_to_image),
459                                        dbus.UInt32(0),
460                                        ntype,
461                                        dbus.Byte(0),
462                                        dbus.String(self.title),
463                                        dbus.String(self.text),
464                                        [dbus.String(self.path_to_image)],
465                                        {'default': 0},
466                                        [''],
467                                        True,
468                                        dbus.UInt32(timeout),
469                                        reply_handler=self.attach_by_id,
470                                        error_handler=self.notify_another_way)
471                        except AttributeError:
472                                version = [0, 3, 1] # we're actually dealing with the newer version
473                if version > [0, 3]:
474                        if version >= [0, 3, 2]:
475                                hints = {}
476                                hints['urgency'] = dbus.Byte(0) # Low Urgency
477                                hints['category'] = dbus.String(ntype)
478                                self.notif.Notify(
479                                        dbus.String(_('Gajim')),
480                                        dbus.UInt32(0), # this notification does not replace other
481                                        dbus.String(self.path_to_image),
482                                        dbus.String(self.title),
483                                        dbus.String(self.text),
484                                        ( dbus.String('default'), dbus.String(self.event_type) ),
485                                        hints,
486                                        dbus.UInt32(timeout*1000),
487                                        reply_handler=self.attach_by_id,
488                                        error_handler=self.notify_another_way)
489                        else:
490                                self.notif.Notify(
491                                        dbus.String(_('Gajim')),
492                                        dbus.String(self.path_to_image),
493                                        dbus.UInt32(0),
494                                        dbus.String(self.title),
495                                        dbus.String(self.text),
496                                        dbus.String(''),
497                                        {},
498                                        dbus.UInt32(timeout*1000),
499                                        reply_handler=self.attach_by_id,
500                                        error_handler=self.notify_another_way)
501
502        def attach_by_id(self, id):
503                self.id = id
504                notification_response_manager.attach_to_interface()
505                notification_response_manager.add_pending(self.id, self)
506
507        def notify_another_way(self,e):
508                gajim.log.debug(str(e))
509                gajim.log.debug('Need to implement a new way of falling back')
510
511        def on_action_invoked(self, id, reason):
512                if self.notif is None:
513                        return
514                self.notif.CloseNotification(dbus.UInt32(id))
515                self.notif = None
516                if not self.msg_type:
517                        self.msg_type = 'chat'
518                gajim.interface.handle_event(self.account, self.jid, self.msg_type)
519
520        def version_reply_handler(self, name, vendor, version, spec_version = None):
521                version_list = version.split('.')
522                self.version = []
523                while len(version_list):
524                        self.version.append(int(version_list.pop(0)))
525                self.attempt_notify()
526
527        def get_version(self):
528                self.notif.GetServerInfo(
529                        reply_handler=self.version_reply_handler,
530                        error_handler=self.version_error_handler_2_x_try)
531
532        def version_error_handler_2_x_try(self, e):
533                self.notif.GetServerInformation(reply_handler=self.version_reply_handler,
534                        error_handler=self.version_error_handler_3_x_try)
535
536        def version_error_handler_3_x_try(self, e):
537                self.version = self.default_version
538                self.attempt_notify()
Note: See TracBrowser for help on using the browser.