connection.py 90.1 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
from encodings.punycode import punycode_encode
49

50
import common.xmpp
51

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

from common import i18n
_ = i18n._

61
62
63
64
65
66
67
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
68
		gajim.log.debug(_('Unable to load idle module'))
69
		HAS_IDLE = False
70

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

distro_info = {
nkour's avatar
nkour committed
75
76
77
	'Arch Linux': '/etc/arch-release',
	'Aurox Linux': '/etc/aurox-release',
	'Conectiva Linux': '/etc/conectiva-release',
78
	'CRUX': '/usr/bin/crux',
nkour's avatar
nkour committed
79
80
81
82
83
84
85
86
87
	'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
88
	'Source Mage': '/etc/sourcemage_version',
89
	'SUSE Linux': '/etc/SuSE-release',
nkour's avatar
nkour committed
90
91
92
	'Sun JDS': '/etc/sun-release',
	'PLD Linux': '/etc/pld-release',
	'Yellow Dog Linux': '/etc/yellowdog-release',
93
94
	# many distros use the /etc/redhat-release for compatibility
	# so Redhat is the last
nkour's avatar
nkour committed
95
	'Redhat Linux': '/etc/redhat-release'
96
97
98
}

def get_os_info():
Yann Leboulanger's avatar
Yann Leboulanger committed
99
	if os.name == 'nt':
nkour's avatar
nkour committed
100
101
		ver = os.sys.getwindowsversion()
		ver_format = ver[3], ver[0], ver[1]
nkour's avatar
nkour committed
102
103
104
105
106
107
		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
108
109
			(2, 5, 1): 'XP',
			(2, 5, 2): '2003'
nkour's avatar
nkour committed
110
111
112
113
114
		}
		if win_version.has_key(ver_format):
			return 'Windows' + ' ' + win_version[ver_format]
		else:
			return 'Windows'
115
	elif os.name == 'posix':
116
117
		executable = 'lsb_release'
		params = ' --id --codename --release --short'
nkour's avatar
nkour committed
118
119
120
121
		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)
122
			output = helpers.temp_failure_retry(child_stdout.readline).strip()
nkour's avatar
nkour committed
123
124
125
126
127
128
129
			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

130
		# lsb_release executable not available, so parse files
nkour's avatar
nkour committed
131
132
		for distro_name in distro_info:
			path_to_file = distro_info[distro_name]
133
			if os.path.exists(path_to_file):
nkour's avatar
nkour committed
134
135
136
				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
137
					text = helpers.get_output_of_command(path_to_file)[0]
138
139
140
141
142
143
144
145
146
147
148
149
				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
150
						text = distro_name + ' ' + text
151
				return text
152

153
154
155
		# 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
156
			return uname_output[0] # only first line
157
	return 'N/A'
158

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

		try:
			idle.init()
		except:
			HAS_IDLE = False
dkirov's avatar
dkirov committed
206
		self.on_connect_success = None
207
		self.retrycount = 0
208
		self.jids_for_auto_auth = [] # list of jid to auto-authorize
209
		self.room_jids = [] # list of gc jids so that vcard are saved in a folder
210
211
	# END __init__

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

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

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

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

229
230
231
232
233
234
	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

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

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

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

255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
	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

274
275
	def save_vcard_to_hd(self, full_jid, card):
		jid, nick = gajim.get_room_and_nick_from_fjid(full_jid)
276
		nick = nick.replace('/', '_')
277
278
		puny_jid = punycode_encode(jid)
		path = os.path.join(gajim.VCARD_PATH, puny_jid)
279
280
281
282
283
284
285
		if jid in self.room_jids:
			# remove room_jid file if needed
			if os.path.isfile(path):
				os.remove(path)
			# create folder if needed
			if not os.path.isdir(path):
				os.mkdir(path, 0700)
286
287
			puny_nick = punycode_encode(nick)
			path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
288
289
290
291
292
293
		else:
			path_to_file = path
		fil = open(path_to_file, 'w')
		fil.write(str(card))
		fil.close()

294
	def _vCardCB(self, con, vc):
nkour's avatar
nkour committed
295
296
		'''Called when we receive a vCard
		Parse the vCard and send it to plugins'''
297
298
		if not vc.getTag('vCard'):
			return
