gajim.py 126 KB
Newer Older
roidelapluie's avatar
roidelapluie committed
1
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/gajim.py
Yann Leboulanger's avatar
Yann Leboulanger committed
3
##
roidelapluie's avatar
roidelapluie committed
4
5
6
7
## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
## Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
##                    Norman Rasmussen <norman AT rasmussen.co.za>
roidelapluie's avatar
roidelapluie committed
8
##                    Stéphan Kochen <stephan AT kochen.nl>
roidelapluie's avatar
roidelapluie committed
9
10
11
12
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
##                         Alex Mauer <hawke AT hawkesnest.net>
## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
roidelapluie's avatar
roidelapluie committed
13
## Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
roidelapluie's avatar
roidelapluie committed
14
15
16
17
18
19
20
21
##                    Stefan Bethge <stefan AT lanpartei.de>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
##                    James Newton <redshodan AT gmail.com>
## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
##                         Julien Pivotto <roidelapluie AT gmail.com>
##                         Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
Yann Leboulanger's avatar
Yann Leboulanger committed
22
##
Yann Leboulanger's avatar
Yann Leboulanger committed
23
24
25
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
Yann Leboulanger's avatar
Yann Leboulanger committed
26
## it under the terms of the GNU General Public License as published
Yann Leboulanger's avatar
Yann Leboulanger committed
27
## by the Free Software Foundation; version 3 only.
Yann Leboulanger's avatar
Yann Leboulanger committed
28
##
Yann Leboulanger's avatar
Yann Leboulanger committed
29
## Gajim is distributed in the hope that it will be useful,
Yann Leboulanger's avatar
Yann Leboulanger committed
30
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
31
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Yann Leboulanger's avatar
Yann Leboulanger committed
32
33
## GNU General Public License for more details.
##
Yann Leboulanger's avatar
Yann Leboulanger committed
34
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
35
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
36
##
37

nkour's avatar
nkour committed
38
import os
Yann Leboulanger's avatar
Yann Leboulanger committed
39
40
41
42
43

if os.name == 'nt':
	import warnings
	warnings.filterwarnings(action='ignore')

44
45
46
47
48
49
50
51
52
53
54
55
	if os.path.isdir('gtk'):
		# Used to create windows installer with GTK included
		paths = os.environ['PATH']
		list_ = paths.split(';')
		new_list = []
		for p in list_:
			if p.find('gtk') < 0 and p.find('GTK') < 0:
				new_list.append(p)
		new_list.insert(0, 'gtk/lib')
		new_list.insert(0, 'gtk/bin')
		os.environ['PATH'] = ';'.join(new_list)
		os.environ['GTK_BASEPATH'] = 'gtk'
Yann Leboulanger's avatar
Yann Leboulanger committed
56
57

import sys
58

59
60
61
62
if os.name == 'nt':
	# needed for docutils
	sys.path.append('.')

63
64
import logging
consoleloghandler = logging.StreamHandler()
65
consoleloghandler.setLevel(1)
66
67
68
consoleloghandler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s'))
log = logging.getLogger('gajim')
69
log.setLevel(logging.WARNING)
70
71
72
73
74
75
76
77
78
log.addHandler(consoleloghandler)
log.propagate = False
log = logging.getLogger('gajim.gajim')

# create intermediate loggers
logging.getLogger('gajim.c')
logging.getLogger('gajim.c.x')

import getopt
79
80
from common import i18n

81
82
83
84
85
def parseLogLevel(arg):
	if arg.isdigit():
		return int(arg)
	if arg.isupper():
		return getattr(logging, arg)
86
	raise ValueError(_('%s is not a valid loglevel'), repr(arg))
87
88
89
90
91
92
93
94

def parseLogTarget(arg):
	arg = arg.lower()
	if arg.startswith('.'): return arg[1:]
	if arg.startswith('gajim'): return arg
	return 'gajim.' + arg

def parseAndSetLogLevels(arg):
95
	for directive in arg.split(','):
96
		directive = directive.strip()
97
		targets, level = directive.rsplit('=', 1)
98
		level = parseLogLevel(level.strip())
99
100
101
102
103
104
105
106
107
		for target in targets.split('='):
			target = parseLogTarget(target.strip())
			if target == '':
				consoleloghandler.setLevel(level)
				print "consoleloghandler level set to %s" % level
			else:
				logger = logging.getLogger(target)
				logger.setLevel(level)
				print "Logger %s level set to %d" % (target, level)
108
109
110
111

