Welcome to mirror list, hosted at ThFree Co, Russian Federation.

notify.py « src - dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 09c3ad82b8b77c27a76dc180ffa988fbb1c671fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
##	notify.py
##
## Copyright (C) 2005-2006 Yann Leboulanger <asterix@lagaule.org>
## Copyright (C) 2005-2007 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com>
## Copyright (C) 2007 Stephan Erb <steve-e@h3c.de> 
##
## Notification daemon connection via D-Bus code:
## Copyright (C) 2005 by Sebastian Estienne
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##

import os
import time
import dialogs
import gobject
import gtkgui_helpers

from common import gajim
from common import helpers

from common import dbus_support
if dbus_support.supported:
	import dbus
	import dbus.glib
	import dbus.service


USER_HAS_PYNOTIFY = True # user has pynotify module
try:
	import pynotify
	pynotify.init('Gajim Notification')
except ImportError:
	USER_HAS_PYNOTIFY = False

USER_HAS_GROWL = True
try:
	import osx.growler
	osx.growler.init()
except:
	USER_HAS_GROWL = False

def get_show_in_roster(event, account, contact, session = None):
	'''Return True if this event must be shown in roster, else False'''
	if event == 'gc_message_received':
		return True
	num = get_advanced_notification(event, account, contact)
	if num is not None:
		if gajim.config.get_per('notifications', str(num), 'roster') == 'yes':
			return True
		if gajim.config.get_per('notifications', str(num), 'roster') == 'no':
			return False
	if event == 'message_received':
		if session and session.control:
			return False
	return True

def get_show_in_systray(event, account, contact, type_=None):
	'''Return True if this event must be shown in systray, else False'''
	num = get_advanced_notification(event, account, contact)
	if num is not None:
		if gajim.config.get_per('notifications', str(num), 'systray') == 'yes':
			return True
		if gajim.config.get_per('notifications', str(num), 'systray') == 'no':
			return False
	if type_ == 'printed_gc_msg' and not gajim.config.get(
	'notify_on_all_muc_messages'):
		# it's not an highlighted message, don't show in systray
		return False
	return gajim.config.get('trayicon_notification_on_events')

def get_advanced_notification(event, account, contact):
	'''Returns the number of the first (top most)
	advanced notification else None'''
	num = 0
	notif = gajim.config.get_per('notifications', str(num))
	while notif:
		recipient_ok = False
		status_ok = False
		tab_opened_ok = False
		# test event
		if gajim.config.get_per('notifications', str(num), 'event') == event:
			# test recipient
			recipient_type = gajim.config.get_per('notifications', str(num),
				'recipient_type')
			recipients = gajim.config.get_per('notifications', str(num),
				'recipients').split()
			if recipient_type == 'all':
				recipient_ok = True
			elif recipient_type == 'contact' and contact.jid in recipients:
				recipient_ok = True
			elif recipient_type == 'group':
				for group in contact.groups:
					if group in contact.groups:
						recipient_ok = True
						break
		if recipient_ok:
			# test status
			our_status = gajim.SHOW_LIST[gajim.connections[account].connected]
			status = gajim.config.get_per('notifications', str(num), 'status')
			if status == 'all' or our_status in status.split():
				status_ok = True
		if status_ok:
			# test window_opened
			tab_opened = gajim.config.get_per('notifications', str(num),
				'tab_opened')
			if tab_opened == 'both':
				tab_opened_ok = True
			else:
				chat_control = helpers.get_chat_control(account, contact)
				if (chat_control and tab_opened == 'yes') or (not chat_control and \
				tab_opened == 'no'):
					tab_opened_ok = True
		if tab_opened_ok:
			return num

		num += 1
		notif = gajim.config.get_per('notifications', str(num))

