gajim.py 80.9 KB
Newer Older
1
#!/usr/bin/env python
2
##	gajim.py
Yann Leboulanger's avatar
Yann Leboulanger committed
3 4
##
##
5 6 7 8
## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
Yann Leboulanger's avatar
Yann Leboulanger committed
9 10 11 12 13 14 15 16 17 18
##
## This program 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 2 only.
##
## This program 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.
##
19

nkour's avatar
nkour committed
20
import sys
nkour's avatar
nkour committed
21
import os
22
import urllib
23

24 25
from common import i18n

nicfit's avatar
nicfit committed
26 27
import message_control

nicfit's avatar
nicfit committed
28
from chat_control import ChatControlBase
29
from atom_window import AtomWindow
nicfit's avatar
nicfit committed
30

31
from common import exceptions
32 33
from common.zeroconf import connection_zeroconf
from common import dbus_support
34

35 36 37 38 39 40 41 42
if os.name == 'posix': # dl module is Unix Only
	try: # rename the process name to gajim
		import dl
		libc = dl.open('/lib/libc.so.6')
		libc.call('prctl', 15, 'gajim\0', 0, 0, 0)
	except:
		pass

nkour's avatar
nkour committed
43 44 45 46
try:
	import gtk
except RuntimeError, msg:
	if str(msg) == 'could not open display':
47
		print >> sys.stderr, _('Gajim needs X server to run. Quiting...')
48
		sys.exit()
49
pritext = ''
nkour's avatar
nkour committed
50
if gtk.pygtk_version < (2, 6, 0):
nkour's avatar
nkour committed
51 52
	pritext = _('Gajim needs PyGTK 2.6 or above')
	sectext = _('Gajim needs PyGTK 2.6 or above to run. Quiting...')
nkour's avatar
nkour committed
53
elif gtk.gtk_version < (2, 6, 0):
nkour's avatar
nkour committed
54
	pritext = _('Gajim needs GTK 2.6 or above')
nkour's avatar
nkour committed
55
	sectext = _('Gajim needs GTK 2.6 or above to run. Quiting...')
nkour's avatar
nkour committed
56

57 58 59 60 61
try:
	import gtk.glade # check if user has libglade (in pygtk and in gtk)
except ImportError:
	pritext = _('GTK+ runtime is missing libglade support')
	if os.name == 'nt':
62
		sectext = _('Please remove your current GTK+ runtime and install the latest stable version from %s') % 'http://gladewin32.sourceforge.net'
63
	else:
64
		sectext = _('Please make sure that GTK+ and PyGTK have libglade support in your system.')
65 66 67 68

try:
	from common import check_paths
except exceptions.PysqliteNotAvailable, e:
nkour's avatar
nkour committed
69
	pritext = _('Gajim needs PySQLite2 to run')
70 71
	sectext = str(e)

72 73 74
if os.name == 'nt':
	try:
		import winsound # windows-only built-in module for playing wav
75
		import win32api # do NOT remove. we req this module
76 77 78 79
	except:
		pritext = _('Gajim needs pywin32 to run')
		sectext = _('Please make sure that Pywin32 is installed on your system. You can get it at %s') % 'http://sourceforge.net/project/showfiles.php?group_id=78018'