def parseOpts():
	profile = ''
	verbose = False
112
	config_path = None
113
114

	try:
115
116
		shortargs = 'hqvl:p:c:'
		longargs = 'help quiet verbose loglevel= profile= config_path='
117
		opts, args = getopt.getopt(sys.argv[1:], shortargs, longargs.split())
118
119
120
121
122
123
	except getopt.error, msg:
		print msg
		print 'for help use --help'
		sys.exit(2)
	for o, a in opts:
		if o in ('-h', '--help'):
124
			print 'gajim [--help] [--quiet] [--verbose] [--loglevel subsystem=level[,subsystem=level[...]]] [--profile name] [--config-path]'
125
126
127
128
129
130
131
132
133
134
135
			sys.exit()
		elif o in ('-q', '--quiet'):
			consoleloghandler.setLevel(logging.CRITICAL)
			verbose = False
		elif o in ('-v', '--verbose'):
			consoleloghandler.setLevel(logging.INFO)
			verbose = True
		elif o in ('-p', '--profile'): # gajim --profile name
			profile = a
		elif o in ('-l', '--loglevel'):
			parseAndSetLogLevels(a)
136
137
138
		elif o in ('-c', '--config-path'):
			config_path = a
	return profile, verbose, config_path
139

140
profile, verbose, config_path = parseOpts()
141
del parseOpts, parseAndSetLogLevels, parseLogTarget, parseLogLevel
142

143
144
145
146
147
148
149
150
151
import locale
profile = unicode(profile, locale.getpreferredencoding())

import common.configpaths
common.configpaths.gajimpaths.init(config_path)
del config_path
common.configpaths.gajimpaths.init_profile(profile)
del profile

Yann Leboulanger's avatar
Yann Leboulanger committed
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
if os.name == 'nt':
	class MyStderr(object):
		_file = None
		_error = None
		def write(self, text):
			fname=os.path.join(common.configpaths.gajimpaths.root,
				os.path.split(sys.executable)[1]+'.log')
			if self._file is None and self._error is None:
				try:
					self._file = open(fname, 'a')
				except Exception, details:
					self._error = details
			if self._file is not None:
				self._file.write(text)
				self._file.flush()
		def flush(self):
			if self._file is not None:
				self._file.flush()

	sys.stderr = MyStderr()

Yann Leboulanger's avatar
Yann Leboulanger committed
173
174
175
176
177
178
179
# PyGTK2.10+ only throws a warning
import warnings
warnings.filterwarnings('error', module='gtk')
try:
	import gtk
except Warning, msg:
	if str(msg) == 'could not open display':
180
181
		print >> sys.stderr, _('Gajim needs X server to run. Quiting...')
		sys.exit()
Yann Leboulanger's avatar
Yann Leboulanger committed
182
183
warnings.resetwarnings()

184
185
186
187
if os.name == 'nt':
	import warnings
	warnings.filterwarnings(action='ignore')

188
pritext = ''
nkour's avatar
nkour committed
189

190
from common import exceptions
191
try:
192
193
194
	from common import gajim
except exceptions.DatabaseMalformed:
	pritext = _('Database Error')
195
	sectext = _('The database file (%s) cannot be read. Try to repair it (see http://trac.gajim.org/wiki/DatabaseBackup) or remove it (all history will be lost).') % common.logger.LOG_DB_PATH
196
197
198
199
200
201
202
203
204
205
else:
	from common import dbus_support
	if dbus_support.supported:
		import dbus

	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)
206
		except Exception:
207
208
209
210
211
212
213
214
			pass

	if gtk.pygtk_version < (2, 8, 0):
		pritext = _('Gajim needs PyGTK 2.8 or above')
		sectext = _('Gajim needs PyGTK 2.8 or above to run. Quiting...')
	elif gtk.gtk_version < (2, 8, 0):
		pritext = _('Gajim needs GTK 2.8 or above')
		sectext = _('Gajim needs GTK 2.8 or above to run. Quiting...')
215

216
217
218
219
220
221
222
223
	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':
			sectext = _('Please remove your current GTK+ runtime and install the latest stable version from %s') % 'http://gladewin32.sourceforge.net'
		else:
			sectext = _('Please make sure that GTK+ and PyGTK have libglade support in your system.')
224

225
	try:
226
227
228
229
230
231
232
233
234
		from common import check_paths
	except exceptions.PysqliteNotAvailable, e:
		pritext = _('Gajim needs PySQLite2 to run')
		sectext = str(e)

	if os.name == 'nt':
		try:
			import winsound # windows-only built-in module for playing wav
			import win32api # do NOT remove. we req this module
