connection.py 87.9 KB
Newer Older
1
##	common/connection.py
2
##
3
## Contributors for this file:
4
5
##	- Yann Le Boulanger <asterix@lagaule.org>
##	- Nikos Kouremenos <nkour@jabber.org>
6
##	- Dimitur Kirov <dkirov@gmail.com>
7
##	- Travis Shirk <travis@pobox.com>
8
##
9
10
11
12
13
14
15
16
## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
##                         Vincent Hanquez <tab@snarc.org>
## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
##                    Vincent Hanquez <tab@snarc.org>
##                    Nikos Kouremenos <nkour@jabber.org>
##                    Dimitur Kirov <dkirov@gmail.com>
##                    Travis Shirk <travis@pobox.com>
##                    Norman Rasmussen <norman@rasmussen.co.za>
17
18
19
20
21
22
23
24
25
26
27
##
## 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.
##

28
29
# kind of events we can wait for an answer
VCARD_PUBLISHED = 'vcard_published'
30
VCARD_ARRIVED = 'vcard_arrived'
31

32
import sys
33
import sha
34
35
import os
import time
36
import sre
37
import traceback
dkirov's avatar
dkirov committed
38
import select
dkirov's avatar
dkirov committed
39
import socket
40
41
import random
random.seed()
42
import signal
43
import base64
Yann Leboulanger's avatar
Yann Leboulanger committed
44
45
if os.name != 'nt':
	signal.signal(signal.SIGPIPE, signal.SIG_DFL)
dkirov's avatar
dkirov committed
46

47
from calendar import timegm
48

49
import common.xmpp
50

Yann Leboulanger's avatar
Yann Leboulanger committed
51
from common import helpers
Yann Leboulanger's avatar
Yann Leboulanger committed
52
53
from common import gajim
from common import GnuPG
dkirov's avatar
dkirov committed
54
import socks5
55
USE_GPG = GnuPG.USE_GPG
56
57
58
59

from common import i18n
_ = i18n._

60
61
62
63
64
65
66
HAS_IDLE = True
try:
	import common.idle as idle # when we launch gajim from sources
except:
	try:
		import idle # when Gajim is installed
	except:
nkour's avatar
nkour committed
67
		gajim.log.debug(_('Unable to load idle module'))
68
		HAS_IDLE = False
69

nkour's avatar
nkour committed
70
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
71
72
73
	'invisible']

distro_info = {
nkour's avatar
nkour committed
74
75
76
	'Arch Linux': '/etc/arch-release',
	'Aurox Linux': '/etc/aurox-release',
	'Conectiva Linux': '/etc/conectiva-release',
77
	'CRUX': '/usr/bin/crux',
nkour's avatar
nkour committed
78
79
80
81
82
83
84
85
86
	'Debian GNU/Linux': '/etc/debian_release',
	'Debian GNU/Linux': '/etc/debian_version',
	'Fedora Linux': '/etc/fedora-release',
	'Gentoo Linux': '/etc/gentoo-release',
	'Linux from Scratch': '/etc/lfs-release',
	'Mandrake Linux': '/etc/mandrake-release',
	'Slackware Linux': '/etc/slackware-release',
	'Slackware Linux': '/etc/slackware-version',
	'Solaris/Sparc': '/etc/release',
nkour's avatar
nkour committed
87
	'Source Mage': '/etc/sourcemage_version',
88
	'SUSE Linux': '/etc/SuSE-release',
nkour's avatar
nkour committed
89
90
91
	'Sun JDS': '/etc/sun-release',
	'PLD Linux': '/etc/pld-release',
	'Yellow Dog Linux': '/etc/yellowdog-release',
92
93
	# many distros use the /etc/redhat-release for compatibility
	# so Redhat is the last
nkour's avatar
nkour committed
94
	'Redhat Linux': '/etc/redhat-release'
95
96
97
}

def get_os_info():
Yann Leboulanger's avatar
Yann Leboulanger committed
98
	if os.name == 'nt':
nkour's avatar
nkour committed
99
100
		ver = os.sys.getwindowsversion()
		ver_format = ver[3], ver[0], ver[1]
nkour's avatar
nkour committed
101
102
103
104
105
106
		win_version = {
			(1, 4, 0): '95',
			(1, 4, 10): '98',
			(1, 4, 90): 'ME',
			(2, 4, 0): 'NT',
			(2, 5, 0): '2000',
nkour's avatar
nkour committed
107
108
			(2, 5, 1): 'XP',
			(2, 5, 2): '2003'
nkour's avatar
nkour committed
109
110
111
112
113
		}
		if win_version.has_key(ver_format):
			return 'Windows' + ' ' + win_version[ver_format]
		else:
			return 'Windows'
114
	elif os.name == 'posix':
115
116
		executable = 'lsb_release'
		params = ' --id --codename --release --short'
nkour's avatar
nkour committed
117
118
119
120
		full_path_to_executable = helpers.is_in_path(executable, return_abs_path = True)
		if full_path_to_executable:
			command = executable + params
			child_stdin, child_stdout = os.popen2(command)