def notify(event, jid, account, parameters, advanced_notif_num = None):
	'''Check what type of notifications we want, depending on basic
	and the advanced configuration of notifications and do these notifications;
	advanced_notif_num holds the number of the first (top most) advanced
	notification'''
	# First, find what notifications we want
	do_popup = False
	do_sound = False
	do_cmd = False
	if event == 'status_change':
		new_show = parameters[0]
		status_message = parameters[1]
		# Default: No popup for status change
	elif event == 'contact_connected':
		status_message = parameters
		j = gajim.get_jid_without_resource(jid)
		server = gajim.get_server_from_jid(j)
		account_server = account + '/' + server
		block_transport = False
		if account_server in gajim.block_signed_in_notifications and \
		gajim.block_signed_in_notifications[account_server]:
			block_transport = True
		if helpers.allow_showing_notification(account, 'notify_on_signin') and \
		not gajim.block_signed_in_notifications[account] and not block_transport:
			do_popup = True
		if gajim.config.get_per('soundevents', 'contact_connected',
		'enabled') and not gajim.block_signed_in_notifications[account] and \
		not block_transport:
			do_sound = True
	elif event == 'contact_disconnected':
		status_message = parameters
		if helpers.allow_showing_notification(account, 'notify_on_signout'):
			do_popup = True
		if gajim.config.get_per('soundevents', 'contact_disconnected',
			'enabled'):
			do_sound = True
	elif event == 'new_message':
		message_type = parameters[0]
		is_first_message = parameters[1]
		nickname = parameters[2]
		if gajim.config.get('notification_preview_message'):
			message = parameters[3]
			if message.startswith('/me ') or message.startswith('/me\n'):
				message = '* ' + nickname + message[3:]
		else:
			# We don't want message preview, do_preview = False
			message = ''
		focused = parameters[4]
		if helpers.allow_showing_notification(account, 'notify_on_new_message',
		advanced_notif_num, is_first_message):
			do_popup = True
		if is_first_message and helpers.allow_sound_notification(
		'first_message_received', advanced_notif_num):
			do_sound = True
		elif not is_first_message and focused and \
		helpers.allow_sound_notification('next_message_received_focused',
		advanced_notif_num):
			do_sound = True
		elif not is_first_message and not focused and \
		helpers.allow_sound_notification('next_message_received_unfocused',
		advanced_notif_num):
			do_sound = True
	else:
		print '*Event not implemeted yet*'

	if advanced_notif_num is not None and gajim.config.get_per('notifications',
	str(advanced_notif_num), 'run_command'):
		do_cmd = True

	# Do the wanted notifications
	if do_popup:
		if event in ('contact_connected', 'contact_disconnected',
		'status_change'): # Common code for popup for these three events
			if event == 'contact_disconnected':
				show_image = 'offline.png'
				suffix = '_notif_size_bw'
			else: #Status Change or Connected
				# FIXME: for status change,
				# we don't always 'online.png', but we
				# first need 48x48 for all status
				show_image = 'online.png'
				suffix = '_notif_size_colored'
			transport_name = gajim.get_transport_name_from_jid(jid)
			img = None
			if transport_name:
				img = os.path.join(helpers.get_transport_path(transport_name),
					'48x48', show_image)
			if not img or not os.path.isfile(img):
				iconset = gajim.config.get('iconset')
				img = os.path.join(helpers.get_iconset_path(iconset), '48x48',
					show_image)
			path = gtkgui_helpers.get_path_to_generic_or_avatar(img,
				jid = jid, suffix = suffix)
			if event == 'status_change':
				title = _('%(nick)s Changed Status') % \
					{'nick': gajim.get_name_from_jid(account, jid)}
				text = _('%(nick)s is now %(status)s') % \
					{'nick': gajim.get_name_from_jid(account, jid),\
					'status': helpers.get_uf_show(gajim.SHOW_LIST[new_show])}
				if status_message:
					text = text + " : " + status_message
				popup(_('Contact Changed Status'), jid, account,
					path_to_image = path, title = title, text = text)
			elif event == 'contact_connected':
				title = _('%(nickname)s Signed In') % \
					{'nickname': gajim.get_name_from_jid(account, jid)}
				text = ''
				if status_message:
					text = status_message
				popup(_('Contact Signed In'), jid, account,
					path_to_image = path, title = title, text = text)
			elif event == 'contact_disconnected':
				title = _('%(nickname)s Signed Out') % \
					{'nickname': gajim.get_name_from_jid(account, jid)}
				text = ''
				if status_message:
					text = status_message
				popup(_('Contact Signed Out'), jid, account,
					path_to_image = path, title = title, text = text)
		elif event == 'new_message':
			if message_type == 'normal': # single message
				event_type = _('New Single Message')
				img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
					'single_msg_recv.png')
				title = _('New Single Message from %(nickname)s') % \
					{'nickname': nickname}
				text = message
			elif message_type == 'pm': # private message
				event_type = _('New Private Message')
				room_name = gajim.get_nick_from_jid(jid)
				img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
					'priv_msg_recv.png')
				title = _('New Private Message from group chat %s') % room_name
				if message:
					text = _('%(nickname)s: %(message)s') % {'nickname': nickname,
						'message': message}
				else:
					text = _('Messaged by %(nickname)s') % {'nickname': nickname}

			else: # chat message
				event_type = _('New Message')
				img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
					'chat_msg_recv.png')
				title = _('New Message from %(nickname)s') % \
					{'nickname': nickname}
				text = message
			path = gtkgui_helpers.get_path_to_generic_or_avatar(img)
			popup(event_type, jid, account, message_type,
				path_to_image = path, title = title, text = text)

	if do_sound:
		snd_file = None
		snd_event = None # If not snd_file, play the event
		if event == 'new_message':
			if advanced_notif_num is not None and gajim.config.get_per(
			'notifications', str(advanced_notif_num), 'sound') == 'yes':
				snd_file = gajim.config.get_per('notifications',
					str(advanced_notif_num), 'sound_file')
			elif advanced_notif_num is not None and gajim.config.get_per(
			'notifications', str(advanced_notif_num), 'sound') == 'no':
				pass # do not set snd_event
			elif is_first_message:
				snd_event = 'first_message_received'
			elif focused:
				snd_event = 'next_message_received_focused'
			else:
				snd_event = 'next_message_received_unfocused'
		elif event in ('contact_connected', 'contact_disconnected'):
			snd_event = event
		if snd_file:
			helpers.play_sound_file(snd_file)
		if snd_event:
			helpers.play_sound(snd_event)

	if do_cmd:
		command = gajim.config.get_per('notifications', str(advanced_notif_num),
			'command')
		try:
			helpers.exec_command(command)
		except:
			pass