235
		except Exception:
236
237
			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'
238

239
if pritext:
Yann Leboulanger's avatar
Yann Leboulanger committed
240
	dlg = gtk.MessageDialog(None,
241
242
		gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
		gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
243
244
245
246
247
248

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

249
250
del pritext

251
import gtkexcepthook
252

253
import gobject
254
255
256
257
if not hasattr(gobject, 'timeout_add_seconds'):
	def timeout_add_seconds_fake(time_sec, *args):
		return gobject.timeout_add(time_sec * 1000, *args)
	gobject.timeout_add_seconds = timeout_add_seconds_fake
258

259
import re
260
import signal
261
import time
262
import math
nkour's avatar
typo    
nkour committed
263

264
import gtkgui_helpers
265
import notify
266
267
268
import message_control

from chat_control import ChatControlBase
269
270
271
from chat_control import ChatControl
from groupchat_control import GroupchatControl
from groupchat_control import PrivateChatControl
272
from atom_window import AtomWindow
273
from session import ChatControlSession
274

275
import common.sleepy
nkour's avatar
nkour committed
276

dkirov's avatar
dkirov committed
277
from common.xmpp import idlequeue
278
from common.zeroconf import connection_zeroconf
dkirov's avatar
dkirov committed
279
from common import nslookup
280
from common import proxy65_manager
281
from common import socks5
282
from common import helpers
283
from common import optparser
Yann Leboulanger's avatar
Yann Leboulanger committed
284
from common import dataforms
285

286
if verbose: gajim.verbose = True
287
del verbose
288

289
290
291
292
293
gajimpaths = common.configpaths.gajimpaths

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

294
295
296
import traceback
import errno

297
import dialogs
298
def pid_alive():
299
	try:
300
		pf = open(pid_filename)
js's avatar
js committed
301
	except IOError:
302
303
		# probably file not found
		return False
304
305
306
307

	try:
		pid = int(pf.read().strip())
		pf.close()
308
	except Exception:
309
310
311
312
		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
313

314
315
316
	if os.name == 'nt':
		try:
			from ctypes import (windll, c_ulong, c_int, Structure, c_char, POINTER, pointer, )
317
		except Exception:
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
			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)

354
		if get_p(pid) in ('python.exe', 'gajim.exe'):
355
356
			return True
		return False
Yann Leboulanger's avatar
Yann Leboulanger committed
357
	elif sys.platform == 'darwin':
358
359
360
		try:
			from osx import checkPID
			return checkPID(pid, 'Gajim.bin')
361
		except ImportError:
362
			return
363
	try:
364
365
366
		if not os.path.exists('/proc'):
			return True # no /proc, assume Gajim is running

367
		try:
Yann Leboulanger's avatar
Yann Leboulanger committed
368
			f = open('/proc/%d/cmdline'% pid)
369
370
371
		except IOError, e:
			if e.errno == errno.ENOENT:
				return False # file/pid does not exist
Yann Leboulanger's avatar
Yann Leboulanger committed
372
			raise
373
374

		n = f.read().lower()
375
		f.close()
376
377
378
		if n.find('gajim') < 0:
			return False
		return True # Running Gajim found at pid
379
	except Exception:
380
381
382
383
384
		traceback.print_exc()

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

if pid_alive():
dkirov's avatar
dkirov committed
387
388
389
	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
390
	pritext = _('Gajim is already running')
391
392
	sectext = _('Another instance of Gajim seems to be running\nRun anyway?')
	dialog = dialogs.YesNoDialog(pritext, sectext)
Yann Leboulanger's avatar
Yann Leboulanger committed
393
	dialog.popup()
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
394
	if dialog.run() != gtk.RESPONSE_YES:
395
		sys.exit(3)
Yann Leboulanger's avatar
Yann Leboulanger committed
396
	dialog.destroy()
jimpp's avatar
jimpp committed
397
	# run anyway, delete pid and useless global vars
dkirov's avatar
dkirov committed
398
399
	if os.path.exists(pid_filename):
		os.remove(pid_filename)
jimpp's avatar
jimpp committed
400
401
402
403
	del path_to_file
	del pix
	del pritext
	del sectext
404
	dialog.destroy()
405

dkirov's avatar
dkirov committed
406
407
408
409
410
# 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
411
412
413
414
415
416
417
418
419
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
420
del pid_dir
421
del f
422
423
424
425
426

def on_exit():
	# delete pid file on normal exit
	if os.path.exists(pid_filename):
		os.remove(pid_filename)
427
428
	# Shutdown GUI and save config
	gajim.interface.roster.prepare_quit()
Yann Leboulanger's avatar
Yann Leboulanger committed
429
	if sys.platform == 'darwin':
js's avatar
js committed
430
431
432
		try:
			import osx
			osx.shutdown()
js's avatar
js committed
433
		except ImportError:
js's avatar
js committed
434
			pass
435
436
437

import atexit
atexit.register(on_exit)
438

439
parser = optparser.OptionsParser(config_filename)
440

441
import roster_window
442
import profile_window
443
import config
444

dkirov's avatar
dkirov committed
445
class GlibIdleQueue(idlequeue.IdleQueue):
Yann Leboulanger's avatar
Yann Leboulanger committed
446
	'''
dkirov's avatar
dkirov committed
447
448
449
450
451
452
453
	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 = {}
Yann Leboulanger's avatar
Yann Leboulanger committed
454
		# time() is already called in glib, we just get the last value
455
456
		# overrides IdleQueue.current_time()
		self.current_time = lambda: gobject.get_current_time()
Yann Leboulanger's avatar
Yann Leboulanger committed
457

dkirov's avatar
dkirov committed
458
459
460
461
	def add_idle(self, fd, flags):
		''' this method is called when we plug a new idle object.
		Start listening for events from fd
		'''
462
		res = gobject.io_add_watch(fd, flags, self._process_events,
463
			priority=gobject.PRIORITY_LOW)
dkirov's avatar
dkirov committed
464
465
		# store the id of the watch, so that we can remove it on unplug
		self.events[fd] = res
Yann Leboulanger's avatar
Yann Leboulanger committed
466

467
468
469
	def _process_events(self, fd, flags):
		try:
			return self.process_events(fd, flags)
470
		except Exception:
471
472
473
474
			self.remove_idle(fd)
			self.add_idle(fd, flags)
			raise

dkirov's avatar
dkirov committed
475
476
477
478
	def remove_idle(self, fd):
		''' this method is called when we unplug a new idle object.
		Stop listening for events from fd
		'''
479
480
		if not fd in self.events:
			return
dkirov's avatar
dkirov committed
481
482
		gobject.source_remove(self.events[fd])
		del(self.events[fd])
Yann Leboulanger's avatar
Yann Leboulanger committed
483

dkirov's avatar
dkirov committed
484
485
	def process(self):
		self.check_time_events()
Yann Leboulanger's avatar
Yann Leboulanger committed
486

487
488
489
490
491
class PassphraseRequest:
	def __init__(self, keyid):
		self.keyid = keyid
		self.callbacks = []
		self.dialog_created = False
492
		self.dialog = None
493
494
		self.completed = False

495
496
497
498
	def interrupt(self):
		self.dialog.window.destroy()
		self.callbacks = []

499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
	def run_callback(self, account, callback):
		gajim.connections[account].gpg_passphrase(self.passphrase)
		callback()

	def add_callback(self, account, cb):
		if self.completed:
			self.run_callback(account, cb)
		else:
			self.callbacks.append((account, cb))
			if not self.dialog_created:
				self.create_dialog(account)

	def complete(self, passphrase):
		self.passphrase = passphrase
		self.completed = True
		if passphrase is not None:
515
			gobject.timeout_add_seconds(30, gajim.interface.forget_gpg_passphrase,
516
517
518
519
520
521
522
				self.keyid)
		for (account, cb) in self.callbacks:
			self.run_callback(account, cb)
		del self.callbacks

	def create_dialog(self, account):
		title = _('Passphrase Required')
523
		second = _('Enter GPG key passphrase for key %(keyid)s (account '
Yann Leboulanger's avatar
Yann Leboulanger committed
524
			'%(account)s).') % {'keyid': self.keyid, 'account': account}
525
526
527

		def _cancel():
			# user cancelled, continue without GPG
528
			self.complete(None)
529
530

		def _ok(passphrase, checked, count):
Yann Leboulanger's avatar
Yann Leboulanger committed
531
532
			result = gajim.connections[account].test_gpg_passphrase(passphrase)
			if result == 'ok':
533
				# passphrase is good
534
				self.complete(passphrase)
535
				return
Yann Leboulanger's avatar
Yann Leboulanger committed
536
537
538
539
540
541
542
543
			elif result == 'expired':
				dialogs.ErrorDialog(_('GPG key expired'),
					_('Your GPG key has expied, you will be connected to %s without '
					'OpenPGP.') % account)
				# Don't try to connect with GPG
				gajim.connections[account].continue_connect_info[2] = False
				self.complete(None)
				return
544
545

			if count < 3:
546
547
548
549
				# ask again
				dialogs.PassphraseDialog(_('Wrong Passphrase'),
					_('Please retype your GPG passphrase or press Cancel.'),
					ok_handler=(_ok, count + 1), cancel_handler=_cancel)
550
551
552
			else:
				# user failed 3 times, continue without GPG
				self.complete(None)
553

554
		self.dialog = dialogs.PassphraseDialog(title, second, ok_handler=(_ok, 1),
555
556
557
			cancel_handler=_cancel)
		self.dialog_created = True

558
class Interface:
559
560
561
562
563

################################################################################		
### Methods handling events from connection 
################################################################################	

564
	def handle_event_roster(self, account, data):
565
		#('ROSTER', account, array)
566
567
		# FIXME: Those methods depend to highly on each other
		# and the order in which they are called
568
		self.roster.fill_contacts_and_groups_dicts(data, account)
569
		self.roster.add_account_contacts(account)
570
		self.roster.fire_up_unread_messages_events(account)
571
572
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('Roster', (account, data))
573

574
575
	def handle_event_warning(self, unused, data):
		#('WARNING', account, (title_text, section_text))
576
		dialogs.WarningDialog(data[0], data[1])
577

578
579
	def handle_event_error(self, unused, data):
		#('ERROR', account, (title_text, section_text))
580
		dialogs.ErrorDialog(data[0], data[1])
581
582
583

	def handle_event_information(self, unused, data):
		#('INFORMATION', account, (title_text, section_text))
584
		dialogs.InformationDialog(data[0], data[1])
Yann Leboulanger's avatar
Yann Leboulanger committed
585

586
	def handle_event_ask_new_nick(self, account, data):
587
		#('ASK_NEW_NICK', account, (room_jid,))
588
		room_jid = data[0]
Brendan Taylor's avatar
Brendan Taylor committed
589
		gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
590
591
592
		if not gc_control and \
		room_jid in self.minimized_controls[account]:
			gc_control = self.minimized_controls[account][room_jid]
nicfit's avatar
nicfit committed
593
		if gc_control: # user may close the window before we are here
594
595
596
597
598
			title = _('Unable to join group chat')
			prompt = _('Your desired nickname in group chat %s is in use or '
				'registered by another occupant.\nPlease specify another nickname '
				'below:') % room_jid
			gc_control.show_change_nick_input_dialog(title, prompt)
599

600
	def handle_event_http_auth(self, account, data):
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
601
		#('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
602
		def response(account, iq_obj, answer):
603
604
605
			self.dialog.destroy()
			gajim.connections[account].build_http_auth_answer(iq_obj, answer)

606
607
608
		def on_yes(is_checked, account, iq_obj):
			response(account, iq_obj, 'yes')

Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
609
		sec_msg = _('Do you accept this request?')
Yann Leboulanger's avatar
Yann Leboulanger committed
610
611
		if gajim.get_number_of_connected_accounts() > 1:
			sec_msg = _('Do you accept this request on account %s?') % account
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
612
613
		if data[4]:
			sec_msg = data[4] + '\n' + sec_msg
614
615
616
		self.dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for '
			'%(url)s (id: %(id)s)') % {'method': data[0], 'url': data[1],
			'id': data[2]}, sec_msg, on_response_yes=(on_yes, account, data[3]),
617
			on_response_no=(response, account, data[3], 'no'))
618

Yann Leboulanger's avatar
Yann Leboulanger committed
619
	def handle_event_error_answer(self, account, array):
620
		#('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
621
		id, jid_from, errmsg, errcode = array
622
		if unicode(errcode) in ('403', '406') and id:
dkirov's avatar
dkirov committed
623
			# show the error dialog
624
			ft = self.instances['file_transfers']
dkirov's avatar
dkirov committed
625
626
627
			sid = id
			if len(id) > 3 and id[2] == '_':
				sid = id[3:]
628
			if sid in ft.files_props['s']:
dkirov's avatar
dkirov committed
629
630
				file_props = ft.files_props['s'][sid]
				file_props['error'] = -4
Yann Leboulanger's avatar
Yann Leboulanger committed
631
				self.handle_event_file_request_error(account,
632
					(jid_from, file_props, errmsg))
dkirov's avatar
dkirov committed
633
634
635
				conn = gajim.connections[account]
				conn.disconnect_transfer(file_props)
				return
636
		elif unicode(errcode) == '404':
dkirov's avatar
dkirov committed
637
			conn = gajim.connections[account]
dkirov's avatar
dkirov committed
638
639
640
			sid = id
			if len(id) > 3 and id[2] == '_':
				sid = id[3:]
641
			if sid in conn.files_props:
dkirov's avatar
dkirov committed
642
				file_props = conn.files_props[sid]
Yann Leboulanger's avatar
Yann Leboulanger committed
643
				self.handle_event_file_send_error(account,
dkirov's avatar
dkirov committed
644
645
646
					(jid_from, file_props))
				conn.disconnect_transfer(file_props)
				return
Brendan Taylor's avatar
Brendan Taylor committed
647

648
649
650
		ctrl = self.msg_win_mgr.get_control(jid_from, account)
		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
651

652
653
	def handle_event_con_type(self, account, con_type):
		# ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp'
654
		gajim.con_types[account] = con_type
655
		self.roster.draw_account(account)
656

657
658
659
660
661
662
663
664
	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])

665
666
	def unblock_signed_in_notifications(self, account):
		gajim.block_signed_in_notifications[account] = False
667

668
	def handle_event_status(self, account, status): # OUR status
669
		#('STATUS', account, status)
670
		model = self.roster.status_combobox.get_model()
Yann Leboulanger's avatar
Yann Leboulanger committed
671
672
673
674
675
676
		if status in ('offline', 'error'):
			for name in self.instances[account]['online_dialog'].keys():
				# .keys() is needed to not have a dictionary length changed during
				# iteration error
				self.instances[account]['online_dialog'][name].destroy()
				del self.instances[account]['online_dialog'][name]
677
678
679
			for request in self.gpg_passphrase.values():
				if request:
					request.interrupt()
680
		if status == 'offline':
nicfit's avatar
nicfit committed
681
			# sensitivity for this menuitem
682
683
			if gajim.get_number_of_connected_accounts() == 0:
				model[self.roster.status_message_menuitem_iter][3] = False
684
			gajim.block_signed_in_notifications[account] = True
685
		else:
686
687
688
689
			# 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
690
			gobject.timeout_add_seconds(30, self.unblock_signed_in_notifications, account)
nicfit's avatar
nicfit committed
691
692
693
694
			# sensitivity for this menuitem
			model[self.roster.status_message_menuitem_iter][3] = True

		# Inform all controls for this account of the connection state change
695
		ctrls = self.msg_win_mgr.get_controls()
696
		if account in self.minimized_controls:
697
698
699
			# Can not be the case when we remove account
			ctrls += self.minimized_controls[account].values()
		for ctrl in ctrls:
nicfit's avatar
nicfit committed
700
			if ctrl.account == account:
701
				if status == 'offline' or (status == 'invisible' and \
702
				gajim.connections[account].is_zeroconf):
nicfit's avatar
nicfit committed
703
704
705
706
707
					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()
708
709
				if ctrl.parent_win:
					ctrl.parent_win.redraw_tab(ctrl)
nicfit's avatar
nicfit committed
710

711
		self.roster.on_status_changed(account, status)
Yann Leboulanger's avatar
Yann Leboulanger committed
712
713
		if account in self.show_vcard_when_connect and status not in ('offline',
		'error'):
714
			self.edit_own_details(account)
715
716
		if self.remote_ctrl:
			self.remote_ctrl.raise_signal('AccountPresence', (status, account))
Yann Leboulanger's avatar
Yann Leboulanger committed
717

718
719
	def edit_own_details(self, account):
		jid = gajim.get_jid_from_account(account)
720
		if 'profile' not in self.instances[account]:
721
722
			self.instances[account]['profile'] = \
				profile_window.ProfileWindow(account)
723
724
			gajim.connections[account].request_vcard(jid)

725
	def handle_event_notify(self, account, array):
js's avatar
js committed
726
727
		# 'NOTIFY' (account, (jid, status, status message, resource,
		# priority, # keyID, timestamp, contact_nickname))
steve-e's avatar
steve-e committed
728
729
730
731
732
		#
		# Contact changed show

		# FIXME: Drop and rewrite...

733
734
		statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd',
			'invisible']
735
736
737
		# Ignore invalid show
		if array[1] not in statuss:
			return
738
		old_show = 0
Yann Leboulanger's avatar
Yann Leboulanger committed
739
		new_show = statuss.index(array[1])
740
		status_message = array[2]
741
		jid = array[0].split('/')[0]
Yann Leboulanger's avatar
Yann Leboulanger committed
742
		keyID = array[5]
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
743
		contact_nickname = array[7]
744

steve-e's avatar
steve-e committed
745
		# Get the proper keyID
746
		keyID = helpers.prepare_and_validate_gpg_keyID(account, jid, keyID)
steve-e's avatar
steve-e committed
747

748
749
750
751
		resource = array[3]
		if not resource:
			resource = ''
		priority = array[4]
752
		if gajim.jid_is_transport(jid):
753
			# It must be an agent
754
			ji = jid.replace('@', '')
755
756
		else:
			ji = jid
757

js's avatar
js committed
758
759
		highest = gajim.contacts. \
			get_contact_with_highest_priority(account, jid)
760
761
		was_highest = (highest and highest.resource == resource)

762
763
		conn = gajim.connections[account]

nkour's avatar
nkour committed
764
		# Update contact
765
		jid_list = gajim.contacts.get_jid_list(account)
766
		if ji in jid_list or jid == gajim.get_jid_from_account(account):
767
			lcontact = gajim.contacts.get_contacts(account, ji)
768
			contact1 = None
769
			resources = []
770
			for c in lcontact:
nkour's avatar
typo    
nkour committed
771
				resources.append(c.resource)
772
773
				if c.resource == resource:
					contact1 = c
774
					break
775

776
777
778
			if contact1:
				if contact1.show in statuss:
					old_show = statuss.index(contact1.show)
steve-e's avatar
steve-e committed
779
				# nick changed
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
780
781
782
783
				if contact_nickname is not None and \
				contact1.contact_name != contact_nickname:
					contact1.contact_name = contact_nickname
					self.roster.draw_contact(jid, account)
steve-e's avatar
steve-e committed
784

785
				if old_show == new_show and contact1.status == status_message and \
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
786
				contact1.priority == priority: # no change
787
					return
788
			else:
789
				contact1 = gajim.contacts.get_first_contact_from_jid(account, ji)
790
				if not contact1:
js's avatar
js committed
791
792
793
					# Presence of another resource of our
					# jid
					# Create self contact and add to roster
794
					if resource == conn.server_resource:
795
						return
796
797
798
					# Ignore offline presence of unknown self resource
					if new_show < 2:
						return
799
800
801
802
803
					contact1 = gajim.contacts.create_contact(jid=ji,
						name=gajim.nicks[account], groups=['self_contact'],
						show=array[1], status=status_message, sub='both', ask='none',
						priority=priority, keyID=keyID, resource=resource,
						mood=conn.mood, tune=conn.tune, activity=conn.activity)
804
					old_show = 0
805
					gajim.contacts.add_contact(account, contact1)
806
807
					lcontact.append(contact1)
				elif contact1.show in statuss:
808
					old_show = statuss.index(contact1.show)
js's avatar
js committed
809
				# FIXME: What am I?
810
811
				if (resources != [''] and (len(lcontact) != 1 or \
				lcontact[0].show != 'offline')) and jid.find('@') > 0:
812
					old_show = 0
813
					contact1 = gajim.contacts.copy_contact(contact1)
814
815
					lcontact.append(contact1)
				contact1.resource = resource
816

steve-e's avatar
steve-e committed
817
				self.roster.add_contact(contact1.jid, account)
818

819
820
			if contact1.jid.find('@') > 0 and len(lcontact) == 1:
				# It's not an agent
Yann Leboulanger's avatar
Yann Leboulanger committed
821
				if old_show == 0 and new_show > 1:
822
823
824
825
826
					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_seconds(5, self.roster.remove_newly_added,
827
						contact1.jid, account)
828
829
830
831
832
833
834
				elif old_show > 1 and new_show == 0 and conn.connected > 1:
					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)
					gobject.timeout_add_seconds(5, self.roster.remove_to_be_removed,
835
						contact1.jid, account)
Yann Leboulanger's avatar
Yann Leboulanger committed
836
837
838
839
840
841
842
843

			# unset custom status
			if (old_show == 0 and new_show > 1) or (old_show > 1 and new_show == 0\
			and conn.connected > 1):
				if account in self.status_sent_to_users and \
				jid in self.status_sent_to_users[account]:
					del self.status_sent_to_users[account][jid]

844
			contact1.show = array[1]
845
			contact1.status = status_message
846
847
			contact1.priority = priority
			contact1.keyID = keyID
848
849
850
851
852
			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
853
				contact1.last_status_time = time.localtime()
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
854
			contact1.contact_nickname = contact_nickname
steve-e's avatar
steve-e committed
855

856
		if gajim.jid_is_transport(jid):
857
			# It must be an agent
858
			if ji in jid_list:
859
				# Update existing iter and group counting
860
				self.roster.draw_contact(ji, account)
861
				self.roster.draw_group(_('Transports'), account)				
862
				if new_show > 1 and ji in gajim.transport_avatar[account]:
js's avatar
js committed
863
864
					# transport just signed in.
					# request avatars
865
866
					for jid_ in gajim.transport_avatar[account][ji]:
						conn.request_vcard(jid_)
js's avatar
js committed
867
868
				# transport just signed in/out, don't show
				# popup notifications for 30s
869
				account_ji = account + '/' + ji
870
				gajim.block_signed_in_notifications[account_ji] = True
js's avatar
js committed
871
				gobject.timeout_add_seconds(30,
872
					self.unblock_signed_in_notifications, account_ji)
873
874
			locations = (self.instances, self.instances[account])
			for location in locations:
875
				if 'add_contact' in location:
876
					if old_show == 0 and new_show > 1:
877
						location['add_contact'].transport_signed_in(jid)
878
879
						break
					elif old_show > 1 and new_show == 0:
880
						location['add_contact'].transport_signed_out(jid)
881
						break
882
		elif ji in jid_list:
883
			# It isn't an agent
884
			# reset chatstate if needed:
885
			# (when contact signs out or has errors)
886
			if array[1] in ('offline', 'error'):
887
				contact1.our_chatstate = contact1.chatstate = \
888
					contact1.composing_xep = None
889

js's avatar
js committed
890
891
				# TODO: This causes problems when another
				#	resource signs off!
892
				conn.remove_transfers_for_contact(contact1)
js's avatar
js committed
893
894
895

				# disable encryption, since if any messages are
				# lost they'll be not decryptable (note that
896
				# this contradicts XEP-0201 - trying to get that
js's avatar
js committed
897
				# in the XEP, though)
898
899
900

				# there won't be any sessions here if the contact terminated
				# their sessions before going offline (which we do)
901
				for sess in conn.get_sessions(ji):
902
903
					if (ji+'/'+resource) != str(sess.jid):
						continue
904
905
					if sess.control:
						sess.control.no_autonegotiation = False
906
907
					if sess.enable_encryption:
						sess.terminate_e2e()
908
						conn.delete_session(jid, sess.thread_id)
js's avatar
js committed
909

910
911
			self.roster.chg_contact_status(contact1, array[1], status_message,
				account)
912
			# Notifications
913
			if old_show < 2 and new_show > 1:
914
				notify.notify('contact_connected', jid, account, status_message)
915
				if self.remote_ctrl:
916
917
					self.remote_ctrl.raise_signal('ContactPresence', (account,
						array))
918

919
			elif old_show > 1 and new_show < 2:
920
				notify.notify('contact_disconnected', jid, account, status_message)
921
				if self.remote_ctrl:
922
					self.remote_ctrl.raise_signal('ContactAbsence', (account, array))
923
				# FIXME: stop non active file transfers
js's avatar
js committed
924
925
926
			# Status change (not connected/disconnected or
			# error (<1))
			elif new_show > 1:
927
928
				notify.notify('status_change', jid, account, [new_show,
					status_message])
929
				if self.remote_ctrl:
930
					self.remote_ctrl.raise_signal('ContactStatus', (account, array))
jimpp's avatar
jimpp committed
931
		else:
js's avatar
js committed
932
933
934
			# FIXME: MSN transport (CMSN1.2.1 and PyMSN) don't
			#	 follow the XEP, still the case in 2008.
			#	 It's maybe a GC_NOTIFY (specialy for MSN gc)
935
936
			self.handle_event_gc_notify(account, (jid, array[1], status_message,
				array[3], None, None, None, None, None, [], None, None))
js's avatar
js committed
937

938
		highest = gajim.contacts.get_contact_with_highest_priority(account, jid)
939
940
		is_highest = (highest and highest.resource == resource)

941
942
		# disconnect the session from the ctrl if the highest resource has changed
		if (was_highest and not is_highest) or (not was_highest and is_highest):
943
			ctrl = self.msg_win_mgr.get_control(jid, account)
944