121
			output = helpers.temp_failure_retry(child_stdout.readline).strip()
nkour's avatar
nkour committed
122
123
124
125
126
127
128
			child_stdout.close()
			child_stdin.close()
			# some distros put n/a in places so remove them
			pattern = sre.compile(r' n/a', sre.IGNORECASE)
			output = sre.sub(pattern, '', output)
			return output

129
		# lsb_release executable not available, so parse files
nkour's avatar
nkour committed
130
131
		for distro_name in distro_info:
			path_to_file = distro_info[distro_name]
132
			if os.path.exists(path_to_file):
nkour's avatar
nkour committed
133
134
135
				if os.access(path_to_file, os.X_OK):
					# the file is executable (f.e. CRUX)
					# yes, then run it and get the first line of output.
nkour's avatar
nkour committed
136
					text = helpers.get_output_of_command(path_to_file)[0]
137
138
139
140
141
142
143
144
145
146
147
148
				else:
					fd = open(path_to_file)
					text = fd.readline().strip() # get only first line
					fd.close()
					if path_to_file.endswith('version'):
						# sourcemage_version has all the info we need
						if not os.path.basename(path_to_file).startswith('sourcemage'):
							text = distro_name + ' ' + text
					elif path_to_file.endswith('aurox-release'):
						# file doesn't have version
						text = distro_name
					elif path_to_file.endswith('lfs-release'): # file just has version
nkour's avatar
nkour committed
149
						text = distro_name + ' ' + text
150
				return text
151

152
153
154
		# our last chance, ask uname and strip it
		uname_output = helpers.get_output_of_command('uname -a | cut -d" " -f1,3')
		if uname_output is not None:
nkour's avatar
nkour committed
155
			return uname_output[0] # only first line
156
	return 'N/A'
157

nkour's avatar
nkour committed
158
class Connection:
nkour's avatar
nkour committed
159
	'''Connection class'''
160
	def __init__(self, name):
161
162
		self.name = name
		self.connected = 0 # offline
163
		self.connection = None # xmpppy instance
164
		self.gpg = None
165
		self.vcard_sha = None
166
		self.vcard_shas = {} # sha of contacts
167
		self.status = ''
168
		self.old_show = ''
dkirov's avatar
dkirov committed
169
		# increase/decrease default timeout for server responses
170
		self.try_connecting_for_foo_secs = 45
171
		# holds the actual hostname to which we are connected
172
		self.connected_hostname = None
173
		self.time_to_reconnect = None
Yann Leboulanger's avatar
Yann Leboulanger committed
174
		self.new_account_info = None
175
		self.bookmarks = []
176
		self.on_purpose = False
177
		self.last_io = time.time()
178
		self.last_sent = []
dkirov's avatar
dkirov committed
179
		self.files_props = {}
180
		self.last_history_line = {}
181
		self.password = gajim.config.get_per('accounts', name, 'password')
dkirov's avatar
dkirov committed
182
		self.server_resource = gajim.config.get_per('accounts', name, 'resource')
dkirov's avatar
dkirov committed
183
184
185
186
		if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'):
			self.keepalives = gajim.config.get_per('accounts', self.name,'keep_alive_every_foo_secs')
		else:
			self.keepalives = 0
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
187
		self.privacy_rules_supported = False
188
		# Do we continue connection when we get roster (send presence,get vcard...)
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
189
		self.continue_connect_info = None
190
		# List of IDs we are waiting answers for {id: (type_of_request, data), }
191
		self.awaiting_answers = {}
192
193
194
		# List of IDs that will produce a timeout is answer doesn't arrive
		# {time_of_the_timeout: (id, message to send to gui), }
		self.awaiting_timeouts = {}
195
196
197
198
199
		if USE_GPG:
			self.gpg = GnuPG.GnuPG()
			gajim.config.set('usegpg', True)
		else:
			gajim.config.set('usegpg', False)
200
201
202
203
204

		try:
			idle.init()
		except:
			HAS_IDLE = False
dkirov's avatar
dkirov committed
205
		self.on_connect_success = None
206
		self.retrycount = 0
207
208
	# END __init__

209
210
	def get_full_jid(self, iq_obj):
		'''return the full jid (with resource) from an iq as unicode'''
211
		return helpers.parse_jid(str(iq_obj.getFrom()))
212
213
214

	def get_jid(self, iq_obj):
		'''return the jid (without resource) from an iq as unicode'''
215
216
		jid = self.get_full_jid(iq_obj)
		return gajim.get_jid_without_resource(jid)
217

218
	def put_event(self, ev):
dkirov's avatar
dkirov committed
219
220
		if gajim.handlers.has_key(ev[0]):
			gajim.handlers[ev[0]](self.name, ev[1])
221

222
	def dispatch(self, event, data):
223
		'''always passes account name as first param'''
dkirov's avatar
dkirov committed
224
		self.put_event((event, data))
225