299
		frm_iq = vc.getFrom()
300
		our_jid = gajim.get_jid_from_account(self.name)
301
302
		resource = ''
		if frm_iq:
303
304
			who = self.get_full_jid(vc)
			frm, resource = gajim.get_room_and_nick_from_fjid(who)
305
		else:
306
			who = frm = our_jid
307
		if vc.getTag('vCard').getNamespace() == common.xmpp.NS_VCARD:
308
			card = vc.getChildren()[0]
309
			vcard = self.node_to_dict(card)
310
			photo_decoded = None
Yann Leboulanger's avatar
Yann Leboulanger committed
311
			if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \
312
313
			vcard['PHOTO'].has_key('BINVAL'):
				photo = vcard['PHOTO']['BINVAL']
314
315
316
317
318
				try:
					photo_decoded = base64.decodestring(photo)
					avatar_sha = sha.sha(photo_decoded).hexdigest()
				except:
					avatar_sha = ''
319
320
321
322
323
324
325
			else:
				avatar_sha = ''

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

			# Save it to file
326
			self.save_vcard_to_hd(who, card)
327
			# Save the decoded avatar to a separate file too, and generate files for dbus notifications
328
			puny_jid = punycode_encode(frm)
329
330
331
			puny_nick = None
			begin_path = os.path.join(gajim.AVATAR_PATH, puny_jid)
			if frm in self.room_jids:
332
				puny_nick = punycode_encode(resource.replace('/', '_'))
333
334
335
336
				# create folder if needed
				if not os.path.isdir(begin_path):
					os.mkdir(begin_path, 0700)
				begin_path = os.path.join(begin_path, puny_nick)
337
			if photo_decoded:
338
				avatar_file = begin_path + '_notif_size_colored.png'
339
				if frm == our_jid and avatar_sha != self.vcard_sha:
340
					gajim.interface.save_avatar_files(frm, photo_decoded, puny_nick)
341
342
				elif frm != our_jid and (not os.path.exists(avatar_file) or \
					not self.vcard_shas.has_key(frm) or \
343
					avatar_sha != self.vcard_shas[frm]):
344
					gajim.interface.save_avatar_files(frm, photo_decoded, puny_nick)
345
346
347
			else:
				for ext in ('.jpeg', '.png', '_notif_size_bw.png',
					'_notif_size_colored.png'):
348
					path = begin_path + ext
349
350
					if os.path.isfile(path):
						os.remove(path)
351
352
353
354
355
356

			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]
357

358
359
			vcard['jid'] = frm
			vcard['resource'] = resource
360
			if frm == our_jid:
361
				self.dispatch('MYVCARD', vcard)
362
363
364
365
366
				# 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
367
368
				if STATUS_LIST[self.connected] == 'invisible':
					return
369
				sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
370
371
				prio = unicode(gajim.config.get_per('accounts', self.name,
					'priority'))
372
				p = common.xmpp.Presence(typ = None, priority = prio, show = sshow,
373
					status = self.status)
374
				p = self.add_sha(p)
dkirov's avatar
dkirov committed
375
				self.connection.send(p)
376
377
378
			else:
				self.dispatch('VCARD', vcard)

379
	def _gMailNewMailCB(self, con, gm):
nkour's avatar
nkour committed
380
		'''Called when we get notified of new mail messages in gmail account'''
381
382
383
		if not gm.getTag('new-mail'):
			return
		if gm.getTag('new-mail').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
384
			# we'll now ask the server for the exact number of new messages
385
			jid = gajim.get_jid_from_account(self.name)
386
			gajim.log.debug('Got notification of new gmail e-mail on %s. Asking the server for more info.' % jid)
387
388
389
390
			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
391
			self.connection.send(iq)
392
			raise common.xmpp.NodeProcessed
393

394
	def _gMailQueryCB(self, con, gm):
nkour's avatar
nkour committed
395
		'''Called when we receive results from Querying the server for mail messages in gmail account'''
396
397
398
399
400
401
402
		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
403
				gajim.log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid))
404
				self.dispatch('GMAIL_NOTIFY', (jid, newmsgs))
405
			raise common.xmpp.NodeProcessed
406

407
	def _messageCB(self, con, msg):