80
if pritext:
81
	dlg = gtk.MessageDialog(None, 
82 83
		gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
		gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
84 85 86 87 88 89

	dlg.format_secondary_text(sectext)
	dlg.run()
	dlg.destroy()
	sys.exit()

Yann Leboulanger's avatar
Yann Leboulanger committed
90 91
del pritext

92
path = os.getcwd()
nkour's avatar
nkour committed
93
if '.svn' in os.listdir(path) or '_svn' in os.listdir(path):
94 95 96 97 98 99
	# import gtkexcepthook only for those that run svn
	# those than run with --verbose run from terminal so no need to care
	# about those
	import gtkexcepthook
del path

100
import gobject
101

102
import re
103
import signal
104
import getopt
105
import time
106
import math
nkour's avatar
typo  
nkour committed
107

108
import gtkgui_helpers
109
import notify
110

111
import common.sleepy
nkour's avatar
nkour committed
112

dkirov's avatar
dkirov committed
113 114
from common.xmpp import idlequeue
from common import nslookup
115
from common import proxy65_manager
116
from common import socks5
117
from common import gajim
118
from common import helpers
119
from common import optparser
120

121 122
profile = ''
try:
123
	opts, args = getopt.getopt(sys.argv[1:], 'hvp:', ['help', 'verbose',
124
		'profile=', 'sm-config-prefix=', 'sm-client-id='])
125
except getopt.error, msg:
Yann Leboulanger's avatar
Yann Leboulanger committed
126 127 128
	print msg
	print 'for help use --help'
	sys.exit(2)
129
for o, a in opts:
Yann Leboulanger's avatar
Yann Leboulanger committed
130
	if o in ('-h', '--help'):
nkour's avatar
nkour committed
131
		print 'gajim [--help] [--verbose] [--profile name]'
132
		sys.exit()
Yann Leboulanger's avatar
Yann Leboulanger committed
133 134 135 136
	elif o in ('-v', '--verbose'):
		gajim.verbose = True
	elif o in ('-p', '--profile'): # gajim --profile name
		profile = a
jimpp's avatar
jimpp committed
137 138
del opts
del args
139

140 141 142 143 144
import locale
profile = unicode(profile, locale.getpreferredencoding())

import common.configpaths
common.configpaths.init_profile(profile)
Yann Leboulanger's avatar
Yann Leboulanger committed
145
del profile
146 147 148 149 150
gajimpaths = common.configpaths.gajimpaths

pid_filename = gajimpaths['PID_FILE']
config_filename = gajimpaths['CONFIG_FILE']

151 152 153
import traceback
import errno

154
import dialogs
155
def pid_alive():
156
	try:
157
		pf = open(pid_filename)
158 159 160
	except:
		# probably file not found
		return False
161 162 163 164 165 166 167 168 169

	try:
		pid = int(pf.read().strip())
		pf.close()
	except:
		traceback.print_exc()
		# PID file exists, but something happened trying to read PID
		# Could be 0.10 style empty PID file, so assume Gajim is running
		return True
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
	if os.name == 'nt':
		try:
			from ctypes import (windll, c_ulong, c_int, Structure, c_char, POINTER, pointer, )
		except:
			return True

		class PROCESSENTRY32(Structure):
			_fields_ = [
				('dwSize', c_ulong, ),
				('cntUsage', c_ulong, ),
				('th32ProcessID', c_ulong, ),
				('th32DefaultHeapID', c_ulong, ),
				('th32ModuleID', c_ulong, ),
				('cntThreads', c_ulong, ),
				('th32ParentProcessID', c_ulong, ),
				('pcPriClassBase', c_ulong, ),
				('dwFlags', c_ulong, ),
				('szExeFile', c_char*512, ),
				]
			def __init__(self):
				Structure.__init__(self, 512+9*4)

		k = windll.kernel32
		k.CreateToolhelp32Snapshot.argtypes = c_ulong, c_ulong,
		k.CreateToolhelp32Snapshot.restype = c_int
		k.Process32First.argtypes = c_int, POINTER(PROCESSENTRY32),
		k.Process32First.restype = c_int
		k.Process32Next.argtypes = c_int, POINTER(PROCESSENTRY32),
		k.Process32Next.restype = c_int

		def get_p(p):
			h = k.CreateToolhelp32Snapshot(2, 0) # TH32CS_SNAPPROCESS
			assert h > 0, 'CreateToolhelp32Snapshot failed'
			b = pointer(PROCESSENTRY32())
			f = k.Process32First(h, b)
			while f:
				if b.contents.th32ProcessID == p:
					return b.contents.szExeFile
				f = k.Process32Next(h, b)

211
		if get_p(pid) in ('python.exe', 'gajim.exe'):
212 213 214
			return True
		return False
	try:
215 216 217
		if not os.path.exists('/proc'):
			return True # no /proc, assume Gajim is running

218 219 220 221 222 223 224 225
		try:
			f = open('/proc/%d/cmdline'% pid) 
		except IOError, e:
			if e.errno == errno.ENOENT:
				return False # file/pid does not exist
			raise 

		n = f.read().lower()
226
		f.close()
227 228 229
		if n.find('gajim') < 0:
			return False
		return True # Running Gajim found at pid
230
	except:
231 232 233 234 235
		traceback.print_exc()

	# If we are here, pidfile exists, but some unexpected error occured.
	# Assume Gajim is running.
	return True
236 237

if pid_alive():
dkirov's avatar
dkirov committed
238 239 240
	path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
	pix = gtk.gdk.pixbuf_new_from_file(path_to_file)
	gtk.window_set_default_icon(pix) # set the icon to all newly opened wind
241
	pritext = _('Gajim is already running')
242 243 244 245
	sectext = _('Another instance of Gajim seems to be running\nRun anyway?')
	dialog = dialogs.YesNoDialog(pritext, sectext)
	if dialog.get_response() != gtk.RESPONSE_YES:
		sys.exit(3)
jimpp's avatar
jimpp committed
246
	# run anyway, delete pid and useless global vars
dkirov's avatar
dkirov committed
247 248
	if os.path.exists(pid_filename):
		os.remove(pid_filename)
jimpp's avatar
jimpp committed
249 250 251 252
	del path_to_file
	del pix
	del pritext
	del sectext
253
	dialog.destroy()
254

dkirov's avatar
dkirov committed
255 256 257 258 259
# Create .gajim dir
pid_dir =  os.path.dirname(pid_filename)
if not os.path.exists(pid_dir):
	check_paths.create_path(pid_dir)
# Create pid file
260 261 262 263 264 265 266 267 268
try:
	f = open(pid_filename, 'w')
	f.write(str(os.getpid()))
	f.close()
except IOError, e:
	dlg = dialogs.ErrorDialog(_('Disk Write Error'), str(e))
	dlg.run()
	dlg.destroy()
	sys.exit()
jimpp's avatar
jimpp committed
269
del pid_dir
Yann Leboulanger's avatar
Yann Leboulanger committed
270
del f
271 272 273 274 275 276 277 278

def on_exit():
	# delete pid file on normal exit
	if os.path.exists(pid_filename):
		os.remove(pid_filename)

import atexit
atexit.register(on_exit)
279

280
parser = optparser.OptionsParser(config_filename)
281

282
import roster_window
283
import profile_window
284
import config
285

dkirov's avatar
dkirov committed
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
class GlibIdleQueue(idlequeue.IdleQueue):
	''' 
	Extends IdleQueue to use glib io_add_wath, instead of select/poll
	In another, `non gui' implementation of Gajim IdleQueue can be used safetly.
	'''
	def init_idle(self):
		''' this method is called at the end of class constructor.
		Creates a dict, which maps file/pipe/sock descriptor to glib event id'''
		self.events = {}
		if gtk.pygtk_version >= (2, 8, 0):
			# time() is already called in glib, we just get the last value 
			# overrides IdleQueue.current_time()
			self.current_time = lambda: gobject.get_current_time()
			
	def add_idle(self, fd, flags):
		''' this method is called when we plug a new idle object.
		Start listening for events from fd
		'''
		res = gobject.io_add_watch(fd, flags, self.process_events, 
305
			priority=gobject.PRIORITY_LOW)
dkirov's avatar
dkirov committed
306 307 308 309 310 311 312 313 314 315 316 317 318
		# store the id of the watch, so that we can remove it on unplug
		self.events[fd] = res
	
	def remove_idle(self, fd):
		''' this method is called when we unplug a new idle object.
		Stop listening for events from fd
		'''
		gobject.source_remove(self.events[fd])
		del(self.events[fd])
	
	def process(self):
		self.check_time_events()
	
319
class Interface:
320
	def handle_event_roster(self, account, data):
321
		#('ROSTER', account, array)
322
		self.roster.fill_contacts_and_groups_dicts(data, account)
323
		self.roster.add_account_contacts(account)
324
		self.roster.fire_up_unread_messages_events(account)
325 326
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('Roster', (account, data))
327

328 329
	def handle_event_warning(self, unused, data):
		#('WARNING', account, (title_text, section_text))
330
		dialogs.WarningDialog(data[0], data[1])
331

332 333
	def handle_event_error(self, unused, data):
		#('ERROR', account, (title_text, section_text))
334
		dialogs.ErrorDialog(data[0], data[1])
335 336 337

	def handle_event_information(self, unused, data):
		#('INFORMATION', account, (title_text, section_text))
338
		dialogs.InformationDialog(data[0], data[1])
339
		
340 341
	def handle_event_ask_new_nick(self, account, data):
		#('ASK_NEW_NICK', account, (room_jid, title_text, prompt_text, proposed_nick))
342 343 344
		room_jid = data[0]
		title = data[1]
		prompt = data[2]
345
		proposed_nick = data[3]
346
		gc_control = self.msg_win_mgr.get_control(room_jid, account)
nicfit's avatar
nicfit committed
347 348
		if gc_control: # user may close the window before we are here
			gc_control.show_change_nick_input_dialog(title, prompt, proposed_nick)
349

350
	def handle_event_http_auth(self, account, data):
351
		#('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
352 353 354 355
		def response(widget, account, iq_obj, answer):
			self.dialog.destroy()
			gajim.connections[account].build_http_auth_answer(iq_obj, answer)

356 357 358
		sec_msg = _('Do you accept this request?')
		if data[4]:
			sec_msg = data[4] + '\n' + sec_msg
359
		self.dialog = dialogs.YesNoDialog(_('HTTP (%s) Authorization for %s (id: %s)') \
360
			% (data[0], data[1], data[2]), sec_msg,
361 362
			on_response_yes = (response, account, data[3], 'yes'),
			on_response_no = (response, account, data[3], 'no'))
363

Yann Leboulanger's avatar
Yann Leboulanger committed
364
	def handle_event_error_answer(self, account, array):
365
		#('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
366
		id, jid_from, errmsg, errcode = array
367
		if unicode(errcode) in ('403', '406') and id:
dkirov's avatar
dkirov committed
368
			# show the error dialog
369
			ft = self.instances['file_transfers']
dkirov's avatar
dkirov committed
370 371 372 373 374 375 376
			sid = id
			if len(id) > 3 and id[2] == '_':
				sid = id[3:]
			if ft.files_props['s'].has_key(sid):
				file_props = ft.files_props['s'][sid]
				file_props['error'] = -4
				self.handle_event_file_request_error(account, 
377
					(jid_from, file_props, errmsg))
dkirov's avatar
dkirov committed
378 379 380
				conn = gajim.connections[account]
				conn.disconnect_transfer(file_props)
				return
381
		elif unicode(errcode) == '404':
dkirov's avatar
dkirov committed
382
			conn = gajim.connections[account]
dkirov's avatar
dkirov committed
383 384 385 386 387 388 389 390 391
			sid = id
			if len(id) > 3 and id[2] == '_':
				sid = id[3:]
			if conn.files_props.has_key(sid):
				file_props = conn.files_props[sid]
				self.handle_event_file_send_error(account, 
					(jid_from, file_props))
				conn.disconnect_transfer(file_props)
				return
392
		ctrl = self.msg_win_mgr.get_control(jid_from, account)
nkour's avatar
nkour committed
393 394
		if ctrl and ctrl.type_id == message_control.TYPE_GC:
			ctrl.print_conversation('Error %s: %s' % (array[2], array[1]))
Yann Leboulanger's avatar
Yann Leboulanger committed
395

396 397
	def handle_event_con_type(self, account, con_type):
		# ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp'
398
		gajim.con_types[account] = con_type
399
		self.roster.draw_account(account)
400

401 402 403 404 405 406 407 408
	def handle_event_connection_lost(self, account, array):
		# ('CONNECTION_LOST', account, [title, text])
		path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
			'connection_lost.png')
		path = gtkgui_helpers.get_path_to_generic_or_avatar(path)
		notify.popup(_('Connection Failed'), account, account,
			'connection_failed', path, array[0], array[1])