226
227
228
229
230
231
	def add_sha(self, p):
		c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE)
		if self.vcard_sha is not None:
			c.setTagData('photo', self.vcard_sha)
		return p

232
	# this is in features.py but it is blocking
233
	def _discover(self, ns, jid, node = None):
234
235
		if not self.connection:
			return
236
237
238
		iq = common.xmpp.Iq(typ = 'get', to = jid, queryNS = ns)
		if node:
			iq.setQuerynode(node)
dkirov's avatar
dkirov committed
239
		self.connection.send(iq)
240
241

	def discoverItems(self, jid, node = None):
242
		'''According to JEP-0030: jid is mandatory,
243
		name, node, action is optional.'''
244
		self._discover(common.xmpp.NS_DISCO_ITEMS, jid, node)
245

246
247
	def discoverInfo(self, jid, node = None):
		'''According to JEP-0030:
248
			For identity: category, type is mandatory, name is optional.
249
250
251
			For feature: var is mandatory'''
		self._discover(common.xmpp.NS_DISCO_INFO, jid, node)

252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
	def node_to_dict(self, node):
		dict = {}
		for info in node.getChildren():
			name = info.getName()
			if name in ('ADR', 'TEL', 'EMAIL'): # we can have several
				if not dict.has_key(name):
					dict[name] = []
				entry = {}
				for c in info.getChildren():
					 entry[c.getName()] = c.getData()
				dict[name].append(entry)
			elif info.getChildren() == []:
				dict[name] = info.getData()
			else:
				dict[name] = {}
				for c in info.getChildren():
					 dict[name][c.getName()] = c.getData()
		return dict

271
	def _vCardCB(self, con, vc):
nkour's avatar
nkour committed
272
273
		'''Called when we receive a vCard
		Parse the vCard and send it to plugins'''
274
275
		if not vc.getTag('vCard'):
			return
276
		frm_iq = vc.getFrom()
277
		our_jid = gajim.get_jid_from_account(self.name)
278
279
		resource = ''
		if frm_iq:
280
281
			who = self.get_full_jid(vc)
			frm, resource = gajim.get_room_and_nick_from_fjid(who)
282
		else:
283
			frm = our_jid
284
		if vc.getTag('vCard').getNamespace() == common.xmpp.NS_VCARD:
285
			card = vc.getChildren()[0]
286
			vcard = self.node_to_dict(card)
287
			photo_decoded = None
Yann Leboulanger's avatar
Yann Leboulanger committed
288
			if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \
289
290
			vcard['PHOTO'].has_key('BINVAL'):
				photo = vcard['PHOTO']['BINVAL']
291
292
293
294
295
				try:
					photo_decoded = base64.decodestring(photo)
					avatar_sha = sha.sha(photo_decoded).hexdigest()
				except:
					avatar_sha = ''
296
297
298
299
300
301
302
			else:
				avatar_sha = ''

			if avatar_sha:
				card.getTag('PHOTO').setTagData('SHA', avatar_sha)

			# Save it to file
nkour's avatar
nkour committed
303
			path_to_file = os.path.join(gajim.VCARD_PATH, frm)
Yann Leboulanger's avatar
Yann Leboulanger committed
304
305
306
			fil = open(path_to_file, 'w')
			fil.write(str(card))
			fil.close()
307
308
			# Save the decoded avatar to a separate file too, and generate files for dbus notifications
			if photo_decoded:
309
				avatar_file = os.path.join(gajim.AVATAR_PATH, frm + '_notif_size_colored.png')
310
311
				if frm == our_jid and avatar_sha != self.vcard_sha:
					gajim.interface.save_avatar_files(frm, photo_decoded)
312
313
				elif frm != our_jid and (not os.path.exists(avatar_file) or \
					not self.vcard_shas.has_key(frm) or \
314
315
					avatar_sha != self.vcard_shas[frm]):
					gajim.interface.save_avatar_files(frm, photo_decoded)
316
317
318
319
320
321
			else:
				for ext in ('.jpeg', '.png', '_notif_size_bw.png',
					'_notif_size_colored.png'):
					path = os.path.join(gajim.AVATAR_PATH, frm + ext)
					if os.path.isfile(path):
						os.remove(path)
322
323
324
325
326
327

			if frm != our_jid:
				if avatar_sha:
					self.vcard_shas[frm] = avatar_sha
				elif self.vcard_shas.has_key(frm):
					del self.vcard_shas[frm]
328

329
330
			vcard['jid'] = frm
			vcard['resource'] = resource
331
			if frm == our_jid:
332
				self.dispatch('MYVCARD', vcard)
333
334
335
336
337
				# we re-send our presence with sha if has changed and if we are
				# not invisible
				if self.vcard_sha == avatar_sha:
					return
				self.vcard_sha = avatar_sha
338
339
				if STATUS_LIST[self.connected] == 'invisible':
					return
340
				sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
341
342
				prio = unicode(gajim.config.get_per('accounts', self.name,
					'priority'))
343
				p = common.xmpp.Presence(typ = None, priority = prio, show = sshow,
344
					status = self.status)
345
				p = self.add_sha(p)
dkirov's avatar
dkirov committed
346
				self.connection.send(p)
347
348
349
			else:
				self.dispatch('VCARD', vcard)

350
	def _gMailNewMailCB(self, con, gm):
nkour's avatar
nkour committed
351
		'''Called when we get notified of new mail messages in gmail account'''
352
353
354
		if not gm.getTag('new-mail'):
			return
		if gm.getTag('new-mail').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
355
			# we'll now ask the server for the exact number of new messages
356
			jid = gajim.get_jid_from_account(self.name)
357
			gajim.log.debug('Got notification of new gmail e-mail on %s. Asking the server for more info.' % jid)
358
359
360
361
			iq = common.xmpp.Iq(typ = 'get')
			iq.setAttr('id', '13')
			query = iq.setTag('query')
			query.setNamespace(common.xmpp.NS_GMAILNOTIFY)
dkirov's avatar
dkirov committed
362
			self.connection.send(iq)
363
			raise common.xmpp.NodeProcessed
364

365
	def _gMailQueryCB(self, con, gm):
nkour's avatar
nkour committed
366
		'''Called when we receive results from Querying the server for mail messages in gmail account'''
367
368
369
370
371
372
373
		if not gm.getTag('mailbox'):
			return
		if gm.getTag('mailbox').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
			newmsgs = gm.getTag('mailbox').getAttr('total-matched')
			if newmsgs != '0':
				# there are new messages
				jid = gajim.get_jid_from_account(self.name)
nkour's avatar
nkour committed
374
				gajim.log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid))
375
				self.dispatch('GMAIL_NOTIFY', (jid, newmsgs))
376
			raise common.xmpp.NodeProcessed
377

378
	def _messageCB(self, con, msg):
nkour's avatar
nkour committed
379
		'''Called when we receive a message'''
380
		msgtxt = msg.getBody()
Yann Leboulanger's avatar
Yann Leboulanger committed
381
		mtype = msg.getType()
nkour's avatar
nkour committed
382
		subject = msg.getSubject() # if not there, it's None
383
384
		tim = msg.getTimestamp()
		tim = time.strptime(tim, '%Y%m%dT%H:%M:%S')
385
		tim = time.localtime(timegm(tim))
386
		frm = self.get_full_jid(msg)
387
		jid = self.get_jid(msg)
Yann Leboulanger's avatar
Yann Leboulanger committed
388
		no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
389
		encrypted = False
nkour's avatar
nkour committed
390
		chatstate = None
391
		encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED)
392
		decmsg = ''
393
		# invitations
394
		invite = None
395
396
397
398
399
400
		if not encTag:
			invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER)
			if invite and not invite.getTag('invite'):
				invite = None
		delayed = msg.getTag('x', namespace = common.xmpp.NS_DELAY) != None
		msg_id = None
401
402
403
		# FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) do NOT RECOMMENDED
		# invitation
		# stanza (MUC JEP) remove in 2007, as we do not do NOT RECOMMENDED
404
		xtags = msg.getTags('x')
405
406
		for xtag in xtags:
			if xtag.getNamespace() == common.xmpp.NS_CONFERENCE and not invite:
407
408
409
				room_jid = xtag.getAttr('jid')
				self.dispatch('GC_INVITATION', (room_jid, frm, '', None))
				return
410
411
412
413
414
415
416
		# chatstates - look for chatstate tags in a message if not delayed
		if not delayed:
			children = msg.getChildren()
			for child in children:
				if child.getNamespace() == 'http://jabber.org/protocol/chatstates':
					chatstate = child.getName()
					break
417
418
419
420
421
422
423
			# No JEP-0085 support, fallback to JEP-0022
			if not chatstate:
				chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT)
				chatstate = 'active'
				if not msgtxt and chatstate_child.getTag('composing'):
 					chatstate = 'composing'
						
424
425
426
		if encTag and USE_GPG:
			#decrypt
			encmsg = encTag.getData()
427

428
429
430
431
432
			keyID = gajim.config.get_per('accounts', self.name, 'keyid')
			if keyID:
				decmsg = self.gpg.decrypt(encmsg, keyID)
		if decmsg:
			msgtxt = decmsg
433
			encrypted = True
Yann Leboulanger's avatar
Yann Leboulanger committed
434
		if mtype == 'error':
Yann Leboulanger's avatar
Yann Leboulanger committed
435
436
			self.dispatch('MSGERROR', (frm, msg.getErrorCode(), msg.getError(),
				msgtxt, tim))
Yann Leboulanger's avatar
Yann Leboulanger committed
437
		elif mtype == 'groupchat':
438
			if subject:
439
				self.dispatch('GC_SUBJECT', (frm, subject, msgtxt))
440
			else:
441
442
				if not msg.getTag('body'): #no <body>
					return
443
444
445
				# Ignore message from room in which we are not
				if not self.last_history_line.has_key(jid):
					return
Yann Leboulanger's avatar
Yann Leboulanger committed
446
				self.dispatch('GC_MSG', (frm, msgtxt, tim))