nkour's avatar
nkour committed
408
		'''Called when we receive a message'''
409
		msgtxt = msg.getBody()
Yann Leboulanger's avatar
Yann Leboulanger committed
410
		mtype = msg.getType()
nkour's avatar
nkour committed
411
		subject = msg.getSubject() # if not there, it's None
412
413
		tim = msg.getTimestamp()
		tim = time.strptime(tim, '%Y%m%dT%H:%M:%S')
414
		tim = time.localtime(timegm(tim))
415
		frm = self.get_full_jid(msg)
416
		jid = self.get_jid(msg)
Yann Leboulanger's avatar
Yann Leboulanger committed
417
		no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
418
		encrypted = False
nkour's avatar
nkour committed
419
		chatstate = None
420
		encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED)
421
		decmsg = ''
422
		# invitations
423
		invite = None
424
425
426
427
428
429
		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
430
		composing_jep = None
431
432
433
		# 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
434
		xtags = msg.getTags('x')
435
436
		for xtag in xtags:
			if xtag.getNamespace() == common.xmpp.NS_CONFERENCE and not invite:
437
438
439
				room_jid = xtag.getAttr('jid')
				self.dispatch('GC_INVITATION', (room_jid, frm, '', None))
				return
440
441
		# chatstates - look for chatstate tags in a message if not delayed
		if not delayed:
442
			composing_jep = False
443
444
445
446
			children = msg.getChildren()
			for child in children:
				if child.getNamespace() == 'http://jabber.org/protocol/chatstates':
					chatstate = child.getName()
447
					composing_jep = 'JEP-0085'
448
					break
449
450
451
			# No JEP-0085 support, fallback to JEP-0022
			if not chatstate:
				chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT)
Yann Leboulanger's avatar
Yann Leboulanger committed
452
453
				if chatstate_child:
					chatstate = 'active'
454
					composing_jep = 'JEP-0022'
Yann Leboulanger's avatar
Yann Leboulanger committed
455
456
					if not msgtxt and chatstate_child.getTag('composing'):
 						chatstate = 'composing'
457
						
458
459
460
		if encTag and USE_GPG:
			#decrypt
			encmsg = encTag.getData()
461

462
463
464
465
466
			keyID = gajim.config.get_per('accounts', self.name, 'keyid')
			if keyID:
				decmsg = self.gpg.decrypt(encmsg, keyID)
		if decmsg:
			msgtxt = decmsg
467
			encrypted = True
Yann Leboulanger's avatar
Yann Leboulanger committed
468
		if mtype == 'error':
Yann Leboulanger's avatar
Yann Leboulanger committed
469
470
			self.dispatch('MSGERROR', (frm, msg.getErrorCode(), msg.getError(),
				msgtxt, tim))
Yann Leboulanger's avatar
Yann Leboulanger committed
471
		elif mtype == 'groupchat':
472
			if subject:
473
				self.dispatch('GC_SUBJECT', (frm, subject, msgtxt))
474
			else:
475
476
				if not msg.getTag('body'): #no <body>
					return
477
478
479
				# 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
480
				self.dispatch('GC_MSG', (frm, msgtxt, tim))
Yann Leboulanger's avatar
Yann Leboulanger committed
481
482
483
				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]:
484
					gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
485
486
487
		elif mtype == 'chat': # it's type 'chat'
			if not msg.getTag('body') and chatstate is None: #no <body>
				return
488
489
			if msg.getTag('body') and self.name not in no_log_for and jid not in\
				no_log_for:
490
491
				gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject)
			self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
492
				chatstate, msg_id, composing_jep))
493
		else: # it's single message
494
495
			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)
496
			if invite is not None:
497
498
499
500
501
502
				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))
503
			else:
504
				self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
505
					subject, chatstate, msg_id, composing_jep))
506
507
508
	# END messageCB

	def _presenceCB(self, con, prs):
nkour's avatar
nkour committed
509
		'''Called when we receive a presence'''
Yann Leboulanger's avatar
Yann Leboulanger committed
510
		ptype = prs.getType()
511
512
		if ptype == 'available':
			ptype = None
nkour's avatar
nkour committed
513
		gajim.log.debug('PresenceCB: %s' % ptype)