409 410
	def unblock_signed_in_notifications(self, account):
		gajim.block_signed_in_notifications[account] = False
411

412
	def handle_event_status(self, account, status): # OUR status
413
		#('STATUS', account, status)
414 415
		model = self.roster.status_combobox.get_model()
		if status == 'offline':
nicfit's avatar
nicfit committed
416
			# sensitivity for this menuitem
417 418
			if gajim.get_number_of_connected_accounts() == 0:
				model[self.roster.status_message_menuitem_iter][3] = False
419
			gajim.block_signed_in_notifications[account] = True
420
		else:
421 422 423 424 425
			# 30 seconds after we change our status to sth else than offline
			# we stop blocking notifications of any kind
			# this prevents from getting the roster items as 'just signed in'
			# contacts. 30 seconds should be enough time
			gobject.timeout_add(30000, self.unblock_signed_in_notifications, account)
nicfit's avatar
nicfit committed
426 427 428 429
			# sensitivity for this menuitem
			model[self.roster.status_message_menuitem_iter][3] = True

		# Inform all controls for this account of the connection state change
430
		for ctrl in self.msg_win_mgr.get_controls():
nicfit's avatar
nicfit committed
431
			if ctrl.account == account:
432 433
				if status == 'offline' or (status == 'invisible' and \
						gajim.connections[account].is_zeroconf):