Yann Leboulanger's avatar
Yann Leboulanger committed
447
448
449
				if self.name not in no_log_for and jid in self.last_history_line \
					and not int(float(time.mktime(tim))) <= \
					self.last_history_line[jid]:
450
					gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
451
452
453
		elif mtype == 'chat': # it's type 'chat'
			if not msg.getTag('body') and chatstate is None: #no <body>
				return
454
455
			if msg.getTag('body') and self.name not in no_log_for and jid not in\
				no_log_for:
456
457
				gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject)
			self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
458
				chatstate, msg_id))
459
		else: # it's single message
460
461
			if self.name not in no_log_for and jid not in no_log_for:
				gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, subject = subject)
462
			if invite is not None:
463
464
465
466
467
468
				item = invite.getTag('invite')
				jid_from = item.getAttr('from')
				reason = item.getTagData('reason')
				item = invite.getTag('password')
				password = invite.getTagData('password')
				self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
469
			else:
470
				self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
471
					subject, chatstate, msg_id))
472
473
474
	# END messageCB

	def _presenceCB(self, con, prs):
nkour's avatar
nkour committed
475
		'''Called when we receive a presence'''
Yann Leboulanger's avatar
Yann Leboulanger committed
476
		ptype = prs.getType()
477
478
		if ptype == 'available':
			ptype = None
nkour's avatar
nkour committed
479
		gajim.log.debug('PresenceCB: %s' % ptype)
480
		is_gc = False # is it a GC presence ?
481
		sigTag = None
482
		avatar_sha = None
483
484
		xtags = prs.getTags('x')
		for x in xtags:
485
			if x.getNamespace().startswith(common.xmpp.NS_MUC):
486
487
				is_gc = True
			if x.getNamespace() == common.xmpp.NS_SIGNED:
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
488
				sigTag = x
489
490
			if x.getNamespace() == common.xmpp.NS_VCARD_UPDATE:
				avatar_sha = x.getTagData('photo')
491

492
		who = self.get_full_jid(prs)
493
		jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
Yann Leboulanger's avatar
Yann Leboulanger committed
494
		no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
495
		status = prs.getStatus()
Yann Leboulanger's avatar
Yann Leboulanger committed
496
497
498
499
500
		show = prs.getShow()
		if not show in STATUS_LIST:
			show = '' # We ignore unknown show
		if not ptype and not show:
			show = 'online'
Yann Leboulanger's avatar
Yann Leboulanger committed
501
		elif ptype == 'unavailable':
502
			show = 'offline'
503

Yann Leboulanger's avatar
Yann Leboulanger committed
504
505
506
507
508
509
510
511
512
513
514
		prio = prs.getPriority()
		try:
			prio = int(prio)
		except:
			prio = 0
		keyID = ''
		if sigTag and USE_GPG:
			#verify
			sigmsg = sigTag.getData()
			keyID = self.gpg.verify(status, sigmsg)

515
516
517
518
519
520
521
522
		if is_gc:
			if ptype == 'error':
				errmsg = prs.getError()
				errcode = prs.getErrorCode()
				if errcode == '502': # Internal Timeout:
					self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
						prio, keyID))
				elif errcode == '401': # password required to join
523
					self.dispatch('ERROR', (_('Unable to join room'),
524
525
						_('A password is required to join this room.')))
				elif errcode == '403': # we are banned
526
					self.dispatch('ERROR', (_('Unable to join room'),
527
528
						_('You are banned from this room.')))
				elif errcode == '404': # room does not exist
529
					self.dispatch('ERROR', (_('Unable to join room'),
530
531
						_('Such room does not exist.')))
				elif errcode == '405':
532
					self.dispatch('ERROR', (_('Unable to join room'),
533
534
						_('Room creation is restricted.')))
				elif errcode == '406':
535
					self.dispatch('ERROR', (_('Unable to join room'),
536
537
						_('Your registered nickname must be used.')))
				elif errcode == '407':
538
					self.dispatch('ERROR', (_('Unable to join room'),
539
540
						_('You are not in the members list.')))
				elif errcode == '409': # nick conflict
541
					# the jid_from in this case is FAKE JID: room_jid/nick
542
					# resource holds the bad nick so propose a new one
543
544
					proposed_nickname = resource + \
						gajim.config.get('gc_proposed_nick_char')
545
					room_jid = gajim.get_room_from_fjid(who)
546
					self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join room'),
547
		_('Your desired nickname is in use or registered by another occupant.\nPlease specify another nickname below:'), proposed_nickname))
548
549
550
551
				else:	# print in the window the error
					self.dispatch('ERROR_ANSWER', ('', jid_stripped,
						errmsg, errcode))
			if not ptype or ptype == 'unavailable':
552
553
				if gajim.config.get('log_contact_status_changes') and self.name\
					not in no_log_for and jid_stripped not in no_log_for:
554
					gajim.logger.write('gcstatus', who, status, show)