514
		is_gc = False # is it a GC presence ?
515
		sigTag = None
516
		avatar_sha = None
517
518
		xtags = prs.getTags('x')
		for x in xtags:
519
			if x.getNamespace().startswith(common.xmpp.NS_MUC):
520
521
				is_gc = True
			if x.getNamespace() == common.xmpp.NS_SIGNED:
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
522
				sigTag = x
523
524
			if x.getNamespace() == common.xmpp.NS_VCARD_UPDATE:
				avatar_sha = x.getTagData('photo')
525

526
		who = self.get_full_jid(prs)
527
		jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
Yann Leboulanger's avatar
Yann Leboulanger committed
528
		no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
529
		status = prs.getStatus()
Yann Leboulanger's avatar
Yann Leboulanger committed
530
531
532
533
534
		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
535
		elif ptype == 'unavailable':
536
			show = 'offline'
537

Yann Leboulanger's avatar
Yann Leboulanger committed
538
539
540
541
542
543
544
545
546
547
548
		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)

549
550
551
552
553
554
555
556
		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
557
					self.dispatch('ERROR', (_('Unable to join room'),
558
559
						_('A password is required to join this room.')))
				elif errcode == '403': # we are banned
560
					self.dispatch('ERROR', (_('Unable to join room'),
561
562
						_('You are banned from this room.')))
				elif errcode == '404': # room does not exist
563
					self.dispatch('ERROR', (_('Unable to join room'),
564
565
						_('Such room does not exist.')))
				elif errcode == '405':
566
					self.dispatch('ERROR', (_('Unable to join room'),
567
568
						_('Room creation is restricted.')))
				elif errcode == '406':
569
					self.dispatch('ERROR', (_('Unable to join room'),
570
571
						_('Your registered nickname must be used.')))
				elif errcode == '407':
572
					self.dispatch('ERROR', (_('Unable to join room'),
573
574
						_('You are not in the members list.')))
				elif errcode == '409': # nick conflict
575
					# the jid_from in this case is FAKE JID: room_jid/nick
576
					# resource holds the bad nick so propose a new one
577
578
					proposed_nickname = resource + \
						gajim.config.get('gc_proposed_nick_char')
579
					room_jid = gajim.get_room_from_fjid(who)
580
					self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join room'),
581
		_('Your desired nickname is in use or registered by another occupant.\nPlease specify another nickname below:'), proposed_nickname))
582
583
584
585
				else:	# print in the window the error
					self.dispatch('ERROR_ANSWER', ('', jid_stripped,
						errmsg, errcode))
			if not ptype or ptype == 'unavailable':
586
587
				if gajim.config.get('log_contact_status_changes') and self.name\
					not in no_log_for and jid_stripped not in no_log_for:
588
					gajim.logger.write('gcstatus', who, status, show)
589
590
591
592
593
594
595
				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':
596
			gajim.log.debug('subscribe request from %s' % who)
597
			if gajim.config.get('alwaysauth') or who.find("@") <= 0 or \
598
			jid_stripped in self.jids_for_auto_auth:
599
				if self.connection:
600
601
					p = common.xmpp.Presence(who, 'subscribed')
					p = self.add_sha(p)
dkirov's avatar
dkirov committed
602
					self.connection.send(p)
603
				if who.find("@") <= 0:
604
					self.dispatch('NOTIFY',
605
						(jid_stripped, 'offline', 'offline', resource, prio, keyID))
606
607
608
609
			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
610
		elif ptype == 'subscribed':
611
			self.dispatch('SUBSCRIBED', (jid_stripped, resource))
nkour's avatar
nkour committed
612
			# BE CAREFUL: no con.updateRosterItem() in a callback
dkirov's avatar
dkirov committed
613
			gajim.log.debug(_('we are now subscribed to %s') % who)
Yann Leboulanger's avatar
Yann Leboulanger committed
614
		elif ptype == 'unsubscribe':
nkour's avatar
nkour committed
615
			gajim.log.debug(_('unsubscribe request from %s') % who)
Yann Leboulanger's avatar
Yann Leboulanger committed
616
		elif ptype == 'unsubscribed':
dkirov's avatar
dkirov committed
617
			gajim.log.debug(_('we are now unsubscribed from %s') % who)