def popup(event_type, jid, account, msg_type = '', path_to_image = None,
	title = None, text = None):
	'''Notifies 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.'''
	if gajim.config.get('use_notif_daemon') and dbus_support.supported:
		try:
			DesktopNotification(event_type, jid, account, msg_type,
				path_to_image, title, text)
			return	# sucessfully did D-Bus Notification procedure!
		except dbus.DBusException, e:
			# Connection to D-Bus failed
			gajim.log.debug(str(e))
		except TypeError, e:
			# This means that we sent the message incorrectly
			gajim.log.debug(str(e))
	# we failed to speak to notification daemon via D-Bus
	if USER_HAS_PYNOTIFY: # try via libnotify
		if not text and event_type == 'new_message':
			# empty text for new_message means do_preview = False
			text = gajim.get_name_from_jid(account, jid) # default value of text
		if not title:
			title = event_type
		# default image
		if not path_to_image:
			path_to_image = os.path.abspath(
				os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
					'chat_msg_recv.png')) # img to display


		notification = pynotify.Notification(title, text)
		timeout = gajim.config.get('notification_timeout') * 1000 # make it ms
		notification.set_timeout(timeout)

		notification.set_category(event_type)
		notification.set_data('event_type', event_type)
		notification.set_data('jid', jid)
		notification.set_data('account', account)
		notification.set_data('msg_type', msg_type)
		notification.set_property('icon-name', path_to_image)
		notification.add_action('default', 'Default Action',
			on_pynotify_notification_clicked)

		try:
			notification.show()
			return
		except gobject.GError, e:
			# Connection to notification-daemon failed, see #2893
			gajim.log.debug(str(e))
	# try os/x growl
	if USER_HAS_GROWL:
		osx.growler.notify(event_type, jid, account, msg_type, path_to_image,
			title, text)
		return

	# go old style
	instance = dialogs.PopupNotificationWindow(event_type, jid, account,
		msg_type, path_to_image, title, text)
	gajim.interface.roster.popup_notification_windows.append(instance)