555
556
557
558
559
560
561
				self.dispatch('GC_NOTIFY', (jid_stripped, show, status, resource,
					prs.getRole(), prs.getAffiliation(), prs.getJid(),
					prs.getReason(), prs.getActor(), prs.getStatusCode(),
					prs.getNewNick()))
			return

		if ptype == 'subscribe':
562
563
564
			gajim.log.debug('subscribe request from %s' % who)
			if gajim.config.get('alwaysauth') or who.find("@") <= 0:
				if self.connection:
565
566
					p = common.xmpp.Presence(who, 'subscribed')
					p = self.add_sha(p)
dkirov's avatar
dkirov committed
567
					self.connection.send(p)
568
				if who.find("@") <= 0:
569
					self.dispatch('NOTIFY',
570
						(jid_stripped, 'offline', 'offline', resource, prio, keyID))
571
572
573
574
			else:
				if not status:
					status = _('I would like to add you to my roster.')
				self.dispatch('SUBSCRIBE', (who, status))
Yann Leboulanger's avatar
Yann Leboulanger committed
575
		elif ptype == 'subscribed':
576
			self.dispatch('SUBSCRIBED', (jid_stripped, resource))
nkour's avatar
nkour committed
577
			# BE CAREFUL: no con.updateRosterItem() in a callback
dkirov's avatar
dkirov committed
578
			gajim.log.debug(_('we are now subscribed to %s') % who)
Yann Leboulanger's avatar
Yann Leboulanger committed
579
		elif ptype == 'unsubscribe':
nkour's avatar
nkour committed
580
			gajim.log.debug(_('unsubscribe request from %s') % who)
Yann Leboulanger's avatar
Yann Leboulanger committed
581
		elif ptype == 'unsubscribed':
dkirov's avatar
dkirov committed
582
			gajim.log.debug(_('we are now unsubscribed from %s') % who)
583
			self.dispatch('UNSUBSCRIBED', jid_stripped)
Yann Leboulanger's avatar
Yann Leboulanger committed
584
		elif ptype == 'error':
585
586
			errmsg = prs.getError()
			errcode = prs.getErrorCode()
587
			if errcode == '502': # Internal Timeout:
588
589
				self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
					prio, keyID))
nkour's avatar
nkour committed
590
			else:	# print in the window the error
591
				self.dispatch('ERROR_ANSWER', ('', jid_stripped,
592
					errmsg, errcode))
593

594
		if avatar_sha and ptype != 'error':
595
596
597
			if self.vcard_shas.has_key(jid_stripped):
				if avatar_sha != self.vcard_shas[jid_stripped]:
					# avatar has been updated
nkour's avatar
nkour committed
598
					self.request_vcard(jid_stripped)
599
600
			else:
				self.vcard_shas[jid_stripped] = avatar_sha
601
		if not ptype or ptype == 'unavailable':
602
603
			if gajim.config.get('log_contact_status_changes') and self.name\
				not in no_log_for and jid_stripped not in no_log_for:
604
				gajim.logger.write('status', jid_stripped, status, show)
605
606
			self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio,
				keyID))
607
608
	# END presenceCB

609
	def _disconnectedCB(self):
nkour's avatar
nkour committed
610
		'''Called when we are disconnected'''
611
		gajim.log.debug('disconnectedCB')
612
613
614
615
616
617
		if not self.connection:
			return
		self.connected = 0
		self.dispatch('STATUS', 'offline')
		self.connection = None
		if not self.on_purpose:
618
			self.dispatch('ERROR',
619
620
			(_('Connection with account "%s" has been lost') % self.name,
			_('To continue sending and receiving messages, you will need to reconnect.')))
621
		self.on_purpose = False
622

623
	# END disconenctedCB
624
625

	def _reconnect(self):
626
627
		# Do not try to reco while we are already trying
		self.time_to_reconnect = None
628
		gajim.log.debug('reconnect')
629
		self.retrycount += 1
630
		signed = self.get_signed_msg(self.status)
dkirov's avatar
dkirov committed
631
		self.on_connect_auth = self._init_roster
632
		self.connect_and_init(self.old_show, self.status, signed)
633

634
635
636
637
		if self.connected < 2: #connection failed
			if self.retrycount > 10:
				self.connected = 0
				self.dispatch('STATUS', 'offline')
638
				self.dispatch('ERROR',
639
640
641
642
643
				(_('Connection with account "%s" has been lost') % self.name,
				_('To continue sending and receiving messages, you will need to reconnect.')))
				self.retrycount = 0
				return
			if self.retrycount > 5:
dkirov's avatar
dkirov committed
644
				self.time_to_reconnect = 20
645
			else:
dkirov's avatar
dkirov committed
646
647
				self.time_to_reconnect = 10
			gajim.idlequeue.set_alarm(self._reconnect_alarm, self.time_to_reconnect)
648
		else:
dkirov's avatar
dkirov committed
649
			# reconnect succeeded
650
651
			self.time_to_reconnect = None
			self.retrycount = 0
652

653
	def _disconnectedReconnCB(self):