nicfit's avatar
nicfit committed
434 435 436 437 438 439 440
					ctrl.got_disconnected()
				else:
					# Other code rejoins all GCs, so we don't do it here
					if not ctrl.type_id == message_control.TYPE_GC:
						ctrl.got_connected()
				ctrl.parent_win.redraw_tab(ctrl)

441
		self.roster.on_status_changed(account, status)
442
		if account in self.show_vcard_when_connect:
443
			self.edit_own_details(account)
444 445
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('AccountPresence', (status, account))
446
	
447 448
	def edit_own_details(self, account):
		jid = gajim.get_jid_from_account(account)
449 450 451
		if not self.instances[account].has_key('profile'):
			self.instances[account]['profile'] = \
				profile_window.ProfileWindow(account)
452 453
			gajim.connections[account].request_vcard(jid)

454
	def handle_event_notify(self, account, array):
455
		# 'NOTIFY' (account, (jid, status, status message, resource, priority,
456
		# keyID, timestamp, contact_nickname))
nkour's avatar
nkour committed
457 458 459
		# if we're here it means contact changed show
		statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd',
			'invisible']
Yann Leboulanger's avatar
Yann Leboulanger committed
460 461 462
		# Ignore invalid show
		if array[1] not in statuss:
			return
463
		old_show = 0
Yann Leboulanger's avatar
Yann Leboulanger committed
464
		new_show = statuss.index(array[1])
465
		status_message = array[2]
466
		jid = array[0].split('/')[0]
Yann Leboulanger's avatar
Yann Leboulanger committed
467
		keyID = array[5]
468
		contact_nickname = array[7]
469 470 471 472
		attached_keys = gajim.config.get_per('accounts', account,
			'attached_gpg_keys').split()
		if jid in attached_keys:
			keyID = attached_keys[attached_keys.index(jid) + 1]
473 474 475 476
		resource = array[3]
		if not resource:
			resource = ''
		priority = array[4]
477
		if gajim.jid_is_transport(jid):
478
			# It must be an agent
479
			ji = jid.replace('@', '')
480 481
		else:
			ji = jid
482

nkour's avatar
nkour committed
483
		# Update contact
484
		jid_list = gajim.contacts.get_jid_list(account)
485
		if ji in jid_list or jid == gajim.get_jid_from_account(account):
486
			lcontact = gajim.contacts.get_contacts_from_jid(account, ji)
487
			contact1 = None
488
			resources = []
489
			for c in lcontact:
nkour's avatar
typo  
nkour committed
490
				resources.append(c.resource)
491 492
				if c.resource == resource:
					contact1 = c
493
					break
494 495 496
			if contact1:
				if contact1.show in statuss:
					old_show = statuss.index(contact1.show)
497 498 499 500
				if contact_nickname is not None and \
				contact1.contact_name != contact_nickname:
					contact1.contact_name = contact_nickname
					self.roster.draw_contact(jid, account)
501
				if old_show == new_show and contact1.status == status_message and \
502
					contact1.priority == priority: # no change
503
					return
504
			else:
505
				contact1 = gajim.contacts.get_first_contact_from_jid(account, ji)
506
				if not contact1:
507 508 509
					# presence of another resource of our jid
					if resource == gajim.connections[account].server_resource:
						return
510 511 512 513 514 515 516 517 518 519
					contact1 = gajim.contacts.create_contact(jid = ji,
						name = gajim.nicks[account], groups = [],
						show = array[1], status = status_message, sub = 'both',
						ask = 'none', priority = priority, keyID = keyID,
						resource = resource)
					old_show = 0
					gajim.contacts.add_contact(account, contact1)
					lcontact.append(contact1)
					self.roster.add_self_contact(account)
				elif contact1.show in statuss:
