connection.py 89.3 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
		self.jids_for_auto_auth = [] # list of jid to auto-authorize
208
		self.room_jids = [] # list of gc jids so that vcard are saved in a folder
209
210
	# END __init__

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

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

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

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

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

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

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

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

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

273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
	def save_vcard_to_hd(self, full_jid, card):
		jid, nick = gajim.get_room_and_nick_from_fjid(full_jid)
		path = os.path.join(gajim.VCARD_PATH, jid)
		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)
			path_to_file = os.path.join(gajim.VCARD_PATH, jid, nick)
		else:
			path_to_file = path
		fil = open(path_to_file, 'w')
		fil.write(str(card))
		fil.close()

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

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

			# Save it to file
322
			self.save_vcard_to_hd(who, card)
323
324
			# Save the decoded avatar to a separate file too, and generate files for dbus notifications
			if photo_decoded:
325
				avatar_file = os.path.join(gajim.AVATAR_PATH, frm + '_notif_size_colored.png')
326
327
				if frm == our_jid and avatar_sha != self.vcard_sha:
					gajim.interface.save_avatar_files(frm, photo_decoded)
328
329
				elif frm != our_jid and (not os.path.exists(avatar_file) or \
					not self.vcard_shas.has_key(frm) or \
330
331
					avatar_sha != self.vcard_shas[frm]):
					gajim.interface.save_avatar_files(frm, photo_decoded)
332
333
334
335
336
337
			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)
338
339
340
341
342
343

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

345
346
			vcard['jid'] = frm
			vcard['resource'] = resource
347
			if frm == our_jid:
348
				self.dispatch('MYVCARD', vcard)
349
350
351
352
353
				# 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
354
355
				if STATUS_LIST[self.connected] == 'invisible':
					return
356
				sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
357
358
				prio = unicode(gajim.config.get_per('accounts', self.name,
					'priority'))
359
				p = common.xmpp.Presence(typ = None, priority = prio, show = sshow,
360
					status = self.status)
361
				p = self.add_sha(p)
dkirov's avatar
dkirov committed
362
				self.connection.send(p)
363
364
365
			else:
				self.dispatch('VCARD', vcard)

366
	def _gMailNewMailCB(self, con, gm):
nkour's avatar
nkour committed
367
		'''Called when we get notified of new mail messages in gmail account'''
368
369
370
		if not gm.getTag('new-mail'):
			return
		if gm.getTag('new-mail').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
371
			# we'll now ask the server for the exact number of new messages
372
			jid = gajim.get_jid_from_account(self.name)
373
			gajim.log.debug('Got notification of new gmail e-mail on %s. Asking the server for more info.' % jid)
374
375
376
377
			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
378
			self.connection.send(iq)
379
			raise common.xmpp.NodeProcessed
380

381
	def _gMailQueryCB(self, con, gm):
nkour's avatar
nkour committed
382
		'''Called when we receive results from Querying the server for mail messages in gmail account'''
383
384
385
386
387
388
389
		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
390
				gajim.log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid))
391
				self.dispatch('GMAIL_NOTIFY', (jid, newmsgs))
392
			raise common.xmpp.NodeProcessed
393

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

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

	def _presenceCB(self, con, prs):
nkour's avatar
nkour committed
496
		'''Called when we receive a presence'''
Yann Leboulanger's avatar
Yann Leboulanger committed
497
		ptype = prs.getType()
498
499
		if ptype == 'available':
			ptype = None
nkour's avatar
nkour committed
500
		gajim.log.debug('PresenceCB: %s' % ptype)
501
		is_gc = False # is it a GC presence ?
502
		sigTag = None
503
		avatar_sha = None
504
505
		xtags = prs.getTags('x')
		for x in xtags:
506
			if x.getNamespace().startswith(common.xmpp.NS_MUC):
507
508
				is_gc = True
			if x.getNamespace() == common.xmpp.NS_SIGNED:
Yann Leboulanger's avatar
typo    
Yann Leboulanger committed
509
				sigTag = x
510
511
			if x.getNamespace() == common.xmpp.NS_VCARD_UPDATE:
				avatar_sha = x.getTagData('photo')
512

513
		who = self.get_full_jid(prs)
514
		jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
Yann Leboulanger's avatar
Yann Leboulanger committed
515
		no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
516
		status = prs.getStatus()
Yann Leboulanger's avatar
Yann Leboulanger committed
517
518
519
520
521
		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
522
		elif ptype == 'unavailable':
523
			show = 'offline'
524

Yann Leboulanger's avatar
Yann Leboulanger committed
525
526
527
528
529
530
531
532
533
534
535
		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)

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

616
		if avatar_sha and ptype != 'error':
617
618
619
			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
620
					self.request_vcard(jid_stripped)
621
622
			else:
				self.vcard_shas[jid_stripped] = avatar_sha
623
		if not ptype or ptype == 'unavailable':
624
625
			if gajim.config.get('log_contact_status_changes') and self.name\
				not in no_log_for and jid_stripped not in no_log_for:
626
				gajim.logger.write('status', jid_stripped, status, show)
627
628
			self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio,
				keyID))
629
630
	# END presenceCB

631
	def _disconnectedCB(self):
nkour's avatar
nkour committed
632
		'''Called when we are disconnected'''
633
		gajim.log.debug('disconnectedCB')
634
635
636
637
638
639
		if not self.connection:
			return
		self.connected = 0
		self.dispatch('STATUS', 'offline')
		self.connection = None
		if not self.on_purpose:
640
			self.dispatch('ERROR',
641
642
			(_('Connection with account "%s" has been lost') % self.name,
			_('To continue sending and receiving messages, you will need to reconnect.')))
643
		self.on_purpose = False
644

645
	# END disconenctedCB
646
647

	def _reconnect(self):
648
649
		# Do not try to reco while we are already trying
		self.time_to_reconnect = None
650
		gajim.log.debug('reconnect')
651
		self.retrycount += 1
652
		signed = self.get_signed_msg(self.status)
dkirov's avatar
dkirov committed
653
		self.on_connect_auth = self._init_roster
654
		self.connect_and_init(self.old_show, self.status, signed)
655

656
657
658
659
		if self.connected < 2: #connection failed
			if self.retrycount > 10:
				self.connected = 0
				self.dispatch('STATUS', 'offline')
660
				self.dispatch('ERROR',
661
662
663
664
665
				(_('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
666
				self.time_to_reconnect = 20
667
			else:
dkirov's avatar
dkirov committed
668
669
				self.time_to_reconnect = 10
			gajim.idlequeue.set_alarm(self._reconnect_alarm, self.time_to_reconnect)
670
		else:
dkirov's avatar
dkirov committed
671
			# reconnect succeeded
672
673
			self.time_to_reconnect = None
			self.retrycount = 0
674

675
	def _disconnectedReconnCB(self):
nkour's avatar
nkour committed
676
		'''Called when we are disconnected'''
677
678
679
680
681
682
683
684
685
686
687
		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
688
689
				self.time_to_reconnect = 10
				gajim.idlequeue.set_alarm(self._reconnect_alarm, 10)
690
			else:
691
				self.dispatch('ERROR',
692
693
694
695
				(_('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
696

697
698
	def _bytestreamErrorCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamErrorCB')
699
		id = unicode(iq_obj.getAttr('id'))
700
		query = iq_obj.getTag('query')
701
		jid = self.get_jid(iq_obj)
702
703
704
705
		id = id[3:]
		if not self.files_props.has_key(id):
			return
		file_props = self.files_props[id]
dkirov's avatar
dkirov committed
706
		file_props['error'] = -4
707
708
		self.dispatch('FILE_REQUEST_ERROR', (jid, file_props))
		raise common.xmpp.NodeProcessed
709

dkirov's avatar
dkirov committed
710
711
	def _bytestreamSetCB(self, con, iq_obj):
		gajim.log.debug('_bytestreamSetCB')
712
713
		target = unicode(iq_obj.getAttr('to'))
		id = unicode(iq_obj.getAttr('id'))
dkirov's avatar
dkirov committed
714
		query = iq_obj.getTag('query')
715
		sid = unicode(query.getAttr('sid'))
dkirov's avatar
dkirov committed
716
717
		file_props = gajim.socks5queue.get_file_props(
			self.name, sid)
dkirov's avatar
dkirov committed
718
719
720
		streamhosts=[]
		for item in query.getChildren():
			if item.getName() == 'streamhost':
721
				host_dict={
722
723
724
					'state': 0,
					'target': target,
					'id': id,
dkirov's avatar
dkirov committed
725
					'sid': sid,
726
					'initiator': self.get_full_jid(iq_obj)
727
				}
dkirov's avatar
dkirov committed
728
				for attr in item.getAttrs():
729
					host_dict[attr] = item.getAttr(attr)
dkirov's avatar
dkirov committed
730
				streamhosts.append(host_dict)
731
732
733
734
		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
735
				if file_props['type'] == 's': # FIXME: remove fast xmlns
736
737
738
739
740
741
742
743
					# 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)
744
					gajim.socks5queue.connect_to_hosts(self.name, sid,
745
						self.send_success_connect_reply, None)
746
				raise common.xmpp.NodeProcessed
747

748
749
		file_props['streamhosts'] = streamhosts
		if file_props['type'] == 'r':
750
			gajim.socks5queue.connect_to_hosts(self.name, sid,
751
752
				self.send_success_connect_reply, self._connect_error)
		raise common.xmpp.NodeProcessed
753

754
	def send_success_connect_reply(self, streamhost):
755
		''' send reply to the initiator of FT that we
756
757
758
759
		made a connection
		'''
		if streamhost is None:
			return None
760
		iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
761
762
763
764
765
766
			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
767
		self.connection.send(iq)
768

dkirov's avatar
dkirov committed
769
	def _connect_error(self, to, _id, sid, code = 404):
770
		msg_dict = {
771
772
773
			404: 'Could not connect to given hosts',
			405: 'Cancel',
			406: 'Not acceptable',
774
775
776
		}
		msg = msg_dict[code]
		iq = None
777
		iq = common.xmpp.Protocol(name = 'iq', to = to,
778
			typ = 'error')
dkirov's avatar
dkirov committed
779
		iq.setAttr('id', _id)
780
		err = iq.setTag('error')
781
		err.setAttr('code', unicode(code))
782
		err.setData(msg)
dkirov's avatar
dkirov committed
783
		self.connection.send(iq)
dkirov's avatar
dkirov committed
784
785
		if code == 404:
			file_props = gajim.socks5queue.get_file_props(self.name, sid)
dkirov's avatar
dkirov committed
786
787
788
789
			if file_props is not None:
				self.disconnect_transfer(file_props)
				file_props['error'] = -3
				self.dispatch('FILE_REQUEST_ERROR', (to, file_props))
790

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

dkirov's avatar
dkirov committed
851
852
853
		if real_id[:3] == 'au_':
			gajim.socks5queue.send_file(file_props, self.name)
			raise common.xmpp.NodeProcessed
854

855
856
857
858
859
		proxy = None
		if file_props.has_key('proxyhosts'):
			for proxyhost in file_props['proxyhosts']:
				if proxyhost['jid'] == jid:
					proxy = proxyhost
860

861
		if proxy != None:
dkirov's avatar
dkirov committed
862
			file_props['streamhost-used'] = True
863
			if not file_props.has_key('streamhosts'):
dkirov's avatar
dkirov committed
864