nkour's avatar
nkour committed
654
		'''Called when we are disconnected'''
655
656
657
658
659
660
661
662
663
664
665
		gajim.log.debug('disconnectedReconnCB')
		if not self.connection:
			return
		self.old_show = STATUS_LIST[self.connected]
		self.connected = 0
		self.dispatch('STATUS', 'offline')
		self.connection = None
		if not self.on_purpose:
			if gajim.config.get_per('accounts', self.name, 'autoreconnect'):
				self.connected = 1
				self.dispatch('STATUS', 'connecting')
dkirov's avatar
dkirov committed
666
667
				self.time_to_reconnect = 10
				gajim.idlequeue.set_alarm(self._reconnect_alarm, 10)
668
			else:
669
				self.dispatch('ERROR',
670
671
672
673
				(_('Connection with account "%s" has been lost') % self.name,
				_('To continue sending and receiving messages, you will need to reconnect.')))
		self.on_purpose = False
	# END disconenctedReconnCB
674

675
676
	def _bytestreamErrorCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamErrorCB')
677
		id = unicode(iq_obj.getAttr('id'))
678
		query = iq_obj.getTag('query')
679
		jid = self.get_jid(iq_obj)
680
681
682
683
		id = id[3:]
		if not self.files_props.has_key(id):
			return
		file_props = self.files_props[id]
dkirov's avatar
dkirov committed
684
		file_props['error'] = -4
685
686
		self.dispatch('FILE_REQUEST_ERROR', (jid, file_props))
		raise common.xmpp.NodeProcessed
687

dkirov's avatar
dkirov committed
688
689
	def _bytestreamSetCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamSetCB')
690
691
		target = unicode(iq_obj.getAttr('to'))
		id = unicode(iq_obj.getAttr('id'))
dkirov's avatar
dkirov committed
692
		query = iq_obj.getTag('query')
693
		sid = unicode(query.getAttr('sid'))
dkirov's avatar
dkirov committed
694
695
		file_props = gajim.socks5queue.get_file_props(
			self.name, sid)
dkirov's avatar
dkirov committed
696
697
698
		streamhosts=[]
		for item in query.getChildren():
			if item.getName() == 'streamhost':
699
				host_dict={
700
701
702
					'state': 0,
					'target': target,
					'id': id,
dkirov's avatar
dkirov committed
703
					'sid': sid,
704
					'initiator': self.get_full_jid(iq_obj)
705
				}
dkirov's avatar
dkirov committed
706
				for attr in item.getAttrs():
707
					host_dict[attr] = item.getAttr(attr)
dkirov's avatar
dkirov committed
708
				streamhosts.append(host_dict)
709
710
711
712
		if file_props is None:
			if self.files_props.has_key(sid):
				file_props = self.files_props[sid]
				file_props['fast'] = streamhosts
nkour's avatar
nkour committed
713
				if file_props['type'] == 's': # FIXME: remove fast xmlns
714
715
716
717
718
719
720
721
					# only psi do this

					if file_props.has_key('streamhosts'):
						file_props['streamhosts'].extend(streamhosts)
					else:
						file_props['streamhosts'] = streamhosts
					if not gajim.socks5queue.get_file_props(self.name, sid):
						gajim.socks5queue.add_file_props(self.name, file_props)
722
					gajim.socks5queue.connect_to_hosts(self.name, sid,
723
						self.send_success_connect_reply, None)
724
				raise common.xmpp.NodeProcessed
725

726
727
		file_props['streamhosts'] = streamhosts
		if file_props['type'] == 'r':
728
			gajim.socks5queue.connect_to_hosts(self.name, sid,
729
730
				self.send_success_connect_reply, self._connect_error)
		raise common.xmpp.NodeProcessed
731

732
	def send_success_connect_reply(self, streamhost):
733
		''' send reply to the initiator of FT that we
734
735
736
737
		made a connection
		'''
		if streamhost is None:
			return None
738
		iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
739
740
741
742
743
744
			frm = streamhost['target'])
		iq.setAttr('id', streamhost['id'])
		query = iq.setTag('query')
		query.setNamespace(common.xmpp.NS_BYTESTREAM)
		stream_tag = query.setTag('streamhost-used')
		stream_tag.setAttr('jid', streamhost['jid'])
dkirov's avatar
dkirov committed
745
		self.connection.send(iq)
746

dkirov's avatar
dkirov committed
747
	def _connect_error(self, to, _id, sid, code = 404):
748
		msg_dict = {
749
750
751
			404: 'Could not connect to given hosts',
			405: 'Cancel',
			406: 'Not acceptable',
752
753
754
		}
		msg = msg_dict[code]
		iq = None
755
		iq = common.xmpp.Protocol(name = 'iq', to = to,
756
			typ = 'error')
dkirov's avatar
dkirov committed
757
		iq.setAttr('id', _id)
758
		err = iq.setTag('error')
759
		err.setAttr('code', unicode(code))
760
		err.setData(msg)
dkirov's avatar
dkirov committed
761
		self.connection.send(iq)