520 521
					old_show = statuss.index(contact1.show)
				if (resources != [''] and (len(lcontact) != 1 or 
522
				lcontact[0].show != 'offline')) and jid.find('@') > 0:
523
					old_show = 0
524
					contact1 = gajim.contacts.copy_contact(contact1)
525 526
					lcontact.append(contact1)
				contact1.resource = resource
527 528
			if contact1.jid.find('@') > 0 and len(lcontact) == 1:
				# It's not an agent
Yann Leboulanger's avatar
Yann Leboulanger committed
529
				if old_show == 0 and new_show > 1:
530 531 532 533 534 535
					if not contact1.jid in gajim.newly_added[account]:
						gajim.newly_added[account].append(contact1.jid)
					if contact1.jid in gajim.to_be_removed[account]:
						gajim.to_be_removed[account].remove(contact1.jid)
					gobject.timeout_add(5000, self.roster.remove_newly_added,
						contact1.jid, account)
536
				elif old_show > 1 and new_show == 0 and gajim.connections[account].\
537
					connected > 1:
538 539 540 541 542
					if not contact1.jid in gajim.to_be_removed[account]:
						gajim.to_be_removed[account].append(contact1.jid)
					if contact1.jid in gajim.newly_added[account]:
						gajim.newly_added[account].remove(contact1.jid)
					self.roster.draw_contact(contact1.jid, account)
543 544
					gobject.timeout_add(5000, self.roster.really_remove_contact,
						contact1, account)
545
			contact1.show = array[1]
546
			contact1.status = status_message
547 548
			contact1.priority = priority
			contact1.keyID = keyID
549 550 551 552 553
			timestamp = array[6]
			if timestamp:
				contact1.last_status_time = timestamp
			elif not gajim.block_signed_in_notifications[account]:
				# We're connected since more that 30 seconds
554
				contact1.last_status_time = time.localtime()
555
			contact1.contact_nickname = contact_nickname
556
		if gajim.jid_is_transport(jid):
557
			# It must be an agent
558
			if ji in jid_list:
559
				# Update existing iter
560
				self.roster.draw_contact(ji, account)
561
				self.roster.draw_group(_('Transports'), account)
562 563 564 565
				if new_show > 1 and ji in gajim.transport_avatar[account]:
					# transport just signed in. request avatars
					for jid_ in gajim.transport_avatar[account][ji]:
						gajim.connections[account].request_vcard(jid_)
566 567 568 569 570 571
				# transport just signed in/out, don't show popup notifications
				# for 30s
				account_ji = account + '/' + ji
				gajim.block_signed_in_notifications[account_ji] = True
				gobject.timeout_add(30000, self.unblock_signed_in_notifications,
					account_ji)
572 573 574 575 576 577 578 579 580
			locations = (self.instances, self.instances[account])
			for location in locations:
				if location.has_key('add_contact'):
					if old_show == 0 and new_show > 1:
						location['add_contact'].transport_signed_in(jid)
						break
					elif old_show > 1 and new_show == 0:
						location['add_contact'].transport_signed_out(jid)
						break
581
		elif ji in jid_list:
582
			# It isn't an agent
583
			# reset chatstate if needed:
584
			# (when contact signs out or has errors)
585
			if array[1] in ('offline', 'error'):
586
				contact1.our_chatstate = contact1.chatstate = \
587
					contact1.composing_xep = None
588
				gajim.connections[account].remove_transfers_for_contact(contact1)
589 590
			self.roster.chg_contact_status(contact1, array[1], status_message,
				account)
591
			# Notifications
592
			if old_show < 2 and new_show > 1:
593
				notify.notify('contact_connected', jid, account, status_message)
594 595
				if self.remote_ctrl:
					self.remote_ctrl.raise_signal('ContactPresence',
596
						(account, array))
597

598
			elif old_show > 1 and new_show < 2:
599
				notify.notify('contact_disconnected', jid, account, status_message)
600 601 602
				if self.remote_ctrl:
					self.remote_ctrl.raise_signal('ContactAbsence', (account, array))
				# FIXME: stop non active file transfers
603
			elif new_show > 1: # Status change (not connected/disconnected or error (<1))
604 605
				notify.notify('status_change', jid, account, [new_show,
					status_message])
606
		else:
607 608 609
			# FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) doesn't follow the JEP
			# remove in 2007
			# It's maybe a GC_NOTIFY (specialy for MSN gc)
610
			self.handle_event_gc_notify(account, (jid, array[1], status_message,
611
				array[3], None, None, None, None, None, None, None, None))
612
			
Yann Leboulanger's avatar
Yann Leboulanger committed
613

614
	def handle_event_msg(self, account, array):
615
		# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
616
		# chatstate, msg_id, composing_xep, user_nick, xhtml))
617
		# user_nick is JEP-0172
618

619 620 621
		full_jid_with_resource = array[0]
		jid = gajim.get_jid_without_resource(full_jid_with_resource)
		resource = gajim.get_resource_from_jid(full_jid_with_resource)
622

623
		message = array[1]
624
		encrypted = array[3]
625
		msg_type = array[4]
626
		subject = array[5]
nkour's avatar
nkour committed
627
		chatstate = array[6]
628
		msg_id = array[7]
629
		composing_xep = array[8]
630 631 632
		xhtml = array[10]
		if gajim.config.get('ignore_incoming_xhtml'):
			xhtml = None
633
		if gajim.jid_is_transport(jid):
634
			jid = jid.replace('@', '')
635

Yann Leboulanger's avatar
Yann Leboulanger committed
636 637 638 639 640 641
		groupchat_control = self.msg_win_mgr.get_control(jid, account)
		pm = False
		if groupchat_control and groupchat_control.type_id == \
		message_control.TYPE_GC:
			# It's a Private message
			pm = True
642
			msg_type = 'pm'
Yann Leboulanger's avatar
Yann Leboulanger committed
643

Yann Leboulanger's avatar
Yann Leboulanger committed
644 645
		chat_control = None
		jid_of_control = full_jid_with_resource
646 647 648 649 650 651 652
		highest_contact = gajim.contacts.get_contact_with_highest_priority(
			account, jid)
		# Look for a chat control that has the given resource, or default to one
		# without resource
		ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account)
		if ctrl:
			chat_control = ctrl
Yann Leboulanger's avatar
Yann Leboulanger committed
653
		elif not pm and (not highest_contact or not highest_contact.resource):
654 655 656
			# unknow contact or offline message
			jid_of_control = jid
			chat_control = self.msg_win_mgr.get_control(jid, account)
657 658
		elif highest_contact and resource != highest_contact.resource and \
		highest_contact.show != 'offline':
659 660
			jid_of_control = full_jid_with_resource
			chat_control = None
Yann Leboulanger's avatar
Yann Leboulanger committed
661
		elif not pm:
662 663 664
			jid_of_control = jid
			chat_control = self.msg_win_mgr.get_control(jid, account)

665
		# Handle chat states  
666
		contact = gajim.contacts.get_contact(account, jid, resource)
667
		if contact and isinstance(contact, list):
668
			contact = contact[0]
669
		if contact:
670 671
			if contact.composing_xep != 'XEP-0085': # We cache xep85 support
				contact.composing_xep = composing_xep
672 673 674 675 676 677 678 679 680 681 682 683 684
			if chat_control and chat_control.type_id == message_control.TYPE_CHAT:
				if chatstate is not None:
					# other peer sent us reply, so he supports jep85 or jep22
					contact.chatstate = chatstate
					if contact.our_chatstate == 'ask': # we were jep85 disco?
						contact.our_chatstate = 'active' # no more
					chat_control.handle_incoming_chatstate()
				elif contact.chatstate != 'active':
					# got no valid jep85 answer, peer does not support it
					contact.chatstate = False
			elif chatstate == 'active':
				# Brand new message, incoming.  
				contact.our_chatstate = chatstate
685
				contact.chatstate = chatstate
686 687
				if msg_id: # Do not overwrite an existing msg_id with None
					contact.msg_id = msg_id
688 689

		# THIS MUST BE AFTER chatstates handling
690
		# AND BEFORE playsound (else we ear sounding on chatstates!)
691
		if not message: # empty message text
692 693
			return

694 695 696
		if gajim.config.get('ignore_unknown_contacts') and \
			not gajim.contacts.get_contact(account, jid) and not pm:
			return
697 698 699 700
		if not contact:
			# contact is not in the roster, create a fake one to display
			# notification
			contact = common.contacts.Contact(jid = jid, resource = resource) 
701 702 703
		advanced_notif_num = notify.get_advanced_notification('message_received',
			account, contact)

704
		# Is it a first or next message received ?
705
		first = False
706 707 708 709 710
		if msg_type == 'normal':
			if not gajim.events.get_events(account, jid, ['normal']):
				first = True
		elif not chat_control and not gajim.events.get_events(account, 
		jid_of_control, [msg_type]): # msg_type can be chat or pm
711 712
			first = True

713
		if pm:
714
			nickname = resource
715
			groupchat_control.on_private_message(nickname, message, array[2],
716
				xhtml, msg_id)
717 718
		else:
			# array: (jid, msg, time, encrypted, msg_type, subject)
719 720 721 722 723 724 725 726 727
			if encrypted:
				self.roster.on_message(jid, message, array[2], account, array[3],
					msg_type, subject, resource, msg_id, array[9],
					advanced_notif_num)
			else:
				# xhtml in last element
				self.roster.on_message(jid, message, array[2], account, array[3],
					msg_type, subject, resource, msg_id, array[9],
					advanced_notif_num, xhtml = xhtml)
728 729
			nickname = gajim.get_name_from_jid(account, jid)
		# Check and do wanted notifications	
730 731 732
		msg = message
		if subject:
			msg = _('Subject: %s') % subject + '\n' + msg
733 734
		notify.notify('new_message', jid_of_control, account, [msg_type,
			first, nickname, msg], advanced_notif_num)
735

736 737
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('NewMessage', (account, array))
738

Yann Leboulanger's avatar
Yann Leboulanger committed
739
	def handle_event_msgerror(self, account, array):
740 741 742
		#'MSGERROR' (account, (jid, error_code, error_msg, msg, time))
		full_jid_with_resource = array[0]
		jids = full_jid_with_resource.split('/', 1)
743
		jid = jids[0]
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
		gc_control = self.msg_win_mgr.get_control(jid, account)
		if gc_control and gc_control.type_id != message_control.TYPE_GC:
			gc_control = None
		if gc_control:
			if len(jids) > 1: # it's a pm
				nick = jids[1]
				if not self.msg_win_mgr.get_control(full_jid_with_resource,
				account):
					tv = gc_control.list_treeview
					model = tv.get_model()
					iter = gc_control.get_contact_iter(nick)
					if iter:
						show = model[iter][3]
					else:
						show = 'offline'
					gc_c = gajim.contacts.create_gc_contact(room_jid = jid,
						name = nick, show = show)
761
					self.roster.new_private_chat(gc_c, account)
762 763 764
				ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account)
				ctrl.print_conversation('Error %s: %s' % (array[1], array[2]),
							'status')
765
				return
nicfit's avatar
nicfit committed
766

767 768 769 770 771
			gc_control.print_conversation('Error %s: %s' % (array[1], array[2]))
			if gc_control.parent_win.get_active_jid() == jid:
				gc_control.set_subject(gc_control.subject)
			return

772
		if gajim.jid_is_transport(jid):
773
			jid = jid.replace('@', '')
774 775 776 777
		msg = array[2]
		if array[3]:
			msg = _('error while sending %s ( %s )') % (array[3], msg)
		self.roster.on_message(jid, msg, array[4], account, \
778
			msg_type='error')
Yann Leboulanger's avatar
Yann Leboulanger committed
779
		
780
	def handle_event_msgsent(self, account, array):
nkour's avatar
nkour committed
781
		#('MSGSENT', account, (jid, msg, keyID))
782 783 784
		msg = array[1]
		# do not play sound when standalone chatstate message (eg no msg)
		if msg and gajim.config.get_per('soundevents', 'message_sent', 'enabled'):
785
			helpers.play_sound('message_sent')
786 787 788 789 790 791 792

	def handle_event_msgnotsent(self, account, array):
		#('MSGNOTSENT', account, (jid, ierror_msg, msg, time))
		msg = _('error while sending %s ( %s )') % (array[2], array[1])
		self.roster.on_message(array[0], msg, array[3], account,
			msg_type='error')

793
	def handle_event_subscribe(self, account, array):
794 795
		#('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
		dialogs.SubscriptionRequestWindow(array[0], array[1], account, array[2])
796 797
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('Subscribe', (account, array))
798 799

	def handle_event_subscribed(self, account, array):
800
		#('SUBSCRIBED', account, (jid, resource))
801
		jid = array[0]
802 803
		if jid in gajim.contacts.get_jid_list(account):
			c = gajim.contacts.get_first_contact_from_jid(account, jid)
804 805
			c.resource = array[1]
			self.roster.remove_contact(c, account)
806 807
			if _('Not in Roster') in c.groups:
				c.groups.remove(_('Not in Roster'))
808
			self.roster.add_contact_to_roster(c.jid, account)
809
		else:
810 811 812 813 814
			keyID = ''
			attached_keys = gajim.config.get_per('accounts', account,
				'attached_gpg_keys').split()
			if jid in attached_keys:
				keyID = attached_keys[attached_keys.index(jid) + 1]
815 816
			name = jid.split('@', 1)[0]
			name = name.split('%', 1)[0]
817
			contact1 = gajim.contacts.create_contact(jid = jid, name = name,
818
				groups = [], show = 'online', status = 'online',
819 820
				ask = 'to', resource = array[1], keyID = keyID)
			gajim.contacts.add_contact(account, contact1)
821
			self.roster.add_contact_to_roster(jid, account)
822
		dialogs.InformationDialog(_('Authorization accepted'),
nkour's avatar
nkour committed
823
				_('The contact "%s" has authorized you to see his or her status.')
824
				% jid)
nkour's avatar
nkour committed
825
		if not gajim.config.get_per('accounts', account, 'dont_ack_subscription'):
826
			gajim.connections[account].ack_subscribed(jid)
827 828
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('Subscribed', (account, array))
829 830

	def handle_event_unsubscribed(self, account, jid):
831 832
		dialogs.InformationDialog(_('Contact "%s" removed subscription from you')\
			% jid, _('You will always see him or her as offline.'))
833 834
		# FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does not show deny
		gajim.connections[account].ack_unsubscribed(jid)
835 836
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('Unsubscribed', (account, jid))
837 838 839 840 841 842 843 844 845 846 847 848 849 850
	
	def handle_event_agent_info_error(self, account, agent):
		#('AGENT_ERROR_INFO', account, (agent))
		try:
			gajim.connections[account].services_cache.agent_info_error(agent)
		except AttributeError:
			return
	
	def handle_event_agent_items_error(self, account, agent):
		#('AGENT_ERROR_INFO', account, (agent))
		try:
			gajim.connections[account].services_cache.agent_items_error(agent)
		except AttributeError:
			return
851

852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868
	def handle_event_agent_removed(self, account, agent):
		# remove transport's contacts from treeview
		jid_list = gajim.contacts.get_jid_list(account)
		for jid in jid_list:
			if jid.endswith('@' + agent):
				c = gajim.contacts.get_first_contact_from_jid(account, jid)
				gajim.log.debug(
					'Removing contact %s due to unregistered transport %s'\
					% (jid, agent))
				gajim.connections[account].unsubscribe(c.jid)
				# Transport contacts can't have 2 resources
				if c.jid in gajim.to_be_removed[account]:
					# This way we'll really remove it
					gajim.to_be_removed[account].remove(c.jid)
				gajim.contacts.remove_jid(account, c.jid)
				self.roster.remove_contact(c, account)