618
			self.dispatch('UNSUBSCRIBED', jid_stripped)
Yann Leboulanger's avatar
Yann Leboulanger committed
619
		elif ptype == 'error':
620
621
			errmsg = prs.getError()
			errcode = prs.getErrorCode()
622
			if errcode == '502': # Internal Timeout:
623
624
				self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
					prio, keyID))
nkour's avatar
nkour committed
625
			else:	# print in the window the error
626
				self.dispatch('ERROR_ANSWER', ('', jid_stripped,
627
					errmsg, errcode))
628

629
		if avatar_sha and ptype != 'error':
630
631
632
			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
633
					self.request_vcard(jid_stripped)
634
635
			else:
				self.vcard_shas[jid_stripped] = avatar_sha
636
		if not ptype or ptype == 'unavailable':
637
638
			if gajim.config.get('log_contact_status_changes') and self.name\
				not in no_log_for and jid_stripped not in no_log_for:
639
				gajim.logger.write('status', jid_stripped, status, show)
640
641
			self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio,
				keyID))
642
643
	# END presenceCB

644
	def _disconnectedCB(self):
nkour's avatar
nkour committed
645
		'''Called when we are disconnected'''
646
		gajim.log.debug('disconnectedCB')
647
648
649
650
651
652
		if not self.connection:
			return
		self.connected = 0
		self.dispatch('STATUS', 'offline')
		self.connection = None
		if not self.on_purpose:
653
			self.dispatch('ERROR',
654
655
			(_('Connection with account "%s" has been lost') % self.name,
			_('To continue sending and receiving messages, you will need to reconnect.')))
656
		self.on_purpose = False
657

658
	# END disconenctedCB
659
660

	def _reconnect(self):
661
662
		# Do not try to reco while we are already trying
		self.time_to_reconnect = None
663
		gajim.log.debug('reconnect')
664
		self.retrycount += 1
665
		signed = self.get_signed_msg(self.status)
dkirov's avatar
dkirov committed
666
		self.on_connect_auth = self._init_roster
667
		self.connect_and_init(self.old_show, self.status, signed)
668

669
670
671
672
		if self.connected < 2: #connection failed
			if self.retrycount > 10:
				self.connected = 0
				self.dispatch('STATUS', 'offline')
673
				self.dispatch('ERROR',
674
675
676
677
678
				(_('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
679
				self.time_to_reconnect = 20
680
			else:
dkirov's avatar
dkirov committed
681
682
				self.time_to_reconnect = 10
			gajim.idlequeue.set_alarm(self._reconnect_alarm, self.time_to_reconnect)
683
		else:
dkirov's avatar
dkirov committed
684
			# reconnect succeeded
685
686
			self.time_to_reconnect = None
			self.retrycount = 0
687

688
	def _disconnectedReconnCB(self):
nkour's avatar
nkour committed
689
		'''Called when we are disconnected'''
690
691
692
693
694
695
696
697
698
699
700
		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
701
702
				self.time_to_reconnect = 10
				gajim.idlequeue.set_alarm(self._reconnect_alarm, 10)
703
			else:
704
				self.dispatch('ERROR',
705
706
707
708
				(_('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
709

710
711
	def _bytestreamErrorCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamErrorCB')
712
		id = unicode(iq_obj.getAttr('id'))
713
		query = iq_obj.getTag('query')
714
		jid = self.get_jid(iq_obj)
715
716
717
718
		id = id[3:]
		if not self.files_props.has_key(id):
			return
		file_props = self.files_props[id]
dkirov's avatar
dkirov committed
719
		file_props['error'] = -4
720
721
		self.dispatch('FILE_REQUEST_ERROR', (jid, file_props))
		raise common.xmpp.NodeProcessed
722

dkirov's avatar
dkirov committed
723
724
	def _bytestreamSetCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamSetCB')
725
726
		target = unicode(iq_obj.getAttr('to'))
		id = unicode(iq_obj.getAttr('id'))
dkirov's avatar
dkirov committed
727
		query = iq_obj.getTag('query')
728
		sid = unicode(query.getAttr('sid'))
dkirov's avatar
dkirov committed
729
730
		file_props = gajim.socks5queue.get_file_props(
			self.name, sid)
dkirov's avatar
dkirov committed
731
732
733
		streamhosts=[]
		for item in query.getChildren():
			if item.getName() == 'streamhost':
734
				host_dict={
735
736
737
					'state': 0,
					'target': target,
					'id': id,
dkirov's avatar
dkirov committed
738
					'sid': sid,
739
					'initiator': self.get_full_jid(iq_obj)
740
				}
dkirov's avatar
dkirov committed
741
				for attr in item.getAttrs():
742
					host_dict[attr] = item.getAttr(attr)
dkirov's avatar
dkirov committed
743
				streamhosts.append(host_dict)
744
745
746
747
		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
748
				if file_props['type'] == 's': # FIXME: remove fast xmlns
749
750
751
752
753
754
755
756
					# 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)
757
					gajim.socks5queue.connect_to_hosts(self.name, sid,
758
						self.send_success_connect_reply, None)
759
				raise common.xmpp.NodeProcessed
760

761
762
		file_props['streamhosts'] = streamhosts
		if file_props['type'] == 'r':
763
			gajim.socks5queue.connect_to_hosts(self.name, sid,
764
765
				self.send_success_connect_reply, self._connect_error)
		raise common.xmpp.NodeProcessed
766

767
	def send_success_connect_reply(self, streamhost):
768
		''' send reply to the initiator of FT that we
769
770
771
772
		made a connection
		'''
		if streamhost is None:
			return None
773
		iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
774
775
776
777
778
779
			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
780
		self.connection.send(iq)
781

dkirov's avatar
dkirov committed
782
	def _connect_error(self, to, _id, sid, code = 404):
783
		msg_dict = {
784
785
786
			404: 'Could not connect to given hosts',
			405: 'Cancel',
			406: 'Not acceptable',
787
788
789
		}
		msg = msg_dict[code]
		iq = None
790
		iq = common.xmpp.Protocol(name = 'iq', to = to,
791
			typ = 'error')
dkirov's avatar
dkirov committed
792
		iq.setAttr('id', _id)
793
		err = iq.setTag('error')
794
		err.setAttr('code', unicode(code))
795
		err.setData(msg)
dkirov's avatar
dkirov committed
796
		self.connection.send(iq)
dkirov's avatar
dkirov committed
797
798
		if code == 404:
			file_props = gajim.socks5queue.get_file_props(self.name, sid)
dkirov's avatar
dkirov committed
799
800
801
802
			if file_props is not None:
				self.disconnect_transfer(file_props)
				file_props['error'] = -3
				self.dispatch('FILE_REQUEST_ERROR', (to, file_props))
803

dkirov's avatar
dkirov committed
804
805
	def _bytestreamResultCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamResultCB')
806
		frm = self.get_full_jid(iq_obj)
807
		real_id = unicode(iq_obj.getAttr('id'))
dkirov's avatar
dkirov committed
808
		query = iq_obj.getTag('query')
809
810
811
		streamhost = None
		try:
			streamhost = query.getTag('streamhost')
812
		except:
813
814
			pass
		if streamhost is not None: # this is a result for proxy request
dkirov's avatar
dkirov committed
815
816
817
818
819
			jid = None
			try:
				jid = streamhost.getAttr('jid')
			except:
				raise common.xmpp.NodeProcessed
820
821
822
			proxyhosts = []
			for item in query.getChildren():
				if item.getName() == 'streamhost':
823
824
825
826
					host = item.getAttr('host')
					port = item.getAttr('port')
					jid = item.getAttr('jid')
					conf = gajim.config
dkirov's avatar
dkirov committed
827
					conf.add_per('ft_proxies65_cache', jid)
828
829
830
831
832
833
					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))
834
835
836
837
			raise common.xmpp.NodeProcessed
		try:
			streamhost =  query.getTag('streamhost-used')
		except: # this bytestream result is not what we need
dkirov's avatar
dkirov committed
838
			pass
839
840
841
842
843
		id = real_id[3:]
		if self.files_props.has_key(id):
			file_props = self.files_props[id]
		else:
			raise common.xmpp.NodeProcessed
844
		if streamhost is None:
dkirov's avatar
dkirov committed
845
846
847
848
849
850
851
852
853
854
			# 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</