dkirov's avatar
dkirov committed
762
763
		if code == 404:
			file_props = gajim.socks5queue.get_file_props(self.name, sid)
dkirov's avatar
dkirov committed
764
765
766
767
			if file_props is not None:
				self.disconnect_transfer(file_props)
				file_props['error'] = -3
				self.dispatch('FILE_REQUEST_ERROR', (to, file_props))
768

dkirov's avatar
dkirov committed
769
770
	def _bytestreamResultCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamResultCB')
771
		frm = self.get_full_jid(iq_obj)
772
		real_id = unicode(iq_obj.getAttr('id'))
dkirov's avatar
dkirov committed
773
		query = iq_obj.getTag('query')
774
775
776
		streamhost = None
		try:
			streamhost = query.getTag('streamhost')
777
		except:
778
779
			pass
		if streamhost is not None: # this is a result for proxy request
dkirov's avatar
dkirov committed
780
781
782
783
784
			jid = None
			try:
				jid = streamhost.getAttr('jid')
			except:
				raise common.xmpp.NodeProcessed
785
786
787
			proxyhosts = []
			for item in query.getChildren():
				if item.getName() == 'streamhost':
788
789
790
791
					host = item.getAttr('host')
					port = item.getAttr('port')
					jid = item.getAttr('jid')
					conf = gajim.config
dkirov's avatar
dkirov committed
792
					conf.add_per('ft_proxies65_cache', jid)
793
794
795
796
797
798
					conf.set_per('ft_proxies65_cache', jid,
						'host', unicode(host))
					conf.set_per('ft_proxies65_cache', jid,
						'port', int(port))
					conf.set_per('ft_proxies65_cache', jid,
						'jid', unicode(jid))
799
800
801
802
			raise common.xmpp.NodeProcessed
		try:
			streamhost =  query.getTag('streamhost-used')
		except: # this bytestream result is not what we need
dkirov's avatar
dkirov committed
803
			pass
804
805
806
807
808
		id = real_id[3:]
		if self.files_props.has_key(id):
			file_props = self.files_props[id]
		else:
			raise common.xmpp.NodeProcessed
809
		if streamhost is None:
dkirov's avatar
dkirov committed
810
811
812
813
814
815
816
817
818
819
			# proxy approves the activate query
			if real_id[:3] == 'au_':
				id = real_id[3:]
				if not file_props.has_key('streamhost-used') or \
					file_props['streamhost-used'] is False:
					raise common.xmpp.NodeProcessed
				if not file_props.has_key('proxyhosts'):
					raise common.xmpp.NodeProcessed
				for host in file_props['proxyhosts']:
					if host['initiator'] == frm and \
820
					unicode(query.getAttr('sid')) == file_props['sid']:
dkirov's avatar
dkirov committed
821
822
823
824
						gajim.socks5queue.activate_proxy(host['idx'])
						break
			raise common.xmpp.NodeProcessed
		jid = streamhost.getAttr('jid')
dkirov's avatar
dkirov committed
825
826
827
		if file_props.has_key('streamhost-used') and \
			file_props['streamhost-used'] is True:
			raise common.xmpp.NodeProcessed
828

dkirov's avatar
dkirov committed
829
830
831
		if real_id[:3] == 'au_':
			gajim.socks5queue.send_file(file_props, self.name)
			raise common.xmpp.NodeProcessed
832

833
834
835
836
837
		proxy = None
		if file_props.has_key('proxyhosts'):
			for proxyhost in file_props['proxyhosts']:
				if proxyhost['jid'] == jid:
					proxy = proxyhost
838

839
		if proxy != None:
dkirov's avatar
dkirov committed
840
			file_props['streamhost-used'] = True
841
			if not file_props.has_key('streamhosts'):
dkirov's avatar
dkirov committed
842
				file_props['streamhosts'] = []
843
844
			file_props['streamhosts'].append(proxy)
			file_props['is_a_proxy'] = True
845
			receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props)
846
847
848
849
			gajim.socks5queue.add_receiver(self.name, receiver)
			proxy['idx'] = receiver.queue_idx
			gajim.socks5queue.on_success = self.proxy_auth_ok
			raise common.xmpp.NodeProcessed
850

dkirov's avatar
dkirov committed
851
		else:
852
853
854
855
			gajim.socks5queue.send_file(file_props, self.name)
			if file_props.has_key('fast'):
				fasts = file_props['fast']
				if len(fasts) > 0:
856
857
					self._connect_error(frm, fasts[0]['id'], file_props['sid'],
						code = 406)
858

dkirov's avatar
dkirov committed
859
		raise common.xmpp.NodeProcessed
860

dkirov's avatar
dkirov committed
861
862
863
864
865
866
	def remove_all_transfers(self):
		''' stops and removes all active connections from the socks5 pool '''
		for file_props in self.files_props.values():
			self.remove_transfer(file_props, remove_from_list = False)
		del(self.files_props)
		self.files_props = {}
867

dkirov's avatar
dkirov committed
868
	def remove_transfer(self, file_props,