869
	def handle_event_register_agent_info(self, account, array):
870 871 872
		# ('REGISTER_AGENT_INFO', account, (agent, infos, is_form))
		# info in a dataform if is_form is True
		if array[2] or array[1].has_key('instructions'):
873
			config.ServiceRegistrationWindow(array[0], array[1], account,
874
				array[2])
875
		else:
876 877
			dialogs.ErrorDialog(_('Contact with "%s" cannot be established') \
				% array[0], _('Check your connection or try again later.'))
878

879
	def handle_event_agent_info_items(self, account, array):
880
		#('AGENT_INFO_ITEMS', account, (agent, node, items))
881 882 883 884 885
		try:
			gajim.connections[account].services_cache.agent_items(array[0],
				array[1], array[2])
		except AttributeError:
			return
886 887

	def handle_event_agent_info_info(self, account, array):
888 889 890 891 892 893
		#('AGENT_INFO_INFO', account, (agent, node, identities, features, data))
		try:
			gajim.connections[account].services_cache.agent_info(array[0],
				array[1], array[2], array[3], array[4])
		except AttributeError:
			return
894

895
	def handle_event_acc_ok(self, account, array):
896
		#('ACC_OK', account, (config))
897 898
		if self.instances.has_key('account_creation_wizard'):
			self.instances['account_creation_wizard'].acc_is_ok(array)
899

900 901
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('NewAccount', (account, array))
902

903 904
	def handle_event_acc_not_ok(self, account, array):
		#('ACC_NOT_OK', account, (reason))
905 906
		if self.instances.has_key('account_creation_wizard'):
			self.instances['account_creation_wizard'].acc_is_not_ok(array)
907

908
	def handle_event_quit(self, p1, p2):
909
		self.roster.quit_gtkgui_interface()
Yann Leboulanger's avatar
Yann Leboulanger committed
910

Yann Leboulanger's avatar
Yann Leboulanger committed
911
	def handle_event_myvcard(self, account, array):
912
		nick = ''
913 914 915 916
		if array.has_key('NICKNAME') and array['NICKNAME']:
			gajim.nicks[account] = array['NICKNAME']
		elif array.has_key('FN') and array['FN']:
			gajim.nicks[account] = array['FN']
917 918
		if self.instances[account].has_key('profile'):
			win = self.instances[account]['profile']
919 920 921
			win.set_values(array)
			if account in self.show_vcard_when_connect:
				self.show_vcard_when_connect.remove(account)
922 923 924
		jid = array['jid']
		if self.instances[account]['infos'].has_key(jid):
			self.instances[account]['infos'][jid].set_values(array)
925

926
	def handle_event_vcard(self, account, vcard):
Yann Leboulanger's avatar
Yann Leboulanger committed
927
		# ('VCARD', account, data)
928 929
		'''vcard holds the vcard data'''
		jid = vcard['jid']
930 931 932
		resource = ''
		if vcard.has_key('resource'):
			resource = vcard['resource']
933 934
		
		# vcard window
Yann Leboulanger's avatar
Yann Leboulanger committed
935
		win = None
936 937
		if self.instances[account]['infos'].has_key(jid):
			win = self.instances[account]['infos'][jid]
938 939
		elif resource and self.instances[account]['infos'].has_key(
			jid + '/' + resource):
940
			win = self.instances[account]['infos'][jid + '/' + resource]
941
		if win:
942
			win.set_values(vcard)
943

944
		# show avatar in chat
945
		win = None
nkour's avatar
nkour committed
946
		ctrl = None
947
		if resource and self.msg_win_mgr.has_window(
948
		jid + '/' + resource, account):
949
			win = self.msg_win_mgr.get_window(jid + '/' + resource,
950 951
				account)
			ctrl = win.get_control(jid + '/' + resource, account)
952 953
		elif self.msg_win_mgr.has_window(jid, account):
			win = self.msg_win_mgr.get_window(jid, account)
954
			ctrl = win.get_control(jid, account)
nkour's avatar
nkour committed
955 956
		if win and ctrl.type_id != message_control.TYPE_GC:
			ctrl.show_avatar()
nicfit's avatar
nicfit committed
957

958
		# Show avatar in roster or gc_roster
959
		gc_ctrl = self.msg_win_mgr.get_control(jid, account)
960
		if gc_ctrl and gc_ctrl.type_id == message_control.TYPE_GC:
961 962 963
			gc_ctrl.draw_avatar(resource)
		else:
			self.roster.draw_avatar(jid, account)
964 965
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('VcardInfo', (account, vcard))
966

967 968 969 970 971 972 973 974 975 976 977
	def handle_event_last_status_time(self, account, array):
		# ('LAST_STATUS_TIME', account, (jid, resource, seconds, status))
		win = None
		if self.instances[account]['infos'].has_key(array[0]):
			win = self.instances[account]['infos'][array[0]]
		elif self.instances[account]['infos'].has_key(array[0] + '/' + array[1]):
			win = self.instances[account]['infos'][array[0] + '/' + array[1]]
		if win:
			c = gajim.contacts.get_contact(account, array[0], array[1])
			# c is a list when no resource is given. it probably means that contact
			# is offline, so only on Contact instance
978
			if