def on_pynotify_notification_clicked(notification, action):
	jid = notification.get_data('jid')
	account = notification.get_data('account')
	msg_type = notification.get_data('msg_type')

	notification.close()
	gajim.interface.handle_event(account, jid, msg_type)

class NotificationResponseManager:
	'''Collects 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):
		self.received.append((id, time.time(), reason))
		if self.pending.has_key(id):
			notification = self.pending[id]
			notification.on_action_invoked(id, reason)
			del self.pending[id]
		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 self.pending.has_key(id):
			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!
			gajim.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):
		self.path_to_image = path_to_image
		self.event_type = event_type
		self.title = title
		self.text = text
		'''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 = gajim.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'
		else:
			# default failsafe values
			self.path_to_image = os.path.abspath(
				os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
					'chat_msg_recv.png')) # img to display
			ntype = 'im' # Notification Type

		self.notif = dbus_support.get_notifications_interface()
		if self.notif is None:
			raise dbus.DBusException('unable to get notifications interface')
		self.ntype = ntype

		self.get_version()

	def attempt_notify(self):
		version = self.version
		timeout = gajim.config.get('notification_timeout') # in seconds
		ntype = self.ntype
		if version[:2] == [0, 2]:
			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)],
					{'default': 0},
					[''],
					True,
					dbus.UInt32(timeout),
					reply_handler=self.attach_by_id,
					error_handler=self.notify_another_way)
			except AttributeError:
				version = [0, 3, 1] # we're actually dealing with the newer version
		if version > [0, 3]:
			if gajim.interface.systray_enabled and \
			gajim.config.get('attach_notifications_to_systray'):
				x, y = gajim.interface.systray.img_tray.window.get_position()
				x_, y_, width, height, depth = \
					gajim.interface.systray.img_tray.window.get_geometry()
				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)
				self.notif.Notify(
					dbus.String(_('Gajim')),
					dbus.UInt32(0), # this notification does not replace other
					dbus.String(self.path_to_image),
					dbus.String(self.title),
					dbus.String(self.text),
					( dbus.String('default'), dbus.String(self.event_type) ),
					hints,
					dbus.UInt32(timeout*1000),
					reply_handler=self.attach_by_id,
					error_handler=self.notify_another_way)
			else:
				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(timeout*1000),
					reply_handler=self.attach_by_id,
					error_handler=self.notify_another_way)

	def attach_by_id(self, id):
		self.id = id
		notification_response_manager.attach_to_interface()
		notification_response_manager.add_pending(self.id, self)

	def notify_another_way(self,e):
		gajim.log.debug(str(e))
		gajim.log.debug('Need to implement a new way of falling back')

	def on_action_invoked(self, id, reason):
		if self.notif is None:
			return
		self.notif.CloseNotification(dbus.UInt32(id))
		self.notif = None

		gajim.interface.handle_event(self.account, self.jid, self.msg_type)

	def version_reply_handler(self, name, vendor, version, spec_version = None):
		version_list = version.split('.')
		self.version = []
		while len(version_list):
			self.version.append(int(version_list.pop(0)))
		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()

# vim: se ts=3: