gajim

view src/common/connection_handlers.py @ 10773:1076fc9700f5

merge elghinn's branch (roster versioning) to trunk. Fixes #4661, #3190
author Yann Leboulanger <asterix@lagaule.org>
date Fri, 10 Jul 2009 15:05:01 +0200
parents 89f0828cd6be f5b5c2a0ca4c
children f0472ee0e7b7 0c2f1f04a96b
line source
1 # -*- coding:utf-8 -*-
2 ## src/common/connection_handlers.py
3 ##
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Junglecow J <junglecow AT gmail.com>
6 ## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
7 ## Travis Shirk <travis AT pobox.com>
8 ## Nikos Kouremenos <kourem AT gmail.com>
9 ## Copyright (C) 2006-2008 Yann Leboulanger <asterix AT lagaule.org>
10 ## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
11 ## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
12 ## Jean-Marie Traissard <jim AT lapin.org>
13 ## Stephan Erb <steve-e AT h3c.de>
14 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
15 ##
16 ## This file is part of Gajim.
17 ##
18 ## Gajim is free software; you can redistribute it and/or modify
19 ## it under the terms of the GNU General Public License as published
20 ## by the Free Software Foundation; version 3 only.
21 ##
22 ## Gajim is distributed in the hope that it will be useful,
23 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
24 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 ## GNU General Public License for more details.
26 ##
27 ## You should have received a copy of the GNU General Public License
28 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
29 ##
31 import os
32 import base64
33 import socket
34 import sys
35 import operator
36 import hashlib
38 from time import (altzone, daylight, gmtime, localtime, mktime, strftime,
39 time as time_time, timezone, tzname)
40 from calendar import timegm
41 import datetime
43 import socks5
44 import common.xmpp
46 from common import helpers
47 from common import gajim
48 from common import atom
49 from common import pep
50 from common import exceptions
51 from common.commands import ConnectionCommands
52 from common.pubsub import ConnectionPubSub
53 from common.caps import ConnectionCaps
55 from common import dbus_support
56 if dbus_support.supported:
57 import dbus
58 from music_track_listener import MusicTrackListener
60 import logging
61 log = logging.getLogger('gajim.c.connection_handlers')
63 # kind of events we can wait for an answer
64 VCARD_PUBLISHED = 'vcard_published'
65 VCARD_ARRIVED = 'vcard_arrived'
66 AGENT_REMOVED = 'agent_removed'
67 METACONTACTS_ARRIVED = 'metacontacts_arrived'
68 ROSTER_ARRIVED = 'roster_arrived'
69 PRIVACY_ARRIVED = 'privacy_arrived'
70 PEP_CONFIG = 'pep_config'
71 HAS_IDLE = True
72 try:
73 import idle
74 except Exception:
75 log.debug(_('Unable to load idle module'))
76 HAS_IDLE = False
78 class ConnectionBytestream:
79 def __init__(self):
80 self.files_props = {}
81 self.awaiting_xmpp_ping_id = None
83 def is_transfer_stopped(self, file_props):
84 if 'error' in file_props and file_props['error'] != 0:
85 return True
86 if 'completed' in file_props and file_props['completed']:
87 return True
88 if 'connected' in file_props and file_props['connected'] == False:
89 return True
90 if 'stopped' not in file_props or not file_props['stopped']:
91 return False
92 return True
94 def send_success_connect_reply(self, streamhost):
95 ''' send reply to the initiator of FT that we
96 made a connection
97 '''
98 if not self.connection or self.connected < 2:
99 return
100 if streamhost is None:
101 return None
102 iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result',
103 frm = streamhost['target'])
104 iq.setAttr('id', streamhost['id'])
105 query = iq.setTag('query')
106 query.setNamespace(common.xmpp.NS_BYTESTREAM)
107 stream_tag = query.setTag('streamhost-used')
108 stream_tag.setAttr('jid', streamhost['jid'])
109 self.connection.send(iq)
111 def remove_transfers_for_contact(self, contact):
112 ''' stop all active transfer for contact '''
113 for file_props in self.files_props.values():
114 if self.is_transfer_stopped(file_props):
115 continue
116 receiver_jid = unicode(file_props['receiver'])
117 if contact.get_full_jid() == receiver_jid:
118 file_props['error'] = -5
119 self.remove_transfer(file_props)
120 self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, ''))
121 sender_jid = unicode(file_props['sender'])
122 if contact.get_full_jid() == sender_jid:
123 file_props['error'] = -3
124 self.remove_transfer(file_props)
126 def remove_all_transfers(self):
127 ''' stops and removes all active connections from the socks5 pool '''
128 for file_props in self.files_props.values():
129 self.remove_transfer(file_props, remove_from_list = False)
130 del(self.files_props)
131 self.files_props = {}
133 def remove_transfer(self, file_props, remove_from_list = True):
134 if file_props is None:
135 return
136 self.disconnect_transfer(file_props)
137 sid = file_props['sid']
138 gajim.socks5queue.remove_file_props(self.name, sid)
140 if remove_from_list:
141 if 'sid' in self.files_props:
142 del(self.files_props['sid'])
144 def disconnect_transfer(self, file_props):
145 if file_props is None:
146 return
147 if 'hash' in file_props:
148 gajim.socks5queue.remove_sender(file_props['hash'])
150 if 'streamhosts' in file_props:
151 for host in file_props['streamhosts']:
152 if 'idx' in host and host['idx'] > 0:
153 gajim.socks5queue.remove_receiver(host['idx'])
154 gajim.socks5queue.remove_sender(host['idx'])
156 def send_socks5_info(self, file_props, fast = True, receiver = None,
157 sender = None):
158 ''' send iq for the present streamhosts and proxies '''
159 if not self.connection or self.connected < 2:
160 return
161 if not isinstance(self.peerhost, tuple):
162 return
163 port = gajim.config.get('file_transfers_port')
164 ft_add_hosts_to_send = gajim.config.get('ft_add_hosts_to_send')
165 cfg_proxies = gajim.config.get_per('accounts', self.name,
166 'file_transfer_proxies')
167 if receiver is None:
168 receiver = file_props['receiver']
169 if sender is None:
170 sender = file_props['sender']
171 proxyhosts = []
172 if fast and cfg_proxies:
173 proxies = [e.strip() for e in cfg_proxies.split(',')]
174 default = gajim.proxy65_manager.get_default_for_name(self.name)
175 if default:
176 # add/move default proxy at top of the others
177 if proxies.__contains__(default):
178 proxies.remove(default)
179 proxies.insert(0, default)
181 for proxy in proxies:
182 (host, _port, jid) = gajim.proxy65_manager.get_proxy(proxy, self.name)
183 if host is None:
184 continue
185 host_dict={
186 'state': 0,
187 'target': unicode(receiver),
188 'id': file_props['sid'],
189 'sid': file_props['sid'],
190 'initiator': proxy,
191 'host': host,
192 'port': unicode(_port),
193 'jid': jid
194 }
195 proxyhosts.append(host_dict)
196 sha_str = helpers.get_auth_sha(file_props['sid'], sender,
197 receiver)
198 file_props['sha_str'] = sha_str
199 ft_add_hosts = []
200 if ft_add_hosts_to_send:
201 ft_add_hosts_to_send = [e.strip() for e in ft_add_hosts_to_send.split(',')]
202 for ft_host in ft_add_hosts_to_send:
203 ft_add_hosts.append(ft_host)
204 listener = gajim.socks5queue.start_listener(port,
205 sha_str, self._result_socks5_sid, file_props['sid'])
206 if listener is None:
207 file_props['error'] = -5
208 self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props,
209 ''))
210 self._connect_error(unicode(receiver), file_props['sid'],
211 file_props['sid'], code = 406)
212 return
214 iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver),
215 typ = 'set')
216 file_props['request-id'] = 'id_' + file_props['sid']
217 iq.setID(file_props['request-id'])
218 query = iq.setTag('query')
219 query.setNamespace(common.xmpp.NS_BYTESTREAM)
220 query.setAttr('mode', 'plain')
221 query.setAttr('sid', file_props['sid'])
222 for ft_host in ft_add_hosts:
223 # The streamhost, if set
224 ostreamhost = common.xmpp.Node(tag = 'streamhost')
225 query.addChild(node = ostreamhost)
226 ostreamhost.setAttr('port', unicode(port))
227 ostreamhost.setAttr('host', ft_host)
228 ostreamhost.setAttr('jid', sender)
229 try:
230 thehost = self.peerhost[0]
231 streamhost = common.xmpp.Node(tag = 'streamhost') # My IP
232 query.addChild(node = streamhost)
233 streamhost.setAttr('port', unicode(port))
234 streamhost.setAttr('host', thehost)
235 streamhost.setAttr('jid', sender)
236 except socket.gaierror:
237 self.dispatch('ERROR', (_('Wrong host'),
238 _('Invalid local address? :-O')))
240 if fast and proxyhosts != [] and gajim.config.get_per('accounts',
241 self.name, 'use_ft_proxies'):
242 file_props['proxy_receiver'] = unicode(receiver)
243 file_props['proxy_sender'] = unicode(sender)
244 file_props['proxyhosts'] = proxyhosts
245 for proxyhost in proxyhosts:
246 streamhost = common.xmpp.Node(tag = 'streamhost')
247 query.addChild(node=streamhost)
248 streamhost.setAttr('port', proxyhost['port'])
249 streamhost.setAttr('host', proxyhost['host'])
250 streamhost.setAttr('jid', proxyhost['jid'])
252 # don't add the proxy child tag for streamhosts, which are proxies
253 # proxy = streamhost.setTag('proxy')
254 # proxy.setNamespace(common.xmpp.NS_STREAM)
255 self.connection.send(iq)
257 def send_file_rejection(self, file_props, code='403', typ=None):
258 ''' informs sender that we refuse to download the file
259 typ is used when code = '400', in this case typ can be 'strean' for
260 invalid stream or 'profile' for invalid profile'''
261 # user response to ConfirmationDialog may come after we've disconneted
262 if not self.connection or self.connected < 2:
263 return
264 iq = common.xmpp.Protocol(name = 'iq',
265 to = unicode(file_props['sender']), typ = 'error')
266 iq.setAttr('id', file_props['request-id'])
267 if code == '400' and typ in ('stream', 'profile'):
268 name = 'bad-request'
269 text = ''
270 else:
271 name = 'forbidden'
272 text = 'Offer Declined'
273 err = common.xmpp.ErrorNode(code=code, typ='cancel', name=name, text=text)
274 if code == '400' and typ in ('stream', 'profile'):
275 if typ == 'stream':
276 err.setTag('no-valid-streams', namespace=common.xmpp.NS_SI)
277 else:
278 err.setTag('bad-profile', namespace=common.xmpp.NS_SI)
279 iq.addChild(node=err)
280 self.connection.send(iq)
282 def send_file_approval(self, file_props):
283 ''' send iq, confirming that we want to download the file '''
284 # user response to ConfirmationDialog may come after we've disconneted
285 if not self.connection or self.connected < 2:
286 return
287 iq = common.xmpp.Protocol(name = 'iq',
288 to = unicode(file_props['sender']), typ = 'result')
289 iq.setAttr('id', file_props['request-id'])
290 si = iq.setTag('si')
291 si.setNamespace(common.xmpp.NS_SI)
292 if 'offset' in file_props and file_props['offset']:
293 file_tag = si.setTag('file')
294 file_tag.setNamespace(common.xmpp.NS_FILE)
295 range_tag = file_tag.setTag('range')
296 range_tag.setAttr('offset', file_props['offset'])
297 feature = si.setTag('feature')
298 feature.setNamespace(common.xmpp.NS_FEATURE)
299 _feature = common.xmpp.DataForm(typ='submit')
300 feature.addChild(node=_feature)
301 field = _feature.setField('stream-method')
302 field.delAttr('type')
303 field.setValue(common.xmpp.NS_BYTESTREAM)
304 self.connection.send(iq)
306 def send_file_request(self, file_props):
307 ''' send iq for new FT request '''
308 if not self.connection or self.connected < 2:
309 return
310 our_jid = gajim.get_jid_from_account(self.name)
311 resource = self.server_resource
312 frm = our_jid + '/' + resource
313 file_props['sender'] = frm
314 fjid = file_props['receiver'].jid + '/' + file_props['receiver'].resource
315 iq = common.xmpp.Protocol(name = 'iq', to = fjid,
316 typ = 'set')
317 iq.setID(file_props['sid'])
318 self.files_props[file_props['sid']] = file_props
319 si = iq.setTag('si')
320 si.setNamespace(common.xmpp.NS_SI)
321 si.setAttr('profile', common.xmpp.NS_FILE)
322 si.setAttr('id', file_props['sid'])
323 file_tag = si.setTag('file')
324 file_tag.setNamespace(common.xmpp.NS_FILE)
325 file_tag.setAttr('name', file_props['name'])
326 file_tag.setAttr('size', file_props['size'])
327 desc = file_tag.setTag('desc')
328 if 'desc' in file_props:
329 desc.setData(file_props['desc'])
330 file_tag.setTag('range')
331 feature = si.setTag('feature')
332 feature.setNamespace(common.xmpp.NS_FEATURE)
333 _feature = common.xmpp.DataForm(typ='form')
334 feature.addChild(node=_feature)
335 field = _feature.setField('stream-method')
336 field.setAttr('type', 'list-single')
337 field.addOption(common.xmpp.NS_BYTESTREAM)
338 self.connection.send(iq)
340 def _result_socks5_sid(self, sid, hash_id):
341 ''' store the result of sha message from auth. '''
342 if sid not in self.files_props:
343 return
344 file_props = self.files_props[sid]
345 file_props['hash'] = hash_id
346 return
348 def _connect_error(self, to, _id, sid, code = 404):
349 ''' cb, when there is an error establishing BS connection, or
350 when connection is rejected'''
351 if not self.connection or self.connected < 2:
352 return
353 msg_dict = {
354 404: 'Could not connect to given hosts',
355 405: 'Cancel',
356 406: 'Not acceptable',
357 }
358 msg = msg_dict[code]
359 iq = None
360 iq = common.xmpp.Protocol(name = 'iq', to = to,
361 typ = 'error')
362 iq.setAttr('id', _id)
363 err = iq.setTag('error')
364 err.setAttr('code', unicode(code))
365 err.setData(msg)
366 self.connection.send(iq)
367 if code == 404:
368 file_props = gajim.socks5queue.get_file_props(self.name, sid)
369 if file_props is not None:
370 self.disconnect_transfer(file_props)
371 file_props['error'] = -3
372 self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg))
374 def _proxy_auth_ok(self, proxy):
375 '''cb, called after authentication to proxy server '''
376 if not self.connection or self.connected < 2:
377 return
378 file_props = self.files_props[proxy['sid']]
379 iq = common.xmpp.Protocol(name = 'iq', to = proxy['initiator'],
380 typ = 'set')
381 auth_id = "au_" + proxy['sid']
382 iq.setID(auth_id)
383 query = iq.setTag('query')
384 query.setNamespace(common.xmpp.NS_BYTESTREAM)
385 query.setAttr('sid', proxy['sid'])
386 activate = query.setTag('activate')
387 activate.setData(file_props['proxy_receiver'])
388 iq.setID(auth_id)
389 self.connection.send(iq)
391 # register xmpppy handlers for bytestream and FT stanzas
392 def _bytestreamErrorCB(self, con, iq_obj):
393 log.debug('_bytestreamErrorCB')
394 id_ = unicode(iq_obj.getAttr('id'))
395 frm = helpers.get_full_jid_from_iq(iq_obj)
396 query = iq_obj.getTag('query')
397 gajim.proxy65_manager.error_cb(frm, query)
398 jid = helpers.get_jid_from_iq(iq_obj)
399 id_ = id_[3:]
400 if id_ not in self.files_props:
401 return
402 file_props = self.files_props[id_]
403 file_props['error'] = -4
404 self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
405 raise common.xmpp.NodeProcessed
407 def _bytestreamSetCB(self, con, iq_obj):
408 log.debug('_bytestreamSetCB')
409 target = unicode(iq_obj.getAttr('to'))
410 id_ = unicode(iq_obj.getAttr('id'))
411 query = iq_obj.getTag('query')
412 sid = unicode(query.getAttr('sid'))
413 file_props = gajim.socks5queue.get_file_props(
414 self.name, sid)
415 streamhosts=[]
416 for item in query.getChildren():
417 if item.getName() == 'streamhost':
418 host_dict={
419 'state': 0,
420 'target': target,
421 'id': id_,
422 'sid': sid,
423 'initiator': helpers.get_full_jid_from_iq(iq_obj)
424 }
425 for attr in item.getAttrs():
426 host_dict[attr] = item.getAttr(attr)
427 streamhosts.append(host_dict)
428 if file_props is None:
429 if sid in self.files_props:
430 file_props = self.files_props[sid]
431 file_props['fast'] = streamhosts
432 if file_props['type'] == 's': # FIXME: remove fast xmlns
433 # only psi do this
435 if 'streamhosts' in file_props:
436 file_props['streamhosts'].extend(streamhosts)
437 else:
438 file_props['streamhosts'] = streamhosts
439 if not gajim.socks5queue.get_file_props(self.name, sid):
440 gajim.socks5queue.add_file_props(self.name, file_props)
441 gajim.socks5queue.connect_to_hosts(self.name, sid,
442 self.send_success_connect_reply, None)
443 raise common.xmpp.NodeProcessed
445 file_props['streamhosts'] = streamhosts
446 if file_props['type'] == 'r':
447 gajim.socks5queue.connect_to_hosts(self.name, sid,
448 self.send_success_connect_reply, self._connect_error)
449 raise common.xmpp.NodeProcessed
451 def _ResultCB(self, con, iq_obj):
452 log.debug('_ResultCB')
453 # if we want to respect xep-0065 we have to check for proxy
454 # activation result in any result iq
455 real_id = unicode(iq_obj.getAttr('id'))
456 if real_id == self.awaiting_xmpp_ping_id:
457 self.awaiting_xmpp_ping_id = None
458 return
459 if not real_id.startswith('au_'):
460 return
461 frm = helpers.get_full_jid_from_iq(iq_obj)
462 id_ = real_id[3:]
463 if id_ in self.files_props:
464 file_props = self.files_props[id_]
465 if file_props['streamhost-used']:
466 for host in file_props['proxyhosts']:
467 if host['initiator'] == frm and 'idx' in host:
468 gajim.socks5queue.activate_proxy(host['idx'])
469 raise common.xmpp.NodeProcessed
471 def _bytestreamResultCB(self, con, iq_obj):
472 log.debug('_bytestreamResultCB')
473 frm = helpers.get_full_jid_from_iq(iq_obj)
474 real_id = unicode(iq_obj.getAttr('id'))
475 query = iq_obj.getTag('query')
476 gajim.proxy65_manager.resolve_result(frm, query)
478 try:
479 streamhost = query.getTag('streamhost-used')
480 except Exception: # this bytestream result is not what we need
481 pass
482 id_ = real_id[3:]
483 if id_ in self.files_props:
484 file_props = self.files_props[id_]
485 else:
486 raise common.xmpp.NodeProcessed
487 if streamhost is None:
488 # proxy approves the activate query
489 if real_id.startswith('au_'):
490 if 'streamhost-used' not in file_props or \
491 file_props['streamhost-used'] is False:
492 raise common.xmpp.NodeProcessed
493 if 'proxyhosts' not in file_props:
494 raise common.xmpp.NodeProcessed
495 for host in file_props['proxyhosts']:
496 if host['initiator'] == frm and \
497 unicode(query.getAttr('sid')) == file_props['sid']:
498 gajim.socks5queue.activate_proxy(host['idx'])
499 break
500 raise common.xmpp.NodeProcessed
501 jid = helpers.parse_jid(streamhost.getAttr('jid'))
502 if 'streamhost-used' in file_props and \
503 file_props['streamhost-used'] is True:
504 raise common.xmpp.NodeProcessed
506 if real_id.startswith('au_'):
507 if 'stopped' in file and file_props['stopped']:
508 self.remove_transfer(file_props)
509 else:
510 gajim.socks5queue.send_file(file_props, self.name)
511 raise common.xmpp.NodeProcessed
513 proxy = None
514 if 'proxyhosts' in file_props:
515 for proxyhost in file_props['proxyhosts']:
516 if proxyhost['jid'] == jid:
517 proxy = proxyhost
519 if proxy is not None:
520 file_props['streamhost-used'] = True
521 if 'streamhosts' not in file_props:
522 file_props['streamhosts'] = []
523 file_props['streamhosts'].append(proxy)
524 file_props['is_a_proxy'] = True
525 receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy,
526 file_props['sid'], file_props)
527 gajim.socks5queue.add_receiver(self.name, receiver)
528 proxy['idx'] = receiver.queue_idx
529 gajim.socks5queue.on_success = self._proxy_auth_ok
530 raise common.xmpp.NodeProcessed
532 else:
533 if 'stopped' in file_props and file_props['stopped']:
534 self.remove_transfer(file_props)
535 else:
536 gajim.socks5queue.send_file(file_props, self.name)
537 if 'fast' in file_props:
538 fasts = file_props['fast']
539 if len(fasts) > 0:
540 self._connect_error(frm, fasts[0]['id'], file_props['sid'],
541 code = 406)
543 raise common.xmpp.NodeProcessed
545 def _siResultCB(self, con, iq_obj):
546 log.debug('_siResultCB')
547 id_ = iq_obj.getAttr('id')
548 if id_ not in self.files_props:
549 # no such jid
550 return
551 file_props = self.files_props[id_]
552 if file_props is None:
553 # file properties for jid is none
554 return
555 if 'request-id' in file_props:
556 # we have already sent streamhosts info
557 return
558 file_props['receiver'] = helpers.get_full_jid_from_iq(iq_obj)
559 si = iq_obj.getTag('si')
560 file_tag = si.getTag('file')
561 range_tag = None
562 if file_tag:
563 range_tag = file_tag.getTag('range')
564 if range_tag:
565 offset = range_tag.getAttr('offset')
566 if offset:
567 file_props['offset'] = int(offset)
568 length = range_tag.getAttr('length')
569 if length:
570 file_props['length'] = int(length)
571 feature = si.setTag('feature')
572 if feature.getNamespace() != common.xmpp.NS_FEATURE:
573 return
574 form_tag = feature.getTag('x')
575 form = common.xmpp.DataForm(node=form_tag)
576 field = form.getField('stream-method')
577 if field.getValue() != common.xmpp.NS_BYTESTREAM:
578 return
579 self.send_socks5_info(file_props, fast = True)
580 raise common.xmpp.NodeProcessed
582 def _siSetCB(self, con, iq_obj):
583 log.debug('_siSetCB')
584 jid = helpers.get_jid_from_iq(iq_obj)
585 file_props = {'type': 'r'}
586 file_props['sender'] = helpers.get_full_jid_from_iq(iq_obj)
587 file_props['request-id'] = unicode(iq_obj.getAttr('id'))
588 si = iq_obj.getTag('si')
589 profile = si.getAttr('profile')
590 mime_type = si.getAttr('mime-type')
591 if profile != common.xmpp.NS_FILE:
592 self.send_file_rejection(file_props, code='400', typ='profile')
593 raise common.xmpp.NodeProcessed
594 feature_tag = si.getTag('feature', namespace=common.xmpp.NS_FEATURE)
595 if not feature_tag:
596 return
597 form_tag = feature_tag.getTag('x', namespace=common.xmpp.NS_DATA)
598 if not form_tag:
599 return
600 form = common.dataforms.ExtendForm(node=form_tag)
601 for f in form.iter_fields():
602 if f.var == 'stream-method' and f.type == 'list-single':
603 values = [o[1] for o in f.options]
604 if common.xmpp.NS_BYTESTREAM in values:
605 break
606 else:
607 self.send_file_rejection(file_props, code='400', typ='stream')
608 raise common.xmpp.NodeProcessed
609 file_tag = si.getTag('file')
610 for attribute in file_tag.getAttrs():
611 if attribute in ('name', 'size', 'hash', 'date'):
612 val = file_tag.getAttr(attribute)
613 if val is None:
614 continue
615 file_props[attribute] = val
616 file_desc_tag = file_tag.getTag('desc')
617 if file_desc_tag is not None:
618 file_props['desc'] = file_desc_tag.getData()
620 if mime_type is not None:
621 file_props['mime-type'] = mime_type
622 our_jid = gajim.get_jid_from_account(self.name)
623 resource = self.server_resource
624 file_props['receiver'] = our_jid + '/' + resource
625 file_props['sid'] = unicode(si.getAttr('id'))
626 file_props['transfered_size'] = []
627 gajim.socks5queue.add_file_props(self.name, file_props)
628 self.dispatch('FILE_REQUEST', (jid, file_props))
629 raise common.xmpp.NodeProcessed
631 def _siErrorCB(self, con, iq_obj):
632 log.debug('_siErrorCB')
633 si = iq_obj.getTag('si')
634 profile = si.getAttr('profile')
635 if profile != common.xmpp.NS_FILE:
636 return
637 id_ = iq_obj.getAttr('id')
638 if id_ not in self.files_props:
639 # no such jid
640 return
641 file_props = self.files_props[id_]
642 if file_props is None:
643 # file properties for jid is none
644 return
645 jid = helpers.get_jid_from_iq(iq_obj)
646 file_props['error'] = -3
647 self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, ''))
648 raise common.xmpp.NodeProcessed
650 class ConnectionDisco:
651 ''' hold xmpppy handlers and public methods for discover services'''
652 def discoverItems(self, jid, node = None, id_prefix = None):
653 '''According to XEP-0030: jid is mandatory,
654 name, node, action is optional.'''
655 self._discover(common.xmpp.NS_DISCO_ITEMS, jid, node, id_prefix)
657 def discoverInfo(self, jid, node = None, id_prefix = None):
658 '''According to XEP-0030:
659 For identity: category, type is mandatory, name is optional.
660 For feature: var is mandatory'''
661 self._discover(common.xmpp.NS_DISCO_INFO, jid, node, id_prefix)
663 def request_register_agent_info(self, agent):
664 if not self.connection or self.connected < 2:
665 return None
666 iq = common.xmpp.Iq('get', common.xmpp.NS_REGISTER, to=agent)
667 id_ = self.connection.getAnID()
668 iq.setID(id_)
669 # Wait the answer during 30 secondes
670 self.awaiting_timeouts[gajim.idlequeue.current_time() + 30] = (id_,
671 _('Registration information for transport %s has not arrived in time')\
672 % agent)
673 self.connection.SendAndCallForResponse(iq, self._ReceivedRegInfo,
674 {'agent': agent})
676 def _agent_registered_cb(self, con, resp, agent):
677 if resp.getType() == 'result':
678 self.dispatch('INFORMATION', (_('Registration succeeded'),
679 _('Registration with agent %s succeeded') % agent))
680 if resp.getType() == 'error':
681 self.dispatch('ERROR', (_('Registration failed'), _('Registration with'
682 ' agent %(agent)s failed with error %(error)s: %(error_msg)s') % {
683 'agent': agent, 'error': resp.getError(),
684 'error_msg': resp.getErrorMsg()}))
686 def register_agent(self, agent, info, is_form = False):
687 if not self.connection or self.connected < 2:
688 return
689 if is_form:
690 iq = common.xmpp.Iq('set', common.xmpp.NS_REGISTER, to = agent)
691 query = iq.getTag('query')
692 info.setAttr('type', 'submit')
693 query.addChild(node = info)
694 self.connection.SendAndCallForResponse(iq, self._agent_registered_cb,
695 {'agent': agent})
696 else:
697 # fixed: blocking
698 common.xmpp.features_nb.register(self.connection, agent, info, None)
700 def _discover(self, ns, jid, node = None, id_prefix = None):
701 if not self.connection or self.connected < 2:
702 return
703 iq = common.xmpp.Iq(typ = 'get', to = jid, queryNS = ns)
704 if id_prefix:
705 id_ = self.connection.getAnID()
706 iq.setID('%s%s' % (id_prefix, id_))
707 if node:
708 iq.setQuerynode(node)
709 self.connection.send(iq)
711 def _ReceivedRegInfo(self, con, resp, agent):
712 common.xmpp.features_nb._ReceivedRegInfo(con, resp, agent)
713 self._IqCB(con, resp)
715 def _discoGetCB(self, con, iq_obj):
716 ''' get disco info '''
717 if not self.connection or self.connected < 2:
718 return
719 frm = helpers.get_full_jid_from_iq(iq_obj)
720 to = unicode(iq_obj.getAttr('to'))
721 id_ = unicode(iq_obj.getAttr('id'))
722 iq = common.xmpp.Iq(to = frm, typ = 'result', queryNS =\
723 common.xmpp.NS_DISCO, frm = to)
724 iq.setAttr('id', id_)
725 query = iq.setTag('query')
726 query.setAttr('node','http://gajim.org#' + gajim.version.split('-',
727 1)[0])
728 for f in (common.xmpp.NS_BYTESTREAM, common.xmpp.NS_SI,
729 common.xmpp.NS_FILE, common.xmpp.NS_COMMANDS):
730 feature = common.xmpp.Node('feature')
731 feature.setAttr('var', f)
732 query.addChild(node=feature)
734 self.connection.send(iq)
735 raise common.xmpp.NodeProcessed
737 def _DiscoverItemsErrorCB(self, con, iq_obj):
738 log.debug('DiscoverItemsErrorCB')
739 jid = helpers.get_full_jid_from_iq(iq_obj)
740 self.dispatch('AGENT_ERROR_ITEMS', (jid))
742 def _DiscoverItemsCB(self, con, iq_obj):
743 log.debug('DiscoverItemsCB')
744 q = iq_obj.getTag('query')
745 node = q.getAttr('node')
746 if not node:
747 node = ''
748 qp = iq_obj.getQueryPayload()
749 items = []
750 if not qp:
751 qp = []
752 for i in qp:
753 # CDATA payload is not processed, only nodes
754 if not isinstance(i, common.xmpp.simplexml.Node):
755 continue
756 attr = {}
757 for key in i.getAttrs():
758 attr[key] = i.getAttrs()[key]
759 if 'jid' not in attr:
760 continue
761 try:
762 attr['jid'] = helpers.parse_jid(attr['jid'])
763 except common.helpers.InvalidFormat:
764 # jid is not conform
765 continue
766 items.append(attr)
767 jid = helpers.get_full_jid_from_iq(iq_obj)
768 hostname = gajim.config.get_per('accounts', self.name,
769 'hostname')
770 id_ = iq_obj.getID()
771 if jid == hostname and id_[:6] == 'Gajim_':
772 for item in items:
773 self.discoverInfo(item['jid'], id_prefix='Gajim_')
774 else:
775 self.dispatch('AGENT_INFO_ITEMS', (jid, node, items))
777 def _DiscoverItemsGetCB(self, con, iq_obj):
778 log.debug('DiscoverItemsGetCB')
780 if not self.connection or self.connected < 2:
781 return
783 if self.commandItemsQuery(con, iq_obj):
784 raise common.xmpp.NodeProcessed
785 node = iq_obj.getTagAttr('query', 'node')
786 if node is None:
787 result = iq_obj.buildReply('result')
788 self.connection.send(result)
789 raise common.xmpp.NodeProcessed
790 if node==common.xmpp.NS_COMMANDS:
791 self.commandListQuery(con, iq_obj)
792 raise common.xmpp.NodeProcessed
794 def _DiscoverInfoGetCB(self, con, iq_obj):
795 log.debug('DiscoverInfoGetCB')
796 if not self.connection or self.connected < 2:
797 return
798 q = iq_obj.getTag('query')
799 node = q.getAttr('node')
801 if self.commandInfoQuery(con, iq_obj):
802 raise common.xmpp.NodeProcessed
804 id_ = unicode(iq_obj.getAttr('id'))
805 if id_[:6] == 'Gajim_':
806 # We get this request from echo.server
807 raise common.xmpp.NodeProcessed
809 iq = iq_obj.buildReply('result')
810 q = iq.getTag('query')
811 if node:
812 q.setAttr('node', node)
813 q.addChild('identity', attrs = gajim.gajim_identity)
814 client_version = 'http://gajim.org#' + gajim.caps_hash[self.name]
816 if node in (None, client_version):
817 for f in gajim.gajim_common_features:
818 q.addChild('feature', attrs = {'var': f})
819 for f in gajim.gajim_optional_features[self.name]:
820 q.addChild('feature', attrs = {'var': f})
822 if q.getChildren():
823 self.connection.send(iq)
824 raise common.xmpp.NodeProcessed
826 def _DiscoverInfoErrorCB(self, con, iq_obj):
827 log.debug('DiscoverInfoErrorCB')
828 jid = helpers.get_full_jid_from_iq(iq_obj)
829 self.dispatch('AGENT_ERROR_INFO', (jid))
831 def _DiscoverInfoCB(self, con, iq_obj):
832 log.debug('DiscoverInfoCB')
833 if not self.connection or self.connected < 2:
834 return
835 # According to XEP-0030:
836 # For identity: category, type is mandatory, name is optional.
837 # For feature: var is mandatory
838 identities, features, data = [], [], []
839 q = iq_obj.getTag('query')
840 node = q.getAttr('node')
841 if not node:
842 node = ''
843 qc = iq_obj.getQueryChildren()
844 if not qc:
845 qc = []
846 is_muc = False
847 transport_type = ''
848 for i in qc:
849 if i.getName() == 'identity':
850 attr = {}
851 for key in i.getAttrs().keys():
852 attr[key] = i.getAttr(key)
853 if 'category' in attr and \
854 attr['category'] in ('gateway', 'headline') and \
855 'type' in attr:
856 transport_type = attr['type']
857 if 'category' in attr and \
858 attr['category'] == 'conference' and \
859 'type' in attr and attr['type'] == 'text':
860 is_muc = True
861 identities.append(attr)
862 elif i.getName() == 'feature':
863 features.append(i.getAttr('var'))
864 elif i.getName() == 'x' and i.getNamespace() == common.xmpp.NS_DATA:
865 data.append(common.xmpp.DataForm(node=i))
866 jid = helpers.get_full_jid_from_iq(iq_obj)
867 if transport_type and jid not in gajim.transport_type:
868 gajim.transport_type[jid] = transport_type
869 gajim.logger.save_transport_type(jid, transport_type)
870 id_ = iq_obj.getID()
871 if id_ is None:
872 log.warn('Invalid IQ received without an ID. Ignoring it: %s' % iq_obj)
873 return
874 if not identities: # ejabberd doesn't send identities when we browse online users
875 #FIXME: see http://www.jabber.ru/bugzilla/show_bug.cgi?id=225
876 identities = [{'category': 'server', 'type': 'im', 'name': node}]
877 if id_[:6] == 'Gajim_':
878 if jid == gajim.config.get_per('accounts', self.name, 'hostname'):
879 if features.__contains__(common.xmpp.NS_GMAILNOTIFY):
880 gajim.gmail_domains.append(jid)
881 self.request_gmail_notifications()
882 for identity in identities:
883 if identity['category'] == 'pubsub' and identity.get('type') == \
884 'pep':
885 self.pep_supported = True
886 if dbus_support.supported:
887 listener = MusicTrackListener.get()
888 track = listener.get_playing_track()
889 if gajim.config.get_per('accounts', self.name,
890 'publish_tune'):
891 gajim.interface.roster.music_track_changed(listener,
892 track, self.name)
893 break
894 if features.__contains__(common.xmpp.NS_PUBSUB):
895 self.pubsub_supported = True
896 if features.__contains__(common.xmpp.NS_BYTESTREAM):
897 our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name) +\
898 '/' + self.server_resource)
899 gajim.proxy65_manager.resolve(jid, self.connection, our_jid,
900 self.name)
901 if features.__contains__(common.xmpp.NS_MUC) and is_muc:
902 type_ = transport_type or 'jabber'
903 self.muc_jid[type_] = jid
904 if transport_type:
905 if transport_type in self.available_transports:
906 self.available_transports[transport_type].append(jid)
907 else:
908 self.available_transports[transport_type] = [jid]
910 self.dispatch('AGENT_INFO_INFO', (jid, node, identities,
911 features, data))
912 self._capsDiscoCB(jid, node, identities, features, data)
914 class ConnectionVcard:
915 def __init__(self):
916 self.vcard_sha = None
917 self.vcard_shas = {} # sha of contacts
918 self.room_jids = [] # list of gc jids so that vcard are saved in a folder
920 def add_sha(self, p, send_caps = True):
921 c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE)
922 if self.vcard_sha is not None:
923 c.setTagData('photo', self.vcard_sha)
924 if send_caps:
925 return self.add_caps(p)
926 return p
928 def add_caps(self, p):
929 ''' advertise our capabilities in presence stanza (xep-0115)'''
930 c = p.setTag('c', namespace = common.xmpp.NS_CAPS)
931 c.setAttr('hash', 'sha-1')
932 c.setAttr('node', 'http://gajim.org')
933 c.setAttr('ver', gajim.caps_hash[self.name])
934 return p
936 def node_to_dict(self, node):
937 dict_ = {}
938 for info in node.getChildren():
939 name = info.getName()
940 if name in ('ADR', 'TEL', 'EMAIL'): # we can have several
941 dict_.setdefault(name, [])
942 entry = {}
943 for c in info.getChildren():
944 entry[c.getName()] = c.getData()
945 dict_[name].append(entry)
946 elif info.getChildren() == []:
947 dict_[name] = info.getData()
948 else:
949 dict_[name] = {}
950 for c in info.getChildren():
951 dict_[name][c.getName()] = c.getData()
952 return dict_
954 def save_vcard_to_hd(self, full_jid, card):
955 jid, nick = gajim.get_room_and_nick_from_fjid(full_jid)
956 puny_jid = helpers.sanitize_filename(jid)
957 path = os.path.join(gajim.VCARD_PATH, puny_jid)
958 if jid in self.room_jids or os.path.isdir(path):
959 if not nick:
960 return
961 # remove room_jid file if needed
962 if os.path.isfile(path):
963 os.remove(path)
964 # create folder if needed
965 if not os.path.isdir(path):
966 os.mkdir(path, 0700)
967 puny_nick = helpers.sanitize_filename(nick)
968 path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
969 else:
970 path_to_file = path
971 try:
972 fil = open(path_to_file, 'w')
973 fil.write(str(card))
974 fil.close()
975 except IOError, e:
976 self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
978 def get_cached_vcard(self, fjid, is_fake_jid = False):
979 '''return the vcard as a dict
980 return {} if vcard was too old
981 return None if we don't have cached vcard'''
982 jid, nick = gajim.get_room_and_nick_from_fjid(fjid)
983 puny_jid = helpers.sanitize_filename(jid)
984 if is_fake_jid:
985 puny_nick = helpers.sanitize_filename(nick)
986 path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick)
987 else:
988 path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid)
989 if not os.path.isfile(path_to_file):
990 return None
991 # We have the vcard cached
992 f = open(path_to_file)
993 c = f.read()
994 f.close()
995 try:
996 card = common.xmpp.Node(node = c)
997 except Exception:
998 # We are unable to parse it. Remove it
999 os.remove(path_to_file)
1000 return None
1001 vcard = self.node_to_dict(card)
1002 if 'PHOTO' in vcard:
1003 if not isinstance(vcard['PHOTO'], dict):
1004 del vcard['PHOTO']
1005 elif 'SHA' in vcard['PHOTO']:
1006 cached_sha = vcard['PHOTO']['SHA']
1007 if jid in self.vcard_shas and self.vcard_shas[jid] != \
1008 cached_sha:
1009 # user change his vcard so don't use the cached one
1010 return {}
1011 vcard['jid'] = jid
1012 vcard['resource'] = gajim.get_resource_from_jid(fjid)
1013 return vcard
1015 def request_vcard(self, jid = None, groupchat_jid = None):
1016 '''request the VCARD. If groupchat_jid is not nul, it means we request a vcard
1017 to a fake jid, like in private messages in groupchat. jid can be the
1018 real jid of the contact, but we want to consider it comes from a fake jid'''
1019 if not self.connection or self.connected < 2:
1020 return
1021 iq = common.xmpp.Iq(typ = 'get')
1022 if jid:
1023 iq.setTo(jid)
1024 iq.setTag(common.xmpp.NS_VCARD + ' vCard')
1026 id_ = self.connection.getAnID()
1027 iq.setID(id_)
1028 j = jid
1029 if not j:
1030 j = gajim.get_jid_from_account(self.name)
1031 self.awaiting_answers[id_] = (VCARD_ARRIVED, j, groupchat_jid)
1032 if groupchat_jid:
1033 room_jid = gajim.get_room_and_nick_from_fjid(groupchat_jid)[0]
1034 if not room_jid in self.room_jids:
1035 self.room_jids.append(room_jid)
1036 self.groupchat_jids[id_] = groupchat_jid
1037 self.connection.send(iq)
1039 def send_vcard(self, vcard):
1040 if not self.connection or self.connected < 2:
1041 return
1042 iq = common.xmpp.Iq(typ = 'set')
1043 iq2 = iq.setTag(common.xmpp.NS_VCARD + ' vCard')
1044 for i in vcard:
1045 if i == 'jid':
1046 continue
1047 if isinstance(vcard[i], dict):
1048 iq3 = iq2.addChild(i)
1049 for j in vcard[i]:
1050 iq3.addChild(j).setData(vcard[i][j])
1051 elif isinstance(vcard[i], list):
1052 for j in vcard[i]:
1053 iq3 = iq2.addChild(i)
1054 for k in j:
1055 iq3.addChild(k).setData(j[k])
1056 else:
1057 iq2.addChild(i).setData(vcard[i])
1059 id_ = self.connection.getAnID()
1060 iq.setID(id_)
1061 self.connection.send(iq)
1063 our_jid = gajim.get_jid_from_account(self.name)
1064 # Add the sha of the avatar
1065 if 'PHOTO' in vcard and isinstance(vcard['PHOTO'], dict) and \
1066 'BINVAL' in vcard['PHOTO']:
1067 photo = vcard['PHOTO']['BINVAL']
1068 photo_decoded = base64.decodestring(photo)
1069 gajim.interface.save_avatar_files(our_jid, photo_decoded)
1070 avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
1071 iq2.getTag('PHOTO').setTagData('SHA', avatar_sha)
1072 else:
1073 gajim.interface.remove_avatar_files(our_jid)
1075 self.awaiting_answers[id_] = (VCARD_PUBLISHED, iq2)
1077 def _IqCB(self, con, iq_obj):
1078 id_ = iq_obj.getID()
1080 # Check if we were waiting a timeout for this id
1081 found_tim = None
1082 for tim in self.awaiting_timeouts:
1083 if id_ == self.awaiting_timeouts[tim][0]:
1084 found_tim = tim
1085 break
1086 if found_tim:
1087 del self.awaiting_timeouts[found_tim]
1089 if id_ not in self.awaiting_answers:
1090 return
1091 if self.awaiting_answers[id_][0] == VCARD_PUBLISHED:
1092 if iq_obj.getType() == 'result':
1093 vcard_iq = self.awaiting_answers[id_][1]
1094 # Save vcard to HD
1095 if vcard_iq.getTag('PHOTO') and vcard_iq.getTag('PHOTO').getTag('SHA'):
1096 new_sha = vcard_iq.getTag('PHOTO').getTagData('SHA')
1097 else:
1098 new_sha = ''
1100 # Save it to file
1101 our_jid = gajim.get_jid_from_account(self.name)
1102 self.save_vcard_to_hd(our_jid, vcard_iq)
1104 # Send new presence if sha changed and we are not invisible
1105 if self.vcard_sha != new_sha and gajim.SHOW_LIST[self.connected] !=\
1106 'invisible':
1107 if not self.connection or self.connected < 2:
1108 return
1109 self.vcard_sha = new_sha
1110 sshow = helpers.get_xmpp_show(gajim.SHOW_LIST[self.connected])
1111 p = common.xmpp.Presence(typ = None, priority = self.priority,
1112 show = sshow, status = self.status)
1113 p = self.add_sha(p)
1114 self.connection.send(p)
1115 self.dispatch('VCARD_PUBLISHED', ())
1116 elif iq_obj.getType() == 'error':
1117 self.dispatch('VCARD_NOT_PUBLISHED', ())
1118 elif self.awaiting_answers[id_][0] == VCARD_ARRIVED:
1119 # If vcard is empty, we send to the interface an empty vcard so that
1120 # it knows it arrived
1121 jid = self.awaiting_answers[id_][1]
1122 groupchat_jid = self.awaiting_answers[id_][2]
1123 frm = jid
1124 if groupchat_jid:
1125 # We do as if it comes from the fake_jid
1126 frm = groupchat_jid
1127 our_jid = gajim.get_jid_from_account(self.name)
1128 if iq_obj.getType() == 'error' and jid == our_jid:
1129 # our server doesn't support vcard
1130 log.debug('xxx error xxx')
1131 self.vcard_supported = False
1132 if not iq_obj.getTag('vCard') or iq_obj.getType() == 'error':
1133 if frm and frm != our_jid:
1134 # Write an empty file
1135 self.save_vcard_to_hd(frm, '')
1136 jid, resource = gajim.get_room_and_nick_from_fjid(frm)
1137 self.dispatch('VCARD', {'jid': jid, 'resource': resource})
1138 elif frm == our_jid:
1139 self.dispatch('MYVCARD', {'jid': frm})
1140 elif self.awaiting_answers[id_][0] == AGENT_REMOVED:
1141 jid = self.awaiting_answers[id_][1]
1142 self.dispatch('AGENT_REMOVED', jid)
1143 elif self.awaiting_answers[id_][0] == METACONTACTS_ARRIVED:
1144 if not self.connection:
1145 return
1146 if iq_obj.getType() == 'result':
1147 # Metacontact tags
1148 # http://www.xmpp.org/extensions/xep-0209.html
1149 meta_list = {}
1150 query = iq_obj.getTag('query')
1151 storage = query.getTag('storage')
1152 metas = storage.getTags('meta')
1153 for meta in metas:
1154 try:
1155 jid = helpers.parse_jid(meta.getAttr('jid'))
1156 except common.helpers.InvalidFormat:
1157 continue
1158 tag = meta.getAttr('tag')
1159 data = {'jid': jid}
1160 order = meta.getAttr('order')
1161 try:
1162 order = int(order)
1163 except Exception:
1164 order = 0
1165 if order is not None:
1166 data['order'] = order
1167 if tag in meta_list:
1168 meta_list[tag].append(data)
1169 else:
1170 meta_list[tag] = [data]
1171 self.dispatch('METACONTACTS', meta_list)
1172 else:
1173 if iq_obj.getErrorCode() not in ('403', '406', '404'):
1174 self.private_storage_supported = False
1175 # We can now continue connection by requesting the roster
1176 version = gajim.config.get_per('accounts', self.name,
1177 'roster_version')
1178 iq_id = self.connection.initRoster(version=version)
1179 self.awaiting_answers[iq_id] = (ROSTER_ARRIVED, )
1180 elif self.awaiting_answers[id_][0] == ROSTER_ARRIVED:
1181 if iq_obj.getType() == 'result':
1182 if not iq_obj.getTag('query'):
1183 account_jid = gajim.get_jid_from_account(self.name)
1184 roster_data = gajim.logger.get_roster(account_jid)
1185 roster = self.connection.getRoster(force=True)
1186 roster.setRaw(roster_data)
1187 self._getRoster()
1188 elif self.awaiting_answers[id_][0] == PRIVACY_ARRIVED:
1189 if iq_obj.getType() != 'error':
1190 self.privacy_rules_supported = True
1191 self.get_privacy_list('block')
1192 elif self.continue_connect_info:
1193 if self.continue_connect_info[0] == 'invisible':
1194 # Trying to login as invisible but privacy list not supported
1195 self.disconnect(on_purpose=True)
1196 self.dispatch('STATUS', 'offline')
1197 self.dispatch('ERROR', (_('Invisibility not supported'),
1198 _('Account %s doesn\'t support invisibility.') % self.name))
1199 return
1200 # Ask metacontacts before roster
1201 self.get_metacontacts()
1202 elif self.awaiting_answers[id_][0] == PEP_CONFIG:
1203 conf = iq_obj.getTag('pubsub').getTag('configure')
1204 node = conf.getAttr('node')
1205 form_tag = conf.getTag('x', namespace=common.xmpp.NS_DATA)
1206 if form_tag:
1207 form = common.dataforms.ExtendForm(node=form_tag)
1208 self.dispatch('PEP_CONFIG', (node, form))
1210 del self.awaiting_answers[id_]
1212 def _vCardCB(self, con, vc):
1213 '''Called when we receive a vCard
1214 Parse the vCard and send it to plugins'''
1215 if not vc.getTag('vCard'):
1216 return
1217 if not vc.getTag('vCard').getNamespace() == common.xmpp.NS_VCARD:
1218 return
1219 id_ = vc.getID()
1220 frm_iq = vc.getFrom()
1221 our_jid = gajim.get_jid_from_account(self.name)
1222 resource = ''
1223 if id_ in self.groupchat_jids:
1224 who = self.groupchat_jids[id_]
1225 frm, resource = gajim.get_room_and_nick_from_fjid(who)
1226 del self.groupchat_jids[id_]
1227 elif frm_iq:
1228 who = helpers.get_full_jid_from_iq(vc)
1229 frm, resource = gajim.get_room_and_nick_from_fjid(who)
1230 else:
1231 who = frm = our_jid
1232 card = vc.getChildren()[0]
1233 vcard = self.node_to_dict(card)
1234 photo_decoded = None
1235 if 'PHOTO' in vcard and isinstance(vcard['PHOTO'], dict) and \
1236 'BINVAL' in vcard['PHOTO']:
1237 photo = vcard['PHOTO']['BINVAL']
1238 try:
1239 photo_decoded = base64.decodestring(photo)
1240 avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
1241 except Exception:
1242 avatar_sha = ''
1243 else:
1244 avatar_sha = ''
1246 if avatar_sha:
1247 card.getTag('PHOTO').setTagData('SHA', avatar_sha)
1249 # Save it to file
1250 self.save_vcard_to_hd(who, card)
1251 # Save the decoded avatar to a separate file too, and generate files for dbus notifications
1252 puny_jid = helpers.sanitize_filename(frm)
1253 puny_nick = None
1254 begin_path = os.path.join(gajim.AVATAR_PATH, puny_jid)
1255 frm_jid = frm
1256 if frm in self.room_jids:
1257 puny_nick = helpers.sanitize_filename(resource)
1258 # create folder if needed
1259 if not os.path.isdir(begin_path):
1260 os.mkdir(begin_path, 0700)
1261 begin_path = os.path.join(begin_path, puny_nick)
1262 frm_jid += '/' + resource
1263 if photo_decoded:
1264 avatar_file = begin_path + '_notif_size_colored.png'
1265 if frm_jid == our_jid and avatar_sha != self.vcard_sha:
1266 gajim.interface.save_avatar_files(frm, photo_decoded, puny_nick)
1267 elif frm_jid != our_jid and (not os.path.exists(avatar_file) or \
1268 frm_jid not in self.vcard_shas or \
1269 avatar_sha != self.vcard_shas[frm_jid]):
1270 gajim.interface.save_avatar_files(frm, photo_decoded, puny_nick)
1271 if avatar_sha:
1272 self.vcard_shas[frm_jid] = avatar_sha
1273 elif frm in self.vcard_shas:
1274 del self.vcard_shas[frm]
1275 else:
1276 for ext in ('.jpeg', '.png', '_notif_size_bw.png',
1277 '_notif_size_colored.png'):
1278 path = begin_path + ext
1279 if os.path.isfile(path):
1280 os.remove(path)
1282 vcard['jid'] = frm
1283 vcard['resource'] = resource
1284 if frm_jid == our_jid:
1285 self.dispatch('MYVCARD', vcard)
1286 # we re-send our presence with sha if has changed and if we are
1287 # not invisible
1288 if self.vcard_sha == avatar_sha:
1289 return
1290 self.vcard_sha = avatar_sha
1291 if gajim.SHOW_LIST[self.connected] == 'invisible':
1292 return
1293 if not self.connection:
1294 return
1295 sshow = helpers.get_xmpp_show(gajim.SHOW_LIST[self.connected])
1296 p = common.xmpp.Presence(typ = None, priority = self.priority,
1297 show = sshow, status = self.status)
1298 p = self.add_sha(p)
1299 self.connection.send(p)
1300 else:
1301 #('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...})
1302 self.dispatch('VCARD', vcard)
1304 # basic connection handlers used here and in zeroconf
1305 class ConnectionHandlersBase:
1306 def __init__(self):
1307 # List of IDs we are waiting answers for {id: (type_of_request, data), }
1308 self.awaiting_answers = {}
1309 # List of IDs that will produce a timeout is answer doesn't arrive
1310 # {time_of_the_timeout: (id, message to send to gui), }
1311 self.awaiting_timeouts = {}
1312 # keep the jids we auto added (transports contacts) to not send the
1313 # SUBSCRIBED event to gui
1314 self.automatically_added = []
1316 # keep track of sessions this connection has with other JIDs
1317 self.sessions = {}
1319 def get_sessions(self, jid):
1320 '''get all sessions for the given full jid'''
1322 if not gajim.interface.is_pm_contact(jid, self.name):
1323 jid = gajim.get_jid_without_resource(jid)
1325 try:
1326 return self.sessions[jid].values()
1327 except KeyError:
1328 return []
1330 def get_or_create_session(self, fjid, thread_id):
1331 '''returns an existing session between this connection and 'jid', returns a
1332 new one if none exist.'''
1334 pm = True
1335 jid = fjid
1337 if not gajim.interface.is_pm_contact(fjid, self.name):
1338 pm = False
1339 jid = gajim.get_jid_without_resource(fjid)
1341 session = self.find_session(jid, thread_id)
1343 if session:
1344 return session
1346 if pm:
1347 return self.make_new_session(fjid, thread_id, type_='pm')
1348 else:
1349 return self.make_new_session(fjid, thread_id)
1351 def find_session(self, jid, thread_id):
1352 try:
1353 if not thread_id:
1354 return self.find_null_session(jid)
1355 else:
1356 return self.sessions[jid][thread_id]
1357 except KeyError:
1358 return None
1360 def terminate_sessions(self, send_termination=False):
1361 '''send termination messages and delete all active sessions'''
1362 for jid in self.sessions:
1363 for thread_id in self.sessions[jid]:
1364 self.sessions[jid][thread_id].terminate(send_termination)
1366 self.sessions = {}
1368 def delete_session(self, jid, thread_id):
1369 if not jid in self.sessions:
1370 jid = gajim.get_jid_without_resource(jid)
1371 if not jid in self.sessions:
1372 return
1374 del self.sessions[jid][thread_id]
1376 if not self.sessions[jid]:
1377 del self.sessions[jid]
1379 def find_null_session(self, jid):
1380 '''finds all of the sessions between us and a remote jid in which we
1381 haven't received a thread_id yet and returns the session that we last
1382 sent a message to.'''
1384 sessions = self.sessions[jid].values()
1386 # sessions that we haven't received a thread ID in
1387 idless = [s for s in sessions if not s.received_thread_id]
1389 # filter out everything except the default session type
1390 chat_sessions = [s for s in idless if isinstance(s,
1391 gajim.default_session_type)]
1393 if chat_sessions:
1394 # return the session that we last sent a message in
1395 return sorted(chat_sessions,
1396 key=operator.attrgetter("last_send"))[-1]
1397 else:
1398 return None
1400 def find_controlless_session(self, jid):
1401 '''find an active session that doesn't have a control attached'''
1403 try:
1404 sessions = self.sessions[jid].values()
1406 # filter out everything except the default session type
1407 chat_sessions = [s for s in sessions if isinstance(s,
1408 gajim.default_session_type)]
1410 orphaned = [s for s in chat_sessions if not s.control]
1412 return orphaned[0]
1413 except (KeyError, IndexError):
1414 return None
1416 def make_new_session(self, jid, thread_id=None, type_='chat', cls=None):
1417 '''create and register a new session. thread_id=None to generate one.
1418 type_ should be 'chat' or 'pm'.'''
1419 if not cls:
1420 cls = gajim.default_session_type
1422 sess = cls(self, common.xmpp.JID(jid), thread_id, type_)
1424 # determine if this session is a pm session
1425 # if not, discard the resource so that all sessions are stored bare
1426 if not type_ == 'pm':
1427 jid = gajim.get_jid_without_resource(jid)
1429 if not jid in self.sessions:
1430 self.sessions[jid] = {}
1432 self.sessions[jid][sess.thread_id] = sess
1434 return sess
1436 class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase):
1437 def __init__(self):
1438 ConnectionVcard.__init__(self)
1439 ConnectionBytestream.__init__(self)
1440 ConnectionCommands.__init__(self)
1441 ConnectionPubSub.__init__(self)
1442 ConnectionHandlersBase.__init__(self)
1443 self.gmail_url = None
1445 # keep the latest subscribed event for each jid to prevent loop when we
1446 # acknowledge presences
1447 self.subscribed_events = {}
1448 # IDs of jabber:iq:last requests
1449 self.last_ids = []
1450 # IDs of jabber:iq:version requests
1451 self.version_ids = []
1452 # IDs of urn:xmpp:time requests
1453 self.entity_time_ids = []
1454 # ID of urn:xmpp:ping requests
1455 self.awaiting_xmpp_ping_id = None
1456 self.continue_connect_info = None
1458 try:
1459 idle.init()
1460 except Exception:
1461 global HAS_IDLE
1462 HAS_IDLE = False
1464 self.gmail_last_tid = None
1465 self.gmail_last_time = None
1467 def build_http_auth_answer(self, iq_obj, answer):
1468 if not self.connection or self.connected < 2:
1469 return
1470 if answer == 'yes':
1471 self.connection.send(iq_obj.buildReply('result'))
1472 elif answer == 'no':
1473 err = common.xmpp.Error(iq_obj,
1474 common.xmpp.protocol.ERR_NOT_AUTHORIZED)
1475 self.connection.send(err)
1477 def _HttpAuthCB(self, con, iq_obj):
1478 log.debug('HttpAuthCB')
1479 opt = gajim.config.get_per('accounts', self.name, 'http_auth')
1480 if opt in ('yes', 'no'):
1481 self.build_http_auth_answer(iq_obj, opt)
1482 else:
1483 id_ = iq_obj.getTagAttr('confirm', 'id')
1484 method = iq_obj.getTagAttr('confirm', 'method')
1485 url = iq_obj.getTagAttr('confirm', 'url')
1486 msg = iq_obj.getTagData('body') # In case it's a message with a body
1487 self.dispatch('HTTP_AUTH', (method, url, id_, iq_obj, msg))
1488 raise common.xmpp.NodeProcessed
1490 def _ErrorCB(self, con, iq_obj):
1491 log.debug('ErrorCB')
1492 jid_from = helpers.get_full_jid_from_iq(iq_obj)
1493 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(jid_from)
1494 id_ = unicode(iq_obj.getID())
1495 if id_ in self.version_ids:
1496 self.dispatch('OS_INFO', (jid_stripped, resource, '', ''))
1497 self.version_ids.remove(id_)
1498 return
1499 if id_ in self.last_ids:
1500 self.dispatch('LAST_STATUS_TIME', (jid_stripped, resource, -1, ''))
1501 self.last_ids.remove(id_)
1502 return
1503 if id_ in self.entity_time_ids:
1504 self.dispatch('ENTITY_TIME', (jid_stripped, resource, ''))
1505 self.entity_time_ids.remove(id_)
1506 return
1507 if id_ == self.awaiting_xmpp_ping_id:
1508 self.awaiting_xmpp_ping_id = None
1509 errmsg = iq_obj.getErrorMsg()
1510 errcode = iq_obj.getErrorCode()
1511 self.dispatch('ERROR_ANSWER', (id_, jid_from, errmsg, errcode))
1513 def _PrivateCB(self, con, iq_obj):
1514 '''
1515 Private Data (XEP 048 and 049)
1516 '''
1517 log.debug('PrivateCB')
1518 query = iq_obj.getTag('query')
1519 storage = query.getTag('storage')
1520 if storage:
1521 ns = storage.getNamespace()
1522 if ns == 'storage:bookmarks':
1523 # Bookmarked URLs and Conferences
1524 # http://www.xmpp.org/extensions/xep-0048.html
1525 confs = storage.getTags('conference')
1526 for conf in confs:
1527 autojoin_val = conf.getAttr('autojoin')
1528 if autojoin_val is None: # not there (it's optional)
1529 autojoin_val = False
1530 minimize_val = conf.getAttr('minimize')
1531 if minimize_val is None: # not there (it's optional)
1532 minimize_val = False
1533 print_status = conf.getTagData('print_status')
1534 if not print_status:
1535 print_status = conf.getTagData('show_status')
1536 try:
1537 bm = {'name': conf.getAttr('name'),
1538 'jid': helpers.parse_jid(conf.getAttr('jid')),
1539 'autojoin': autojoin_val,
1540 'minimize': minimize_val,
1541 'password': conf.getTagData('password'),
1542 'nick': conf.getTagData('nick'),
1543 'print_status': print_status}
1544 except common.helpers.InvalidFormat:
1545 log.warn('Invalid JID: %s, ignoring it' % conf.getAttr('jid'))
1546 continue
1548 self.bookmarks.append(bm)
1549 self.dispatch('BOOKMARKS', self.bookmarks)
1551 elif ns == 'gajim:prefs':
1552 # Preferences data
1553 # http://www.xmpp.org/extensions/xep-0049.html
1554 #TODO: implement this
1555 pass
1556 elif ns == 'storage:rosternotes':
1557 # Annotations
1558 # http://www.xmpp.org/extensions/xep-0145.html
1559 notes = storage.getTags('note')
1560 for note in notes:
1561 try:
1562 jid = helpers.parse_jid(note.getAttr('jid'))
1563 except common.helpers.InvalidFormat:
1564 log.warn('Invalid JID: %s, ignoring it' % note.getAttr('jid'))
1565 continue
1566 annotation = note.getData()
1567 self.annotations[jid] = annotation
1569 def _rosterSetCB(self, con, iq_obj):
1570 log.debug('rosterSetCB')
1571 version = iq_obj.getTagAttr('query', 'ver')
1572 for item in iq_obj.getTag('query').getChildren():
1573 try:
1574 jid = helpers.parse_jid(item.getAttr('jid'))
1575 except common.helpers.InvalidFormat:
1576 log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid'))
1577 continue
1578 name = item.getAttr('name')
1579 sub = item.getAttr('subscription')
1580 ask = item.getAttr('ask')
1581 groups = []
1582 for group in item.getTags('group'):
1583 groups.append(group.getData())
1584 self.dispatch('ROSTER_INFO', (jid, name, sub, ask, groups))
1585 account_jid = gajim.get_jid_from_account(self.name)
1586 gajim.logger.add_or_update_contact(account_jid, jid, name, sub, ask,
1587 groups)
1588 if version:
1589 gajim.config.set_per('accounts', self.name, 'roster_version',
1590 version)
1591 if not self.connection or self.connected < 2:
1592 raise common.xmpp.NodeProcessed
1593 reply = common.xmpp.Iq(typ='result', attrs={'id': iq_obj.getID()},
1594 to=iq_obj.getFrom(), frm=iq_obj.getTo(), xmlns=None)
1595 self.connection.send(reply)
1596 raise common.xmpp.NodeProcessed
1598 def _VersionCB(self, con, iq_obj):
1599 log.debug('VersionCB')
1600 if not self.connection or self.connected < 2:
1601 return
1602 iq_obj = iq_obj.buildReply('result')
1603 qp = iq_obj.getTag('query')
1604 qp.setTagData('name', 'Gajim')
1605 qp.setTagData('version', gajim.version)
1606 send_os = gajim.config.get_per('accounts', self.name, 'send_os_info')
1607 if send_os:
1608 qp.setTagData('os', helpers.get_os_info())
1609 self.connection.send(iq_obj)
1610 raise common.xmpp.NodeProcessed
1612 def _LastCB(self, con, iq_obj):
1613 log.debug('LastCB')
1614 if not self.connection or self.connected < 2:
1615 return
1616 iq_obj = iq_obj.buildReply('result')
1617 qp = iq_obj.getTag('query')
1618 if not HAS_IDLE:
1619 qp.attrs['seconds'] = '0'
1620 else:
1621 qp.attrs['seconds'] = idle.getIdleSec()
1623 self.connection.send(iq_obj)
1624 raise common.xmpp.NodeProcessed
1626 def _LastResultCB(self, con, iq_obj):
1627 log.debug('LastResultCB')
1628 qp = iq_obj.getTag('query')
1629 seconds = qp.getAttr('seconds')
1630 status = qp.getData()
1631 try:
1632 seconds = int(seconds)
1633 except Exception:
1634 return
1635 id_ = iq_obj.getID()
1636 if id_ in self.groupchat_jids:
1637 who = self.groupchat_jids[id_]
1638 del self.groupchat_jids[id_]
1639 else:
1640 who = helpers.get_full_jid_from_iq(iq_obj)
1641 if id_ in self.last_ids:
1642 self.last_ids.remove(id_)
1643 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
1644 self.dispatch('LAST_STATUS_TIME', (jid_stripped, resource, seconds, status))
1646 def _VersionResultCB(self, con, iq_obj):
1647 log.debug('VersionResultCB')
1648 client_info = ''
1649 os_info = ''
1650 qp = iq_obj.getTag('query')
1651 if qp.getTag('name'):
1652 client_info += qp.getTag('name').getData()
1653 if qp.getTag('version'):
1654 client_info += ' ' + qp.getTag('version').getData()
1655 if qp.getTag('os'):
1656 os_info += qp.getTag('os').getData()
1657 id_ = iq_obj.getID()
1658 if id_ in self.groupchat_jids:
1659 who = self.groupchat_jids[id_]
1660 del self.groupchat_jids[id_]
1661 else:
1662 who = helpers.get_full_jid_from_iq(iq_obj)
1663 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
1664 if id_ in self.version_ids:
1665 self.version_ids.remove(id_)
1666 self.dispatch('OS_INFO', (jid_stripped, resource, client_info, os_info))
1668 def _TimeCB(self, con, iq_obj):
1669 log.debug('TimeCB')
1670 if not self.connection or self.connected < 2:
1671 return
1672 iq_obj = iq_obj.buildReply('result')
1673 qp = iq_obj.getTag('query')
1674 qp.setTagData('utc', strftime('%Y%m%dT%H:%M:%S', gmtime()))
1675 qp.setTagData('tz', helpers.decode_string(tzname[daylight]))
1676 qp.setTagData('display', helpers.decode_string(strftime('%c',
1677 localtime())))
1678 self.connection.send(iq_obj)
1679 raise common.xmpp.NodeProcessed
1681 def _TimeRevisedCB(self, con, iq_obj):
1682 log.debug('TimeRevisedCB')
1683 if not self.connection or self.connected < 2:
1684 return
1685 iq_obj = iq_obj.buildReply('result')
1686 qp = iq_obj.setTag('time',
1687 namespace=common.xmpp.NS_TIME_REVISED)
1688 qp.setTagData('utc', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()))
1689 isdst = localtime().tm_isdst
1690 zone = -(timezone, altzone)[isdst] / 60
1691 tzo = (zone / 60, abs(zone % 60))
1692 qp.setTagData('tzo', '%+03d:%02d' % (tzo))
1693 self.connection.send(iq_obj)
1694 raise common.xmpp.NodeProcessed
1696 def _TimeRevisedResultCB(self, con, iq_obj):
1697 log.debug('TimeRevisedResultCB')
1698 time_info = ''
1699 qp = iq_obj.getTag('time')
1700 if not qp:
1701 # wrong answer
1702 return
1703 tzo = qp.getTag('tzo').getData()
1704 if tzo == 'Z':
1705 tzo = '0:0'
1706 tzoh, tzom = tzo.split(':')
1707 utc_time = qp.getTag('utc').getData()
1708 ZERO = datetime.timedelta(0)
1709 class UTC(datetime.tzinfo):
1710 def utcoffset(self, dt):
1711 return ZERO
1712 def tzname(self, dt):
1713 return "UTC"
1714 def dst(self, dt):
1715 return ZERO
1717 class contact_tz(datetime.tzinfo):
1718 def utcoffset(self, dt):
1719 return datetime.timedelta(hours=int(tzoh), minutes=int(tzom))
1720 def tzname(self, dt):
1721 return "remote timezone"
1722 def dst(self, dt):
1723 return ZERO
1725 try:
1726 t = datetime.datetime.strptime(utc_time, '%Y-%m-%dT%H:%M:%SZ')
1727 t = t.replace(tzinfo=UTC())
1728 time_info = t.astimezone(contact_tz()).strftime('%c')
1729 except ValueError, e:
1730 log.info('Wrong time format: %s' % str(e))
1732 id_ = iq_obj.getID()
1733 if id_ in self.groupchat_jids:
1734 who = self.groupchat_jids[id_]
1735 del self.groupchat_jids[id_]
1736 else:
1737 who = helpers.get_full_jid_from_iq(iq_obj)
1738 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
1739 if id_ in self.entity_time_ids:
1740 self.entity_time_ids.remove(id_)
1741 self.dispatch('ENTITY_TIME', (jid_stripped, resource, time_info))
1743 def _gMailNewMailCB(self, con, gm):
1744 '''Called when we get notified of new mail messages in gmail account'''
1745 if not self.connection or self.connected < 2:
1746 return
1747 if not gm.getTag('new-mail'):
1748 return
1749 if gm.getTag('new-mail').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
1750 # we'll now ask the server for the exact number of new messages
1751 jid = gajim.get_jid_from_account(self.name)
1752 log.debug('Got notification of new gmail e-mail on %s. Asking the server for more info.' % jid)
1753 iq = common.xmpp.Iq(typ = 'get')
1754 iq.setID(self.connection.getAnID())
1755 query = iq.setTag('query')
1756 query.setNamespace(common.xmpp.NS_GMAILNOTIFY)
1757 # we want only be notified about newer mails
1758 if self.gmail_last_tid:
1759 query.setAttr('newer-than-tid', self.gmail_last_tid)
1760 if self.gmail_last_time:
1761 query.setAttr('newer-than-time', self.gmail_last_time)
1762 self.connection.send(iq)
1763 raise common.xmpp.NodeProcessed
1765 def _gMailQueryCB(self, con, gm):
1766 '''Called when we receive results from Querying the server for mail messages in gmail account'''
1767 if not gm.getTag('mailbox'):
1768 return
1769 self.gmail_url = gm.getTag('mailbox').getAttr('url')
1770 if gm.getTag('mailbox').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
1771 newmsgs = gm.getTag('mailbox').getAttr('total-matched')
1772 if newmsgs != '0':
1773 # there are new messages
1774 gmail_messages_list = []
1775 if gm.getTag('mailbox').getTag('mail-thread-info'):
1776 gmail_messages = gm.getTag('mailbox').getTags('mail-thread-info')
1777 for gmessage in gmail_messages:
1778 unread_senders = []
1779 for sender in gmessage.getTag('senders').getTags('sender'):
1780 if sender.getAttr('unread') != '1':
1781 continue
1782 if sender.getAttr('name'):
1783 unread_senders.append(sender.getAttr('name') + '< ' + \
1784 sender.getAttr('address') + '>')
1785 else:
1786 unread_senders.append(sender.getAttr('address'))
1788 if not unread_senders:
1789 continue
1790 gmail_subject = gmessage.getTag('subject').getData()
1791 gmail_snippet = gmessage.getTag('snippet').getData()
1792 tid = int(gmessage.getAttr('tid'))
1793 if not self.gmail_last_tid or tid > self.gmail_last_tid:
1794 self.gmail_last_tid = tid
1795 gmail_messages_list.append({ \
1796 'From': unread_senders, \
1797 'Subject': gmail_subject, \
1798 'Snippet': gmail_snippet, \
1799 'url': gmessage.getAttr('url'), \
1800 'participation': gmessage.getAttr('participation'), \
1801 'messages': gmessage.getAttr('messages'), \
1802 'date': gmessage.getAttr('date')})
1803 self.gmail_last_time = int(gm.getTag('mailbox').getAttr(
1804 'result-time'))
1806 jid = gajim.get_jid_from_account(self.name)
1807 log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid))
1808 self.dispatch('GMAIL_NOTIFY', (jid, newmsgs, gmail_messages_list))
1809 raise common.xmpp.NodeProcessed
1812 def _rosterItemExchangeCB(self, con, msg):
1813 ''' XEP-0144 Roster Item Echange '''
1814 exchange_items_list = {}
1815 jid_from = helpers.get_full_jid_from_iq(msg)
1816 items_list = msg.getTag('x').getChildren()
1817 action = items_list[0].getAttr('action')
1818 if action == None:
1819 action = 'add'
1820 for item in msg.getTag('x',
1821 namespace=common.xmpp.NS_ROSTERX).getChildren():
1822 try:
1823 jid = helpers.parse_jid(item.getAttr('jid'))
1824 except common.helpers.InvalidFormat:
1825 log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid'))
1826 continue
1827 name = item.getAttr('name')
1828 groups=[]
1829 for group in item.getTags('group'):
1830 groups.append(group.getData())
1831 exchange_items_list[jid] = []
1832 exchange_items_list[jid].append(name)
1833 exchange_items_list[jid].append(groups)
1834 self.dispatch('ROSTERX', (action, exchange_items_list, jid_from))
1837 def _messageCB(self, con, msg):
1838 '''Called when we receive a message'''
1839 log.debug('MessageCB')
1841 mtype = msg.getType()
1842 # check if the message is pubsub#event
1843 if msg.getTag('event') is not None:
1844 if mtype == 'groupchat':
1845 return
1846 if msg.getTag('error') is None:
1847 self._pubsubEventCB(con, msg)
1848 return
1850 # check if the message is a roster item exchange (XEP-0144)
1851 if msg.getTag('x', namespace=common.xmpp.NS_ROSTERX):
1852 self._rosterItemExchangeCB(con, msg)
1853 return
1855 # check if the message is a XEP-0070 confirmation request
1856 if msg.getTag('confirm', namespace=common.xmpp.NS_HTTP_AUTH):
1857 self._HttpAuthCB(con, msg)
1858 return
1860 try:
1861 frm = helpers.get_full_jid_from_iq(msg)
1862 jid = helpers.get_jid_from_iq(msg)
1863 except helpers.InvalidFormat:
1864 self.dispatch('ERROR', (_('Invalid Jabber ID'),
1865 _('A message from a non-valid JID arrived, it has been ignored.')))
1867 addressTag = msg.getTag('addresses', namespace = common.xmpp.NS_ADDRESS)
1869 # Be sure it comes from one of our resource, else ignore address element
1870 if addressTag and jid == gajim.get_jid_from_account(self.name):
1871 address = addressTag.getTag('address', attrs={'type': 'ofrom'})
1872 if address:
1873 try:
1874 frm = helpers.parse_jid(address.getAttr('jid'))
1875 except common.helpers.InvalidFormat:
1876 log.warn('Invalid JID: %s, ignoring it' % address.getAttr('jid'))
1877 return
1878 jid = gajim.get_jid_without_resource(frm)
1880 # invitations
1881 invite = None
1882 encTag = msg.getTag('x', namespace=common.xmpp.NS_ENCRYPTED)
1884 if not encTag:
1885 invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER)
1886 if invite and not invite.getTag('invite'):
1887 invite = None
1889 # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) do NOT RECOMMENDED
1890 # invitation
1891 # stanza (MUC XEP) remove in 2007, as we do not do NOT RECOMMENDED
1892 xtags = msg.getTags('x')
1893 for xtag in xtags:
1894 if xtag.getNamespace() == common.xmpp.NS_CONFERENCE and not invite:
1895 try:
1896 room_jid = helpers.parse_jid(xtag.getAttr('jid'))
1897 except common.helpers.InvalidFormat:
1898 log.warn('Invalid JID: %s, ignoring it' % xtag.getAttr('jid'))
1899 continue
1900 is_continued = False
1901 if xtag.getTag('continue'):
1902 is_continued = True
1903 self.dispatch('GC_INVITATION', (room_jid, frm, '', None,
1904 is_continued))
1905 return
1907 thread_id = msg.getThread()
1909 if not mtype:
1910 mtype = 'normal'
1912 msgtxt = msg.getBody()
1914 encrypted = False
1915 xep_200_encrypted = msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO)
1917 session = None
1918 if mtype != 'groupchat':
1919 session = self.get_or_create_session(frm, thread_id)
1921 if thread_id and not session.received_thread_id:
1922 session.received_thread_id = True
1924 session.last_receive = time_time()
1926 # check if the message is a XEP-0020 feature negotiation request
1927 if msg.getTag('feature', namespace=common.xmpp.NS_FEATURE):
1928 if gajim.HAVE_PYCRYPTO:
1929 feature = msg.getTag(name='feature', namespace=common.xmpp.NS_FEATURE)
1930 form = common.xmpp.DataForm(node=feature.getTag('x'))
1932 if form['FORM_TYPE'] == 'urn:xmpp:ssn':
1933 session.handle_negotiation(form)
1934 else:
1935 reply = msg.buildReply()
1936 reply.setType('error')
1938 reply.addChild(feature)
1939 err = common.xmpp.ErrorNode('service-unavailable', typ='cancel')
1940 reply.addChild(node=err)
1942 con.send(reply)
1944 raise common.xmpp.NodeProcessed
1946 return
1948 if msg.getTag('init', namespace=common.xmpp.NS_ESESSION_INIT):
1949 init = msg.getTag(name='init', namespace=common.xmpp.NS_ESESSION_INIT)
1950 form = common.xmpp.DataForm(node=init.getTag('x'))
1952 session.handle_negotiation(form)
1954 raise common.xmpp.NodeProcessed
1956 tim = msg.getTimestamp()
1957 tim = helpers.datetime_tuple(tim)
1958 tim = localtime(timegm(tim))
1960 if xep_200_encrypted:
1961 encrypted = 'xep200'
1963 try:
1964 msg = session.decrypt_stanza(msg)
1965 msgtxt = msg.getBody()
1966 except Exception:
1967 self.dispatch('FAILED_DECRYPT', (frm, tim, session))
1969 # Receipt requested
1970 # TODO: We shouldn't answer if we're invisible!
1971 contact = gajim.contacts.get_contact(self.name, jid)
1972 nick = gajim.get_room_and_nick_from_fjid(frm)[1]
1973 gc_contact = gajim.contacts.get_gc_contact(self.name, jid, nick)
1974 if msg.getTag('request', namespace=common.xmpp.NS_RECEIPTS) \
1975 and gajim.config.get_per('accounts', self.name,
1976 'answer_receipts') and ((contact and contact.sub \
1977 not in (u'to', u'none')) or gc_contact):
1978 receipt = common.xmpp.Message(to=frm, typ='chat')
1979 receipt.setID(msg.getID())
1980 receipt.setTag('received',
1981 namespace='urn:xmpp:receipts')
1983 if thread_id:
1984 receipt.setThread(thread_id)
1985 con.send(receipt)
1987 # We got our message's receipt
1988 if msg.getTag('received', namespace=common.xmpp.NS_RECEIPTS) \
1989 and session.control and gajim.config.get_per('accounts',
1990 self.name, 'request_receipt'):
1991 session.control.conv_textview.hide_xep0184_warning(
1992 msg.getID())
1994 if encTag and self.USE_GPG:
1995 encmsg = encTag.getData()
1997 keyID = gajim.config.get_per('accounts', self.name, 'keyid')
1998 if keyID:
1999 def decrypt_thread(encmsg, keyID):
2000 decmsg = self.gpg.decrypt(encmsg, keyID)
2001 # \x00 chars are not allowed in C (so in GTK)
2002 msgtxt = helpers.decode_string(decmsg.replace('\x00', ''))
2003 encrypted = 'xep27'
2004 return (msgtxt, encrypted)
2005 gajim.thread_interface(decrypt_thread, [encmsg, keyID],
2006 self._on_message_decrypted, [mtype, msg, session, frm, jid,
2007 invite, tim])
2008 return
2009 self._on_message_decrypted((msgtxt, encrypted), mtype, msg, session, frm,
2010 jid, invite, tim)
2012 def _on_message_decrypted(self, output, mtype, msg, session, frm, jid,
2013 invite, tim):
2014 msgtxt, encrypted = output
2015 if mtype == 'error':
2016 self.dispatch_error_message(msg, msgtxt, session, frm, tim)
2017 elif mtype == 'groupchat':
2018 self.dispatch_gc_message(msg, frm, msgtxt, jid, tim)
2019 elif invite is not None:
2020 self.dispatch_invite_message(invite, frm)
2021 else:
2022 if isinstance(session, gajim.default_session_type):
2023 session.received(frm, msgtxt, tim, encrypted, msg)
2024 else:
2025 session.received(msg)
2026 # END messageCB
2028 # process and dispatch an error message
2029 def dispatch_error_message(self, msg, msgtxt, session, frm, tim):
2030 error_msg = msg.getErrorMsg()
2032 if not error_msg:
2033 error_msg = msgtxt
2034 msgtxt = None
2036 subject = msg.getSubject()
2038 if session.is_loggable():
2039 try:
2040 gajim.logger.write('error', frm, error_msg, tim=tim,
2041 subject=subject)
2042 except exceptions.PysqliteOperationalError, e:
2043 self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
2044 except exceptions.DatabaseMalformed:
2045 pritext = _('Database Error')
2046 sectext = _('The database file (%s) cannot be read. Try to repair '
2047 'it (see http://trac.gajim.org/wiki/DatabaseBackup) or remove '
2048 'it (all history will be lost).') % common.logger.LOG_DB_PATH
2049 self.dispatch('ERROR', (pritext, sectext))
2050 self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt,
2051 tim, session))
2053 # process and dispatch a groupchat message
2054 def dispatch_gc_message(self, msg, frm, msgtxt, jid, tim):
2055 has_timestamp = bool(msg.timestamp)
2057 subject = msg.getSubject()
2059 if subject is not None:
2060 self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp))
2061 return
2063 statusCode = msg.getStatusCode()
2065 if not msg.getTag('body'): # no <body>
2066 # It could be a config change. See
2067 # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
2068 if msg.getTag('x'):
2069 if statusCode != []:
2070 self.dispatch('GC_CONFIG_CHANGE', (jid, statusCode))
2071 return
2073 # Ignore message from room in which we are not
2074 if jid not in self.last_history_time:
2075 return
2077 self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msg.getXHTML(),
2078 statusCode))
2080 tim_int = int(float(mktime(tim)))
2081 if gajim.config.should_log(self.name, jid) and not \
2082 tim_int <= self.last_history_time[jid] and msgtxt and frm.find('/') >= 0:
2083 # if frm.find('/') < 0, it means message comes from room itself
2084 # usually it hold description and can be send at each connection
2085 # so don't store it in logs
2086 try:
2087 gajim.logger.write('gc_msg', frm, msgtxt, tim=tim)
2088 except exceptions.PysqliteOperationalError, e:
2089 self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
2090 except exceptions.DatabaseMalformed:
2091 pritext = _('Database Error')
2092 sectext = _('The database file (%s) cannot be read. Try to repair '
2093 'it (see http://trac.gajim.org/wiki/DatabaseBackup) or remove '
2094 'it (all history will be lost).') % common.logger.LOG_DB_PATH
2095 self.dispatch('ERROR', (pritext, sectext))
2097 def dispatch_invite_message(self, invite, frm):
2098 item = invite.getTag('invite')
2099 try:
2100 jid_from = helpers.parse_jid(item.getAttr('from'))
2101 except common.helpers.InvalidFormat:
2102 log.warn('Invalid JID: %s, ignoring it' % item.getAttr('from'))
2103 return
2104 reason = item.getTagData('reason')
2105 item = invite.getTag('password')
2106 password = invite.getTagData('password')
2108 is_continued = False
2109 if invite.getTag('invite').getTag('continue'):
2110 is_continued = True
2111 self.dispatch('GC_INVITATION',(frm, jid_from, reason, password,
2112 is_continued))
2114 def _pubsubEventCB(self, con, msg):
2115 ''' Called when we receive <message/> with pubsub event. '''
2116 # TODO: Logging? (actually services where logging would be useful, should
2117 # TODO: allow to access archives remotely...)
2118 jid = helpers.get_full_jid_from_iq(msg)
2119 event = msg.getTag('event')
2121 # XEP-0107: User Mood
2122 items = event.getTag('items', {'node': common.xmpp.NS_MOOD})
2123 if items: pep.user_mood(items, self.name, jid)
2124 # XEP-0118: User Tune
2125 items = event.getTag('items', {'node': common.xmpp.NS_TUNE})
2126 if items: pep.user_tune(items, self.name, jid)
2127 # XEP-0080: User Geolocation
2128 items = event.getTag('items', {'node': common.xmpp.NS_GEOLOC})
2129 if items: pep.user_geoloc(items, self.name, jid)
2130 # XEP-0108: User Activity
2131 items = event.getTag('items', {'node': common.xmpp.NS_ACTIVITY})
2132 if items: pep.user_activity(items, self.name, jid)
2133 # XEP-0172: User Nickname
2134 items = event.getTag('items', {'node': common.xmpp.NS_NICK})
2135 if items: pep.user_nickname(items, self.name, jid)
2137 items = event.getTag('items')
2138 if items is None: return
2140 for item in items.getTags('item'):
2141 entry = item.getTag('entry')
2142 if entry is not None:
2143 # for each entry in feed (there shouldn't be more than one,
2144 # but to be sure...
2145 self.dispatch('ATOM_ENTRY', (atom.OldEntry(node=entry),))
2146 continue
2147 # unknown type... probably user has another client who understands that event
2148 raise common.xmpp.NodeProcessed
2150 def _presenceCB(self, con, prs):
2151 '''Called when we receive a presence'''
2152 ptype = prs.getType()
2153 if ptype == 'available':
2154 ptype = None
2155 rfc_types = ('unavailable', 'error', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed')
2156 if ptype and not ptype in rfc_types:
2157 ptype = None
2158 log.debug('PresenceCB: %s' % ptype)
2159 if not self.connection or self.connected < 2:
2160 log.debug('account is no more connected')
2161 return
2162 try:
2163 who = helpers.get_full_jid_from_iq(prs)
2164 except Exception:
2165 if prs.getTag('error') and prs.getTag('error').getTag('jid-malformed'):
2166 # wrong jid, we probably tried to change our nick in a room to a non
2167 # valid one
2168 who = str(prs.getFrom())
2169 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
2170 self.dispatch('GC_MSG', (jid_stripped,
2171 _('Nickname not allowed: %s') % resource, None, False, None, []))
2172 return
2173 jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
2174 timestamp = None
2175 is_gc = False # is it a GC presence ?
2176 sigTag = None
2177 ns_muc_user_x = None
2178 avatar_sha = None
2179 # XEP-0172 User Nickname
2180 user_nick = prs.getTagData('nick')
2181 if not user_nick:
2182 user_nick = ''
2183 contact_nickname = None
2184 transport_auto_auth = False
2185 # XEP-0203
2186 delay_tag = prs.getTag('delay', namespace=common.xmpp.NS_DELAY2)
2187 if delay_tag:
2188 tim = prs.getTimestamp2()
2189 tim = helpers.datetime_tuple(tim)
2190 timestamp = localtime(timegm(tim))
2191 xtags = prs.getTags('x')
2192 for x in xtags:
2193 namespace = x.getNamespace()
2194 if namespace.startswith(common.xmpp.NS_MUC):
2195 is_gc = True
2196 if namespace == common.xmpp.NS_MUC_USER and x.getTag('destroy'):
2197 ns_muc_user_x = x
2198 elif namespace == common.xmpp.NS_SIGNED:
2199 sigTag = x
2200 elif namespace == common.xmpp.NS_VCARD_UPDATE:
2201 avatar_sha = x.getTagData('photo')
2202 contact_nickname = x.getTagData('nickname')
2203 elif namespace == common.xmpp.NS_DELAY and not timestamp:
2204 # XEP-0091
2205 tim = prs.getTimestamp()
2206 tim = helpers.datetime_tuple(tim)
2207 timestamp = localtime(timegm(tim))
2208 elif namespace == 'http://delx.cjb.net/protocol/roster-subsync':
2209 # see http://trac.gajim.org/ticket/326
2210 agent = gajim.get_server_from_jid(jid_stripped)
2211 if self.connection.getRoster().getItem(agent): # to be sure it's a transport contact
2212 transport_auto_auth = True
2214 status = prs.getStatus() or ''
2215 show = prs.getShow()
2216 if not show in gajim.SHOW_LIST:
2217 show = '' # We ignore unknown show
2218 if not ptype and not show:
2219 show = 'online'
2220 elif ptype == 'unavailable':
2221 show = 'offline'
2223 prio = prs.getPriority()
2224 try:
2225 prio = int(prio)
2226 except Exception:
2227 prio = 0
2228 keyID = ''
2229 if sigTag and self.USE_GPG and ptype != 'error':
2230 # error presences contain our own signature
2231 # verify
2232 sigmsg = sigTag.getData()
2233 keyID = self.gpg.verify(status, sigmsg)
2235 if is_gc:
2236 if ptype == 'error':
2237 errmsg = prs.getError()
2238 errcode = prs.getErrorCode()
2239 room_jid, nick = gajim.get_room_and_nick_from_fjid(who)
2240 if errcode == '502': # Internal Timeout:
2241 self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
2242 prio, keyID, timestamp, None))
2243 elif errcode == '401': # password required to join
2244 self.dispatch('GC_PASSWORD_REQUIRED', (room_jid, nick))
2245 elif errcode == '403': # we are banned
2246 self.dispatch('ERROR', (_('Unable to join group chat'),
2247 _('You are banned from group chat %s.') % room_jid))
2248 elif errcode == '404': # group chat does not exist
2249 self.dispatch('ERROR', (_('Unable to join group chat'),
2250 _('Group chat %s does not exist.') % room_jid))
2251 elif errcode == '405':
2252 self.dispatch('ERROR', (_('Unable to join group chat'),
2253 _('Group chat creation is restricted.')))
2254 elif errcode == '406':
2255 self.dispatch('ERROR', (_('Unable to join group chat'),
2256 _('Your registered nickname must be used in group chat %s.') \
2257 % room_jid))
2258 elif errcode == '407':
2259 self.dispatch('ERROR', (_('Unable to join group chat'),
2260 _('You are not in the members list in groupchat %s.') % \
2261 room_jid))
2262 elif errcode == '409': # nick conflict
2263 room_jid = gajim.get_room_from_fjid(who)
2264 self.dispatch('ASK_NEW_NICK', (room_jid,))
2265 else: # print in the window the error
2266 self.dispatch('ERROR_ANSWER', ('', jid_stripped,
2267 errmsg, errcode))
2268 if not ptype or ptype == 'unavailable':
2269 if gajim.config.get('log_contact_status_changes') and \
2270 gajim.config.should_log(self.name, jid_stripped):
2271 gc_c = gajim.contacts.get_gc_contact(self.name, jid_stripped,
2272 resource)
2273 st = status or ''
2274 if gc_c:
2275 jid = gc_c.jid
2276 else:
2277 jid = prs.getJid()
2278 if jid:
2279 # we know real jid, save it in db
2280 st += ' (%s)' % jid
2281 try:
2282 gajim.logger.write('gcstatus', who, st, show)
2283 except exceptions.PysqliteOperationalError, e:
2284 self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
2285 except exceptions.DatabaseMalformed:
2286 pritext = _('Database Error')
2287 sectext = _('The database file (%s) cannot be read. Try to '
2288 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup)'
2289 ' or remove it (all history will be lost).') % \
2290 common.logger.LOG_DB_PATH
2291 self.dispatch('ERROR', (pritext, sectext))
2292 if avatar_sha or avatar_sha == '':
2293 if avatar_sha == '':
2294 # contact has no avatar
2295 puny_nick = helpers.sanitize_filename(resource)
2296 gajim.interface.remove_avatar_files(jid_stripped, puny_nick)
2297 # if it's a gc presence, don't ask vcard here. We may ask it to
2298 # real jid in gui part.
2299 if ns_muc_user_x:
2300 # Room has been destroyed. see
2301 # http://www.xmpp.org/extensions/xep-0045.html#destroyroom
2302 reason = _('Room has been destroyed')
2303 destroy = ns_muc_user_x.getTag('destroy')
2304 r = destroy.getTagData('reason')
2305 if r:
2306 reason += ' (%s)' % r
2307 try:
2308 jid = helpers.parse_jid(destroy.getAttr('jid'))
2309 except common.helpers.InvalidFormat:
2310 pass
2311 if jid:
2312 reason += '\n' + _('You can join this room instead: %s') % jid
2313 statusCode = ['destroyed']
2314 else:
2315 reason = prs.getReason()
2316 statusCode = prs.getStatusCode()
2317 self.dispatch('GC_NOTIFY', (jid_stripped, show, status, resource,
2318 prs.getRole(), prs.getAffiliation(), prs.getJid(),
2319 reason, prs.getActor(), statusCode, prs.getNewNick(),
2320 avatar_sha))
2321 return
2323 if ptype == 'subscribe':
2324 log.debug('subscribe request from %s' % who)
2325 if gajim.config.get('alwaysauth') or who.find("@") <= 0 or \
2326 jid_stripped in self.jids_for_auto_auth or transport_auto_auth:
2327 if self.connection:
2328 p = common.xmpp.Presence(who, 'subscribed')
2329 p = self.add_sha(p)
2330 self.connection.send(p)
2331 if who.find("@") <= 0 or transport_auto_auth:
2332 self.dispatch('NOTIFY', (jid_stripped, 'offline', 'offline',
2333 resource, prio, keyID, timestamp, None))
2334 if transport_auto_auth:
2335 self.automatically_added.append(jid_stripped)
2336 self.request_subscription(jid_stripped, name = user_nick)
2337 else:
2338 if not status:
2339 status = _('I would like to add you to my roster.')
2340 self.dispatch('SUBSCRIBE', (jid_stripped, status, user_nick))
2341 elif ptype == 'subscribed':
2342 if jid_stripped in self.automatically_added:
2343 self.automatically_added.remove(jid_stripped)
2344 else:
2345 # detect a subscription loop
2346 if jid_stripped not in self.subscribed_events:
2347 self.subscribed_events[jid_stripped] = []
2348 self.subscribed_events[jid_stripped].append(time_time())
2349 block = False
2350 if len(self.subscribed_events[jid_stripped]) > 5:
2351 if time_time() - self.subscribed_events[jid_stripped][0] < 5:
2352 block = True
2353 self.subscribed_events[jid_stripped] = self.subscribed_events[jid_stripped][1:]
2354 if block:
2355 gajim.config.set_per('account', self.name,
2356 'dont_ack_subscription', True)
2357 else:
2358 self.dispatch('SUBSCRIBED', (jid_stripped, resource))
2359 # BE CAREFUL: no con.updateRosterItem() in a callback
2360 log.debug(_('we are now subscribed to %s') % who)
2361 elif ptype == 'unsubscribe':
2362 log.debug(_('unsubscribe request from %s') % who)
2363 elif ptype == 'unsubscribed':
2364 log.debug(_('we are now unsubscribed from %s') % who)
2365 # detect a unsubscription loop
2366 if jid_stripped not in self.subscribed_events:
2367 self.subscribed_events[jid_stripped] = []
2368 self.subscribed_events[jid_stripped].append(time_time())
2369 block = False
2370 if len(self.subscribed_events[jid_stripped]) > 5:
2371 if time_time() - self.subscribed_events[jid_stripped][0] < 5:
2372 block = True
2373 self.subscribed_events[jid_stripped] = self.subscribed_events[jid_stripped][1:]
2374 if block:
2375 gajim.config.set_per('account', self.name, 'dont_ack_subscription',
2376 True)
2377 else:
2378 self.dispatch('UNSUBSCRIBED', jid_stripped)
2379 elif ptype == 'error':
2380 errmsg = prs.getError()
2381 errcode = prs.getErrorCode()
2382 if errcode != '502': # Internal Timeout:
2383 # print in the window the error
2384 self.dispatch('ERROR_ANSWER', ('', jid_stripped,
2385 errmsg, errcode))
2386 self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource, prio,
2387 keyID, timestamp, None))
2389 if ptype == 'unavailable' and jid_stripped in self.sessions:
2390 # automatically terminate sessions that they haven't sent a thread ID
2391 # in, only if other part support thread ID
2392 for sess in self.sessions[jid_stripped].values():
2393 if not sess.received_thread_id:
2394 contact = gajim.contacts.get_contact(self.name, jid_stripped)
2396 session_supported = gajim.capscache.is_supported(contact,
2397 common.xmpp.NS_SSN) or gajim.capscache.is_supported(contact,
2398 common.xmpp.NS_ESESSION)
2399 if session_supported:
2400 sess.terminate()
2401 del self.sessions[jid_stripped][sess.thread_id]
2403 if avatar_sha is not None and ptype != 'error':
2404 if jid_stripped not in self.vcard_shas:
2405 cached_vcard = self.get_cached_vcard(jid_stripped)
2406 if cached_vcard and 'PHOTO' in cached_vcard and \
2407 'SHA' in cached_vcard['PHOTO']:
2408 self.vcard_shas[jid_stripped] = cached_vcard['PHOTO']['SHA']
2409 else:
2410 self.vcard_shas[jid_stripped] = ''
2411 if avatar_sha != self.vcard_shas[jid_stripped]:
2412 # avatar has been updated
2413 self.request_vcard(jid_stripped)
2414 if not ptype or ptype == 'unavailable':
2415 if gajim.config.get('log_contact_status_changes') and \
2416 gajim.config.should_log(self.name, jid_stripped):
2417 try:
2418 gajim.logger.write('status', jid_stripped, status, show)
2419 except exceptions.PysqliteOperationalError, e:
2420 self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
2421 except exceptions.DatabaseMalformed:
2422 pritext = _('Database Error')
2423 sectext = _('The database file (%s) cannot be read. Try to '
2424 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup) '
2425 'or remove it (all history will be lost).') % \
2426 common.logger.LOG_DB_PATH
2427 self.dispatch('ERROR', (pritext, sectext))
2428 our_jid = gajim.get_jid_from_account(self.name)
2429 if jid_stripped == our_jid and resource == self.server_resource:
2430 # We got our own presence
2431 self.dispatch('STATUS', show)
2432 else:
2433 self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio,
2434 keyID, timestamp, contact_nickname))
2435 # END presenceCB
2437 def _StanzaArrivedCB(self, con, obj):
2438 self.last_io = gajim.idlequeue.current_time()
2440 def _MucOwnerCB(self, con, iq_obj):
2441 log.debug('MucOwnerCB')
2442 qp = iq_obj.getQueryPayload()
2443 node = None
2444 for q in qp:
2445 if q.getNamespace() == common.xmpp.NS_DATA:
2446 node = q
2447 if not node:
2448 return
2449 self.dispatch('GC_CONFIG', (helpers.get_full_jid_from_iq(iq_obj), node))
2451 def _MucAdminCB(self, con, iq_obj):
2452 log.debug('MucAdminCB')
2453 items = iq_obj.getTag('query', namespace = common.xmpp.NS_MUC_ADMIN).getTags('item')
2454 users_dict = {}
2455 for item in items:
2456 if item.has_attr('jid') and item.has_attr('affiliation'):
2457 try:
2458 jid = helpers.parse_jid(item.getAttr('jid'))
2459 except common.helpers.InvalidFormat:
2460 log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid'))
2461 return
2462 affiliation = item.getAttr('affiliation')
2463 users_dict[jid] = {'affiliation': affiliation}
2464 if item.has_attr('nick'):
2465 users_dict[jid]['nick'] = item.getAttr('nick')
2466 if item.has_attr('role'):
2467 users_dict[jid]['role'] = item.getAttr('role')
2468 reason = item.getTagData('reason')
2469 if reason:
2470 users_dict[jid]['reason'] = reason
2472 self.dispatch('GC_AFFILIATION', (helpers.get_full_jid_from_iq(iq_obj),
2473 users_dict))
2475 def _MucErrorCB(self, con, iq_obj):
2476 log.debug('MucErrorCB')
2477 jid = helpers.get_full_jid_from_iq(iq_obj)
2478 errmsg = iq_obj.getError()
2479 errcode = iq_obj.getErrorCode()
2480 self.dispatch('MSGERROR', (jid, errcode, errmsg))
2482 def _IqPingCB(self, con, iq_obj):
2483 log.debug('IqPingCB')
2484 if not self.connection or self.connected < 2:
2485 return
2486 iq_obj = iq_obj.buildReply('result')
2487 self.connection.send(iq_obj)
2488 raise common.xmpp.NodeProcessed
2490 def _PrivacySetCB(self, con, iq_obj):
2491 '''
2492 Privacy lists (XEP 016)
2494 A list has been set
2495 '''
2496 log.debug('PrivacySetCB')
2497 if not self.connection or self.connected < 2:
2498 return
2499 result = iq_obj.buildReply('result')
2500 q = result.getTag('query')
2501 if q:
2502 result.delChild(q)
2503 self.connection.send(result)
2504 raise common.xmpp.NodeProcessed
2506 def _getRoster(self):
2507 log.debug('getRosterCB')
2508 if not self.connection:
2509 return
2510 self.connection.getRoster(self._on_roster_set)
2511 self.discoverItems(gajim.config.get_per('accounts', self.name,
2512 'hostname'), id_prefix='Gajim_')
2513 self.discoverInfo(gajim.config.get_per('accounts', self.name,
2514 'hostname'), id_prefix='Gajim_')
2515 if gajim.config.get_per('accounts', self.name, 'use_ft_proxies'):
2516 self.discover_ft_proxies()
2518 def discover_ft_proxies(self):
2519 cfg_proxies = gajim.config.get_per('accounts', self.name,
2520 'file_transfer_proxies')
2521 our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name) + '/' +\
2522 self.server_resource)
2523 if cfg_proxies:
2524 proxies = [e.strip() for e in cfg_proxies.split(',')]
2525 for proxy in proxies:
2526 gajim.proxy65_manager.resolve(proxy, self.connection, our_jid)
2528 def _on_roster_set(self, roster):
2529 roster_version = roster.version
2530 received_from_server = roster.received_from_server
2531 raw_roster = roster.getRaw()
2532 roster = {}
2533 our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name))
2534 if self.connected > 1 and self.continue_connect_info:
2535 msg = self.continue_connect_info[1]
2536 sign_msg = self.continue_connect_info[2]
2537 signed = ''
2538 send_first_presence = True
2539 if sign_msg:
2540 signed = self.get_signed_presence(msg, self._send_first_presence)
2541 if signed is None:
2542 self.dispatch('GPG_PASSWORD_REQUIRED',
2543 (self._send_first_presence,))
2544 # _send_first_presence will be called when user enter passphrase
2545 send_first_presence = False
2546 if send_first_presence:
2547 self._send_first_presence(signed)
2549 for jid in raw_roster:
2550 try:
2551 j = helpers.parse_jid(jid)
2552 except Exception:
2553 print >> sys.stderr, _('JID %s is not RFC compliant. It will not be added to your roster. Use roster management tools such as http://jru.jabberstudio.org/ to remove it') % jid
2554 else:
2555 infos = raw_roster[jid]
2556 if jid != our_jid and (not infos['subscription'] or \
2557 infos['subscription'] == 'none') and (not infos['ask'] or \
2558 infos['ask'] == 'none') and not infos['name'] and \
2559 not infos['groups']:
2560 # remove this useless item, it won't be shown in roster anyway
2561 self.connection.getRoster().delItem(jid)
2562 elif jid != our_jid: # don't add our jid
2563 roster[j] = raw_roster[jid]
2564 if gajim.jid_is_transport(jid) and \
2565 not gajim.get_transport_name_from_jid(jid):
2566 # we can't determine which iconset to use
2567 self.discoverInfo(jid)
2569 gajim.logger.replace_roster(self.name, roster_version, roster)
2570 if received_from_server:
2571 for contact in gajim.contacts.iter_contacts(self.name):
2572 if not contact.is_groupchat() and contact.jid not in roster:
2573 self.dispatch('ROSTER_INFO', (self.name,
2574 (contact.jid, None, None, None, ())))
2575 for jid in roster:
2576 self.dispatch('ROSTER_INFO', (jid, roster[jid]['name'],
2577 roster[jid]['subscription'], roster[jid]['ask'],
2578 roster[jid]['groups']))
2580 def _send_first_presence(self, signed = ''):
2581 show = self.continue_connect_info[0]
2582 msg = self.continue_connect_info[1]
2583 sign_msg = self.continue_connect_info[2]
2584 if sign_msg and not signed:
2585 signed = self.get_signed_presence(msg)
2586 if signed is None:
2587 self.dispatch('ERROR', (_('OpenPGP passphrase was not given'),
2588 #%s is the account name here
2589 _('You will be connected to %s without OpenPGP.') % self.name))
2590 self.USE_GPG = False
2591 signed = ''
2592 self.connected = gajim.SHOW_LIST.index(show)
2593 sshow = helpers.get_xmpp_show(show)
2594 # send our presence
2595 if show == 'invisible':
2596 self.send_invisible_presence(msg, signed, True)
2597 return
2598 priority = gajim.get_priority(self.name, sshow)
2599 our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name))
2600 vcard = self.get_cached_vcard(our_jid)
2601 if vcard and 'PHOTO' in vcard and 'SHA' in vcard['PHOTO']:
2602 self.vcard_sha = vcard['PHOTO']['SHA']
2603 p = common.xmpp.Presence(typ = None, priority = priority, show = sshow)
2604 p = self.add_sha(p)
2605 if msg:
2606 p.setStatus(msg)
2607 if signed:
2608 p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
2610 if self.connection:
2611 self.connection.send(p)
2612 self.priority = priority
2613 self.dispatch('STATUS', show)
2614 # ask our VCard
2615 self.request_vcard(None)
2617 # Get bookmarks from private namespace
2618 self.get_bookmarks()
2620 # Get annotations from private namespace
2621 self.get_annotations()
2623 # Inform GUI we just signed in
2624 self.dispatch('SIGNED_IN', ())
2625 self.continue_connect_info = None
2627 def request_gmail_notifications(self):
2628 if not self.connection or self.connected < 2:
2629 return
2630 # It's a gmail account,
2631 # inform the server that we want e-mail notifications
2632 our_jid = helpers.parse_jid(gajim.get_jid_from_account(self.name))
2633 log.debug(('%s is a gmail account. Setting option '
2634 'to get e-mail notifications on the server.') % (our_jid))
2635 iq = common.xmpp.Iq(typ = 'set', to = our_jid)
2636 iq.setAttr('id', 'MailNotify')
2637 query = iq.setTag('usersetting')
2638 query.setNamespace(common.xmpp.NS_GTALKSETTING)
2639 query = query.setTag('mailnotifications')
2640 query.setAttr('value', 'true')
2641 self.connection.send(iq)
2642 # Ask how many messages there are now
2643 iq = common.xmpp.Iq(typ = 'get')
2644 iq.setID(self.connection.getAnID())
2645 query = iq.setTag('query')
2646 query.setNamespace(common.xmpp.NS_GMAILNOTIFY)
2647 self.connection.send(iq)
2650 def _search_fields_received(self, con, iq_obj):
2651 jid = jid = helpers.get_jid_from_iq(iq_obj)
2652 tag = iq_obj.getTag('query', namespace = common.xmpp.NS_SEARCH)
2653 if not tag:
2654 self.dispatch('SEARCH_FORM', (jid, None, False))
2655 return
2656 df = tag.getTag('x', namespace = common.xmpp.NS_DATA)
2657 if df:
2658 self.dispatch('SEARCH_FORM', (jid, df, True))
2659 return
2660 df = {}
2661 for i in iq_obj.getQueryPayload():
2662 df[i.getName()] = i.getData()
2663 self.dispatch('SEARCH_FORM', (jid, df, False))
2665 def _StreamCB(self, con, obj):
2666 if obj.getTag('conflict'):
2667 # disconnected because of a resource conflict
2668 self.dispatch('RESOURCE_CONFLICT', ())
2670 def _register_handlers(self, con, con_type):
2671 # try to find another way to register handlers in each class
2672 # that defines handlers
2673 con.RegisterHandler('message', self._messageCB)
2674 con.RegisterHandler('presence', self._presenceCB)
2675 con.RegisterHandler('presence', self._capsPresenceCB)
2676 con.RegisterHandler('iq', self._vCardCB, 'result',
2677 common.xmpp.NS_VCARD)
2678 con.RegisterHandler('iq', self._rosterSetCB, 'set',
2679 common.xmpp.NS_ROSTER)
2680 con.RegisterHandler('iq', self._siSetCB, 'set',
2681 common.xmpp.NS_SI)
2682 con.RegisterHandler('iq', self._rosterItemExchangeCB, 'set',
2683 common.xmpp.NS_ROSTERX)
2684 con.RegisterHandler('iq', self._siErrorCB, 'error',
2685 common.xmpp.NS_SI)
2686 con.RegisterHandler('iq', self._siResultCB, 'result',
2687 common.xmpp.NS_SI)
2688 con.RegisterHandler('iq', self._discoGetCB, 'get',
2689 common.xmpp.NS_DISCO)
2690 con.RegisterHandler('iq', self._bytestreamSetCB, 'set',
2691 common.xmpp.NS_BYTESTREAM)
2692 con.RegisterHandler('iq', self._bytestreamResultCB, 'result',
2693 common.xmpp.NS_BYTESTREAM)
2694 con.RegisterHandler('iq', self._bytestreamErrorCB, 'error',
2695 common.xmpp.NS_BYTESTREAM)
2696 con.RegisterHandler('iq', self._DiscoverItemsCB, 'result',
2697 common.xmpp.NS_DISCO_ITEMS)
2698 con.RegisterHandler('iq', self._DiscoverItemsErrorCB, 'error',
2699 common.xmpp.NS_DISCO_ITEMS)
2700 con.RegisterHandler('iq', self._DiscoverInfoCB, 'result',
2701 common.xmpp.NS_DISCO_INFO)
2702 con.RegisterHandler('iq', self._DiscoverInfoErrorCB, 'error',
2703 common.xmpp.NS_DISCO_INFO)
2704 con.RegisterHandler('iq', self._VersionCB, 'get',
2705 common.xmpp.NS_VERSION)
2706 con.RegisterHandler('iq', self._TimeCB, 'get',
2707 common.xmpp.NS_TIME)
2708 con.RegisterHandler('iq', self._TimeRevisedCB, 'get',
2709 common.xmpp.NS_TIME_REVISED)
2710 con.RegisterHandler('iq', self._LastCB, 'get',
2711 common.xmpp.NS_LAST)
2712 con.RegisterHandler('iq', self._LastResultCB, 'result',
2713 common.xmpp.NS_LAST)
2714 con.RegisterHandler('iq', self._VersionResultCB, 'result',
2715 common.xmpp.NS_VERSION)
2716 con.RegisterHandler('iq', self._TimeRevisedResultCB, 'result',
2717 common.xmpp.NS_TIME_REVISED)
2718 con.RegisterHandler('iq', self._MucOwnerCB, 'result',
2719 common.xmpp.NS_MUC_OWNER)
2720 con.RegisterHandler('iq', self._MucAdminCB, 'result',
2721 common.xmpp.NS_MUC_ADMIN)
2722 con.RegisterHandler('iq', self._PrivateCB, 'result',
2723 common.xmpp.NS_PRIVATE)
2724 con.RegisterHandler('iq', self._HttpAuthCB, 'get',
2725 common.xmpp.NS_HTTP_AUTH)
2726 con.RegisterHandler('iq', self._CommandExecuteCB, 'set',
2727 common.xmpp.NS_COMMANDS)
2728 con.RegisterHandler('iq', self._gMailNewMailCB, 'set',
2729 common.xmpp.NS_GMAILNOTIFY)
2730 con.RegisterHandler('iq', self._gMailQueryCB, 'result',
2731 common.xmpp.NS_GMAILNOTIFY)
2732 con.RegisterHandler('iq', self._DiscoverInfoGetCB, 'get',
2733 common.xmpp.NS_DISCO_INFO)
2734 con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
2735 common.xmpp.NS_DISCO_ITEMS)
2736 con.RegisterHandler('iq', self._IqPingCB, 'get',
2737 common.xmpp.NS_PING)
2738 con.RegisterHandler('iq', self._search_fields_received, 'result',
2739 common.xmpp.NS_SEARCH)
2740 con.RegisterHandler('iq', self._PrivacySetCB, 'set',
2741 common.xmpp.NS_PRIVACY)
2742 con.RegisterHandler('iq', self._PubSubCB, 'result')
2743 con.RegisterHandler('iq', self._ErrorCB, 'error')
2744 con.RegisterHandler('iq', self._IqCB)
2745 con.RegisterHandler('iq', self._StanzaArrivedCB)
2746 con.RegisterHandler('iq', self._ResultCB, 'result')
2747 con.RegisterHandler('presence', self._StanzaArrivedCB)
2748 con.RegisterHandler('message', self._StanzaArrivedCB)
2749 con.RegisterHandler('unknown', self._StreamCB, 'urn:ietf:params:xml:ns:xmpp-streams', xmlns='http://etherx.jabber.org/streams')
2751 # vim: se ts=3: