root/branches/gajim_0.9.1/src/common/connection.py

Revision 4863, 77.4 kB (checked in by nk, 3 years ago)

fix so we log muc messages [was broken only in svn]

  • Property svn:eol-style set to LF
Line 
1##      common/connection.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <nkour@jabber.org>
6##      - Dimitur Kirov <dkirov@gmail.com>
7##      - Travis Shirk <travis@pobox.com>
8##
9## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
10##                         Vincent Hanquez <tab@snarc.org>
11## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
12##                    Vincent Hanquez <tab@snarc.org>
13##                    Nikos Kouremenos <nkour@jabber.org>
14##                    Dimitur Kirov <dkirov@gmail.com>
15##                    Travis Shirk <travis@pobox.com>
16##                    Norman Rasmussen <norman@rasmussen.co.za>
17##
18## This program 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 2 only.
21##
22## This program 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
28# kind of events we can wait for an answer
29VCARD_PUBLISHED = 'vcard_published'
30VCARD_ARRIVED = 'vcard_arrived'
31
32import sys
33import sha
34import os
35import time
36import sre
37import traceback
38import threading
39import select
40import socket
41import random
42random.seed()
43import signal
44import base64
45if os.name != 'nt':
46        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
47
48from calendar import timegm
49
50import common.xmpp
51
52from common import helpers
53from common import gajim
54from common import GnuPG
55import socks5
56USE_GPG = GnuPG.USE_GPG
57
58from common import i18n
59_ = i18n._
60
61# determine which DNS resolution library is available
62HAS_DNSPYTHON = False
63HAS_PYDNS = False
64try:
65        import dns.resolver # http://dnspython.org/
66        HAS_DNSPYTHON = True
67except ImportError:
68        try:
69                import DNS # http://pydns.sf.net/
70                HAS_PYDNS = True
71        except ImportError:
72                gajim.log.debug("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.")
73
74
75STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
76        'invisible']
77
78distro_info = {
79        'Arch Linux': '/etc/arch-release',
80        'Aurox Linux': '/etc/aurox-release',
81        'Conectiva Linux': '/etc/conectiva-release',
82        'Debian GNU/Linux': '/etc/debian_release',
83        'Debian GNU/Linux': '/etc/debian_version',
84        'Fedora Linux': '/etc/fedora-release',
85        'Gentoo Linux': '/etc/gentoo-release',
86        'Linux from Scratch': '/etc/lfs-release',
87        'Mandrake Linux': '/etc/mandrake-release',
88        'Slackware Linux': '/etc/slackware-release',
89        'Slackware Linux': '/etc/slackware-version',
90        'Solaris/Sparc': '/etc/release',
91        'Source Mage': '/etc/sourcemage_version',
92        'SUSE Linux': '/etc/SuSE-release',
93        'Sun JDS': '/etc/sun-release',
94        'PLD Linux': '/etc/pld-release',
95        'Yellow Dog Linux': '/etc/yellowdog-release',
96        # many distros use the /etc/redhat-release for compatibility
97        # so Redhat is the last
98        'Redhat Linux': '/etc/redhat-release'
99}
100
101def get_os_info():
102        if os.name == 'nt':
103                ver = os.sys.getwindowsversion()
104                ver_format = ver[3], ver[0], ver[1]
105                win_version = {
106                        (1, 4, 0): '95',
107                        (1, 4, 10): '98',
108                        (1, 4, 90): 'ME',
109                        (2, 4, 0): 'NT',
110                        (2, 5, 0): '2000',
111                        (2, 5, 1): 'XP',
112                        (2, 5, 2): '2003'
113                }
114                if win_version.has_key(ver_format):
115                        return 'Windows' + ' ' + win_version[ver_format]
116                else:
117                        return 'Windows'
118        elif os.name == 'posix':
119                executable = 'lsb_release'
120                params = ' --id --codename --release --short'
121                full_path_to_executable = helpers.is_in_path(executable, return_abs_path = True)
122                if full_path_to_executable:
123                        command = executable + params
124                        child_stdin, child_stdout = os.popen2(command)
125                        output = helpers.temp_failure_retry(child_stdout.readline).strip()
126                        child_stdout.close()
127                        child_stdin.close()
128                        # some distros put n/a in places so remove them
129                        pattern = sre.compile(r' n/a', sre.IGNORECASE)
130                        output = sre.sub(pattern, '', output)
131                        return output
132
133                # lsb_release executable not available, so parse files
134                for distro_name in distro_info:
135                        path_to_file = distro_info[distro_name]
136                        if os.path.exists(path_to_file):
137                                fd = open(path_to_file)
138                                text = fd.readline().strip() #get only first line
139                                fd.close()
140                                if path_to_file.endswith('version'):
141                                        # sourcemage_version has all the info we need
142                                        if not os.path.basename(path_to_file).startswith('sourcemage'):
143                                                text = distro_name + ' ' + text
144                                elif path_to_file.endswith('aurox-release'):
145                                        # file doesn't have version
146                                        text = distro_name
147                                elif path_to_file.endswith('lfs-release'): # file just has version
148                                        text = distro_name + ' ' + text
149                                return text
150
151                # our last chance, ask uname and strip it
152                uname_output = helpers.get_output_of_command('uname -a | cut -d" " -f1,3')
153                if uname_output is not None:
154                        return uname_output[0] # only first line
155        return 'N/A'
156
157class Connection:
158        """Connection class"""
159        def __init__(self, name):
160                self.name = name
161                self.connected = 0 # offline
162                self.connection = None # xmpppy instance
163                self.gpg = None
164                self.vcard_sha = None
165                self.vcard_shas = {} # sha of contacts
166                self.status = ''
167                self.old_show = ''
168                # holds the actual hostname to which we are connected
169                self.connected_hostname = None
170                self.time_to_reconnect = None
171                self.new_account_info = None
172                self.bookmarks = []
173                self.on_purpose = False
174                self.last_io = time.time()
175                self.to_be_sent = []
176                self.last_sent = []
177                self.files_props = {}
178                self.last_history_line = {}
179                self.password = gajim.config.get_per('accounts', name, 'password')
180                self.server_resource = gajim.config.get_per('accounts', name, 'resource')
181                self.privacy_rules_supported = False
182                # Do we continue connection when we get roster (send presence,get vcard...)
183                self.continue_connect_info = None
184                # List of IDs we are waiting answers for {id: (type_of_request, data), }
185                self.awaiting_answers = {}
186                # List of IDs that will produce a timeout is answer doesn't arrive
187                # {time_of_the_timeout: (id, message to send to gui), }
188                self.awaiting_timeouts = {}
189                if USE_GPG:
190                        self.gpg = GnuPG.GnuPG()
191                        gajim.config.set('usegpg', True)
192                else:
193                        gajim.config.set('usegpg', False)
194                self.retrycount = 0
195        # END __init__
196
197        def get_full_jid(self, iq_obj):
198                '''return the full jid (with resource) from an iq as unicode'''
199                return helpers.parse_jid(str(iq_obj.getFrom()))
200
201        def get_jid(self, iq_obj):
202                '''return the jid (without resource) from an iq as unicode'''
203                jid = self.get_full_jid(iq_obj)
204                return gajim.get_jid_without_resource(jid)
205
206        def put_event(self, ev):
207                if gajim.events_for_ui.has_key(self.name):
208                        gajim.events_for_ui[self.name].append(ev)
209
210        def dispatch(self, event, data):
211                '''always passes account name as first param'''
212                gajim.mutex_events_for_ui.lock(self.put_event, [event, data])
213                gajim.mutex_events_for_ui.unlock()
214
215        def add_sha(self, p):
216                c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE)
217                if self.vcard_sha is not None:
218                        c.setTagData('photo', self.vcard_sha)
219                return p
220
221        # this is in features.py but it is blocking
222        def _discover(self, ns, jid, node = None):
223                if not self.connection:
224                        return
225                iq = common.xmpp.Iq(typ = 'get', to = jid, queryNS = ns)
226                if node:
227                        iq.setQuerynode(node)
228                self.to_be_sent.append(iq)
229
230        def discoverItems(self, jid, node = None):
231                '''According to JEP-0030: jid is mandatory,
232                name, node, action is optional.'''
233                self._discover(common.xmpp.NS_DISCO_ITEMS, jid, node)
234
235        def discoverInfo(self, jid, node = None):
236                '''According to JEP-0030:
237                        For identity: category, type is mandatory, name is optional.
238                        For feature: var is mandatory'''
239                self._discover(common.xmpp.NS_DISCO_INFO, jid, node)
240
241        def node_to_dict(self, node):
242                dict = {}
243                for info in node.getChildren():
244                        name = info.getName()
245                        if name in ('ADR', 'TEL', 'EMAIL'): # we can have several
246                                if not dict.has_key(name):
247                                        dict[name] = []
248                                entry = {}
249                                for c in info.getChildren():
250                                         entry[c.getName()] = c.getData()
251                                dict[name].append(entry)
252                        elif info.getChildren() == []:
253                                dict[name] = info.getData()
254                        else:
255                                dict[name] = {}
256                                for c in info.getChildren():
257                                         dict[name][c.getName()] = c.getData()
258                return dict
259
260        def _vCardCB(self, con, vc):
261                """Called when we receive a vCard
262                Parse the vCard and send it to plugins"""
263                if not vc.getTag('vCard'):
264                        return
265                frm_iq = vc.getFrom()
266                our_jid = gajim.get_jid_from_account(self.name)
267                resource = ''
268                if frm_iq:
269                        who = self.get_full_jid(vc)
270                        frm, resource = gajim.get_room_and_nick_from_fjid(who)
271                else:
272                        frm = our_jid
273                if vc.getTag('vCard').getNamespace() == common.xmpp.NS_VCARD:
274                        card = vc.getChildren()[0]
275                        vcard = self.node_to_dict(card)
276                        if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \
277                        vcard['PHOTO'].has_key('BINVAL'):
278                                photo = vcard['PHOTO']['BINVAL']
279                                photo_decoded = base64.decodestring(photo)
280                                avatar_sha = sha.sha(photo_decoded).hexdigest()
281                        else:
282                                avatar_sha = ''
283
284                        if avatar_sha:
285                                card.getTag('PHOTO').setTagData('SHA', avatar_sha)
286                        if frm != our_jid:
287                                if avatar_sha:
288                                        self.vcard_shas[frm] = avatar_sha
289                                elif self.vcard_shas.has_key(frm):
290                                        del self.vcard_shas[frm]
291
292                        # Save it to file
293                        path_to_file = os.path.join(gajim.VCARDPATH, frm)
294                        fil = open(path_to_file, 'w')
295                        fil.write(str(card))
296                        fil.close()
297
298                        vcard['jid'] = frm
299                        vcard['resource'] = resource
300                        if frm == our_jid:
301                                self.dispatch('MYVCARD', vcard)
302                                # we re-send our presence with sha if has changed and if we are
303                                # not invisible
304                                if self.vcard_sha == avatar_sha:
305                                        return
306                                self.vcard_sha = avatar_sha
307                                if STATUS_LIST[self.connected] == 'invisible':
308                                        return
309                                sshow = helpers.get_xmpp_show(STATUS_LIST[self.connected])
310                                prio = unicode(gajim.config.get_per('accounts', self.name,
311                                        'priority'))
312                                p = common.xmpp.Presence(typ = None, priority = prio, show = sshow,
313                                        status = self.status)
314                                p = self.add_sha(p)
315                                self.to_be_sent.append(p)
316                        else:
317                                self.dispatch('VCARD', vcard)
318
319
320        def _messageCB(self, con, msg):
321                """Called when we receive a message"""
322                msgtxt = msg.getBody()
323                mtype = msg.getType()
324                subject = msg.getSubject() # if not there, it's None
325                tim = msg.getTimestamp()
326                tim = time.strptime(tim, '%Y%m%dT%H:%M:%S')
327                tim = time.localtime(timegm(tim))
328                frm = self.get_full_jid(msg)
329                jid = self.get_jid(msg)
330                no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
331                encrypted = False
332                chatstate = None
333                xtags = msg.getTags('x')
334                encTag = None
335                decmsg = ''
336                invite = None
337                for xtag in xtags:
338                        if xtag.getNamespace() == common.xmpp.NS_ENCRYPTED:
339                                encTag = xtag
340                                break
341                        #invitations
342                        elif xtag.getNamespace() == common.xmpp.NS_MUC_USER and \
343                                xtag.getTag('invite'):
344                                invite = xtag
345                        # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) do NOT RECOMMENDED
346                        # invitation
347                        # stanza (MUC JEP) remove in 2007, as we do not do NOT RECOMMENDED
348                        elif xtag.getNamespace() == common.xmpp.NS_CONFERENCE:
349                                room_jid = xtag.getAttr('jid')
350                                self.dispatch('GC_INVITATION', (room_jid, frm, '', None))
351                                return
352                # chatstates - look for chatstate tags in a message
353                children = msg.getChildren()
354                for child in children:
355                        if child.getNamespace() == 'http://jabber.org/protocol/chatstates':
356                                chatstate = child.getName()
357                                break
358
359                if encTag and USE_GPG:
360                        #decrypt
361                        encmsg = encTag.getData()
362
363                        keyID = gajim.config.get_per('accounts', self.name, 'keyid')
364                        if keyID:
365                                decmsg = self.gpg.decrypt(encmsg, keyID)
366                if decmsg:
367                        msgtxt = decmsg
368                        encrypted = True
369                if mtype == 'error':
370                        self.dispatch('MSGERROR', (frm, msg.getErrorCode(), msg.getError(),
371                                msgtxt, tim))
372                elif mtype == 'groupchat':
373                        if subject:
374                                self.dispatch('GC_SUBJECT', (frm, subject))
375                        else:
376                                if not msg.getTag('body'): #no <body>
377                                        return
378                                self.dispatch('GC_MSG', (frm, msgtxt, tim))
379                                if self.name not in no_log_for and not\
380                                        int(float(time.mktime(tim))) <= self.last_history_line[jid]:
381                                        gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
382                elif mtype == 'chat': # it's type 'chat'
383                        if not msg.getTag('body') and chatstate is None: #no <body>
384                                return
385                        if msg.getTag('body') and self.name not in no_log_for and jid not in\
386                                no_log_for:
387                                gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, subject = subject)
388                        self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
389                                chatstate))
390                else: # it's single message
391                        if self.name not in no_log_for and jid not in no_log_for:
392                                gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, subject = subject)
393                        if invite is not None:
394                                item = invite.getTag('invite')
395                                jid_from = item.getAttr('from')
396                                reason = item.getTagData('reason')
397                                item = invite.getTag('password')
398                                password = invite.getTagData('password')
399                                self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
400                        else:
401                                self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
402                                        subject, None))
403        # END messageCB
404
405        def _presenceCB(self, con, prs):
406                """Called when we receive a presence"""
407                ptype = prs.getType()
408                if ptype == 'available':
409                        ptype = None
410                gajim.log.debug('PresenceCB: %s' % ptype)
411                is_gc = False # is it a GC presence ?
412                sigTag = None
413                avatar_sha = None
414                xtags = prs.getTags('x')
415                for x in xtags:
416                        if x.getNamespace().startswith(common.xmpp.NS_MUC):
417                                is_gc = True
418                        if x.getNamespace() == common.xmpp.NS_SIGNED:
419                                sigTag = x
420                        if x.getNamespace() == common.xmpp.NS_VCARD_UPDATE:
421                                avatar_sha = x.getTagData('photo')
422
423                who = self.get_full_jid(prs)
424                jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
425                no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
426                status = prs.getStatus()
427                show = prs.getShow()
428                if not show in STATUS_LIST:
429                        show = '' # We ignore unknown show
430                if not ptype and not show:
431                        show = 'online'
432                elif ptype == 'unavailable':
433                        show = 'offline'
434
435                prio = prs.getPriority()
436                try:
437                        prio = int(prio)
438                except:
439                        prio = 0
440                keyID = ''
441                if sigTag and USE_GPG:
442                        #verify
443                        sigmsg = sigTag.getData()
444                        keyID = self.gpg.verify(status, sigmsg)
445
446                if is_gc:
447                        if ptype == 'error':
448                                errmsg = prs.getError()
449                                errcode = prs.getErrorCode()
450                                if errcode == '502': # Internal Timeout:
451                                        self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
452                                                prio, keyID))
453                                elif errcode == '401': # password required to join
454                                        self.dispatch('ERROR', (_('Unable to join room'),
455                                                _('A password is required to join this room.')))
456                                elif errcode == '403': # we are banned
457                                        self.dispatch('ERROR', (_('Unable to join room'),
458                                                _('You are banned from this room.')))
459                                elif errcode == '404': # room does not exist
460                                        self.dispatch('ERROR', (_('Unable to join room'),
461                                                _('Such room does not exist.')))
462                                elif errcode == '405':
463                                        self.dispatch('ERROR', (_('Unable to join room'),
464                                                _('Room creation is restricted.')))
465                                elif errcode == '406':
466                                        self.dispatch('ERROR', (_('Unable to join room'),
467                                                _('Your registered nickname must be used.')))
468                                elif errcode == '407':
469                                        self.dispatch('ERROR', (_('Unable to join room'),
470                                                _('You are not in the members list.')))
471                                elif errcode == '409': # nick conflict
472                                        # the jid_from in this case is FAKE JID: room_jid/nick
473                                        # resource holds the bad nick so propose a new one
474                                        proposed_nickname = resource + \
475                                                gajim.config.get('gc_proposed_nick_char')
476                                        room_jid = gajim.get_room_from_fjid(who)
477                                        self.dispatch('ASK_NEW_NICK', (room_jid, _('Unable to join room'),
478                _('Your desired nickname is in use or registered by another occupant.\nPlease specify another nickname below:'), proposed_nickname))
479                                else:   # print in the window the error
480                                        self.dispatch('ERROR_ANSWER', ('', jid_stripped,
481                                                errmsg, errcode))
482                        if not ptype or ptype == 'unavailable':
483                                if gajim.config.get('log_contact_status_changes') and self.name\
484                                        not in no_log_for and jid_stripped not in no_log_for:
485                                        gajim.logger.write('gcstatus', who, status, show)
486                                self.dispatch('GC_NOTIFY', (jid_stripped, show, status, resource,
487                                        prs.getRole(), prs.getAffiliation(), prs.getJid(),
488                                        prs.getReason(), prs.getActor(), prs.getStatusCode(),
489                                        prs.getNewNick()))
490                        return
491
492                if ptype == 'subscribe':
493                        gajim.log.debug('subscribe request from %s' % who)
494                        if gajim.config.get('alwaysauth') or who.find("@") <= 0:
495                                if self.connection:
496                                        p = common.xmpp.Presence(who, 'subscribed')
497                                        p = self.add_sha(p)
498                                        self.to_be_sent.append(p)
499                                if who.find("@") <= 0:
500                                        self.dispatch('NOTIFY',
501                                                (jid_stripped, 'offline', 'offline', resource, prio, keyID))
502                        else:
503                                if not status:
504                                        status = _('I would like to add you to my roster.')
505                                self.dispatch('SUBSCRIBE', (who, status))
506                elif ptype == 'subscribed':
507                        self.dispatch('SUBSCRIBED', (jid_stripped, resource))
508                        # BE CAREFUL: no con.updateRosterItem() in a callback
509                        gajim.log.debug(_('we are now subscribed to %s') % who)
510                elif ptype == 'unsubscribe':
511                        gajim.log.debug(_('unsubscribe request from %s') % who)
512                elif ptype == 'unsubscribed':
513                        gajim.log.debug(_('we are now unsubscribed from %s') % who)
514                        self.dispatch('UNSUBSCRIBED', jid_stripped)
515                elif ptype == 'error':
516                        errmsg = prs.getError()
517                        errcode = prs.getErrorCode()
518                        if errcode == '502': # Internal Timeout:
519                                self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource,
520                                        prio, keyID))
521                        else:   # print in the window the error
522                                self.dispatch('ERROR_ANSWER', ('', jid_stripped,
523                                        errmsg, errcode))
524
525                if avatar_sha:
526                        if self.vcard_shas.has_key(jid_stripped):
527                                if avatar_sha != self.vcard_shas[jid_stripped]:
528                                        # avatar has been updated
529                                        self.request_vcard(jid_stripped)
530                        else:
531                                self.vcard_shas[jid_stripped] = avatar_sha
532                if not ptype or ptype == 'unavailable':
533                        if gajim.config.get('log_contact_status_changes') and self.name\
534                                not in no_log_for and jid_stripped not in no_log_for:
535                                gajim.logger.write('status', jid_stripped, status, show)
536                        self.dispatch('NOTIFY', (jid_stripped, show, status, resource, prio,
537                                keyID))
538        # END presenceCB
539
540        def _disconnectedCB(self):
541                """Called when we are disconnected"""
542                gajim.log.debug('disconnectedCB')
543                if not self.connection:
544                        return
545                self.connected = 0
546                self.dispatch('STATUS', 'offline')
547                self.connection = None
548                if not self.on_purpose:
549                        self.dispatch('ERROR',
550                        (_('Connection with account "%s" has been lost') % self.name,
551                        _('To continue sending and receiving messages, you will need to reconnect.')))
552                self.on_purpose = False
553
554        # END disconenctedCB
555
556        def _reconnect(self):
557                # Do not try to reco while we are already trying
558                self.time_to_reconnect = None
559                t = threading.Thread(target=self._reconnect2)
560                t.start()
561
562        def _reconnect2(self):
563                gajim.log.debug('reconnect')
564                self.retrycount += 1
565                signed = self.get_signed_msg(self.status)
566                self.connect_and_init(self.old_show, self.status, signed)
567                if self.connected < 2: #connection failed
568                        if self.retrycount > 10:
569                                self.connected = 0
570                                self.dispatch('STATUS', 'offline')
571                                self.dispatch('ERROR',
572                                (_('Connection with account "%s" has been lost') % self.name,
573                                _('To continue sending and receiving messages, you will need to reconnect.')))
574                                self.retrycount = 0
575