root/trunk/src/gajim.py

Revision 10724, 118.6 kB (checked in by steve-e, 44 hours ago)

Do not duplicate a contact in roster on subscription changes. Fixes #4524.

For performance reasons, get_contact_iters() cannot find a contact if get_shown_groups() returns something different to what has been applied to roster model. The contact was therefore duplicated as it was believed not to be there...

  • Property executable set to 1
  • Property svn:eol-style set to LF
  • Property svn:executable set to *
  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
Line 
1#!/usr/bin/env python
2# -*- coding:utf-8 -*-
3## src/gajim.py
4##
5## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
6## Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
7## Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
8##                    Norman Rasmussen <norman AT rasmussen.co.za>
9##                    Stéphan Kochen <stephan AT kochen.nl>
10## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
11##                         Alex Mauer <hawke AT hawkesnest.net>
12## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
13##                         Nikos Kouremenos <kourem AT gmail.com>
14## Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
15##                    Stefan Bethge <stefan AT lanpartei.de>
16## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
17## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
18##                    James Newton <redshodan AT gmail.com>
19## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
20##                         Julien Pivotto <roidelapluie AT gmail.com>
21##                         Stephan Erb <steve-e AT h3c.de>
22## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
23##
24## This file is part of Gajim.
25##
26## Gajim is free software; you can redistribute it and/or modify
27## it under the terms of the GNU General Public License as published
28## by the Free Software Foundation; version 3 only.
29##
30## Gajim is distributed in the hope that it will be useful,
31## but WITHOUT ANY WARRANTY; without even the implied warranty of
32## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33## GNU General Public License for more details.
34##
35## You should have received a copy of the GNU General Public License
36## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
37##
38
39import os
40
41if os.name == 'nt':
42        import warnings
43        warnings.filterwarnings(action='ignore')
44
45        if os.path.isdir('gtk'):
46                # Used to create windows installer with GTK included
47                paths = os.environ['PATH']
48                list_ = paths.split(';')
49                new_list = []
50                for p in list_:
51                        if p.find('gtk') < 0 and p.find('GTK') < 0:
52                                new_list.append(p)
53                new_list.insert(0, 'gtk/lib')
54                new_list.insert(0, 'gtk/bin')
55                os.environ['PATH'] = ';'.join(new_list)
56                os.environ['GTK_BASEPATH'] = 'gtk'
57
58import sys
59
60if os.name == 'nt':
61        # needed for docutils
62        sys.path.append('.')
63
64import logging
65consoleloghandler = logging.StreamHandler()
66consoleloghandler.setLevel(1)
67consoleloghandler.setFormatter(
68logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s'))
69log = logging.getLogger('gajim')
70log.setLevel(logging.WARNING)
71log.addHandler(consoleloghandler)
72log.propagate = False
73log = logging.getLogger('gajim.gajim')
74
75# create intermediate loggers
76logging.getLogger('gajim.c')
77logging.getLogger('gajim.c.x')
78
79import getopt
80from common import i18n
81
82def parseLogLevel(arg):
83        if arg.isdigit():
84                return int(arg)
85        if arg.isupper():
86                return getattr(logging, arg)
87        raise ValueError(_('%s is not a valid loglevel'), repr(arg))
88
89def parseLogTarget(arg):
90        arg = arg.lower()
91        if arg.startswith('.'): return arg[1:]
92        if arg.startswith('gajim'): return arg
93        return 'gajim.' + arg
94
95def parseAndSetLogLevels(arg):
96        for directive in arg.split(','):
97                directive = directive.strip()
98                targets, level = directive.rsplit('=', 1)
99                level = parseLogLevel(level.strip())
100                for target in targets.split('='):
101                        target = parseLogTarget(target.strip())
102                        if target == '':
103                                consoleloghandler.setLevel(level)
104                                print "consoleloghandler level set to %s" % level
105                        else:
106                                logger = logging.getLogger(target)
107                                logger.setLevel(level)
108                                print "Logger %s level set to %d" % (target, level)
109
110def parseOpts():
111        profile = ''
112        verbose = False
113        config_path = None
114
115        try:
116                shortargs = 'hqvl:p:c:'
117                longargs = 'help quiet verbose loglevel= profile= config_path='
118                opts, args = getopt.getopt(sys.argv[1:], shortargs, longargs.split())
119        except getopt.error, msg:
120                print msg
121                print 'for help use --help'
122                sys.exit(2)
123        for o, a in opts:
124                if o in ('-h', '--help'):
125                        print 'gajim [--help] [--quiet] [--verbose] [--loglevel subsystem=level[,subsystem=level[...]]] [--profile name] [--config-path]'
126                        sys.exit()
127                elif o in ('-q', '--quiet'):
128                        consoleloghandler.setLevel(logging.CRITICAL)
129                        verbose = False
130                elif o in ('-v', '--verbose'):
131                        consoleloghandler.setLevel(logging.INFO)
132                        verbose = True
133                elif o in ('-p', '--profile'): # gajim --profile name
134                        profile = a
135                elif o in ('-l', '--loglevel'):
136                        parseAndSetLogLevels(a)
137                elif o in ('-c', '--config-path'):
138                        config_path = a
139        return profile, verbose, config_path
140
141profile, verbose, config_path = parseOpts()
142del parseOpts, parseAndSetLogLevels, parseLogTarget, parseLogLevel
143
144import locale
145profile = unicode(profile, locale.getpreferredencoding())
146
147import common.configpaths
148common.configpaths.gajimpaths.init(config_path)
149del config_path
150common.configpaths.gajimpaths.init_profile(profile)
151del profile
152
153# PyGTK2.10+ only throws a warning
154import warnings
155warnings.filterwarnings('error', module='gtk')
156try:
157        import gtk
158except Warning, msg:
159        if str(msg) == 'could not open display':
160                print >> sys.stderr, _('Gajim needs X server to run. Quiting...')
161                sys.exit()
162warnings.resetwarnings()
163
164if os.name == 'nt':
165        import warnings
166        warnings.filterwarnings(action='ignore')
167
168pritext = ''
169
170from common import exceptions
171try:
172        from common import gajim
173except exceptions.DatabaseMalformed:
174        pritext = _('Database Error')
175        sectext = _('The database file (%s) cannot be read. Try to repair it (see http://trac.gajim.org/wiki/DatabaseBackup) or remove it (all history will be lost).') % common.logger.LOG_DB_PATH
176else:
177        from common import dbus_support
178        if dbus_support.supported:
179                import dbus
180
181        if os.name == 'posix': # dl module is Unix Only
182                try: # rename the process name to gajim
183                        import dl
184                        libc = dl.open('/lib/libc.so.6')
185                        libc.call('prctl', 15, 'gajim\0', 0, 0, 0)
186                except Exception:
187                        pass
188
189        if gtk.pygtk_version < (2, 8, 0):
190                pritext = _('Gajim needs PyGTK 2.8 or above')
191                sectext = _('Gajim needs PyGTK 2.8 or above to run. Quiting...')
192        elif gtk.gtk_version < (2, 8, 0):
193                pritext = _('Gajim needs GTK 2.8 or above')
194                sectext = _('Gajim needs GTK 2.8 or above to run. Quiting...')
195
196        try:
197                import gtk.glade # check if user has libglade (in pygtk and in gtk)
198        except ImportError:
199                pritext = _('GTK+ runtime is missing libglade support')
200                if os.name == 'nt':
201                        sectext = _('Please remove your current GTK+ runtime and install the latest stable version from %s') % 'http://gladewin32.sourceforge.net'
202                else:
203                        sectext = _('Please make sure that GTK+ and PyGTK have libglade support in your system.')
204
205        try:
206                from common import check_paths
207        except exceptions.PysqliteNotAvailable, e:
208                pritext = _('Gajim needs PySQLite2 to run')
209                sectext = str(e)
210
211        if os.name == 'nt':
212                try:
213                        import winsound # windows-only built-in module for playing wav
214                        import win32api # do NOT remove. we req this module
215                except Exception:
216                        pritext = _('Gajim needs pywin32 to run')
217                        sectext = _('Please make sure that Pywin32 is installed on your system. You can get it at %s') % 'http://sourceforge.net/project/showfiles.php?group_id=78018'
218
219if pritext:
220        dlg = gtk.MessageDialog(None,
221                gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
222                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
223
224        dlg.format_secondary_text(sectext)
225        dlg.run()
226        dlg.destroy()
227        sys.exit()
228
229del pritext
230
231import gtkexcepthook
232
233import gobject
234if not hasattr(gobject, 'timeout_add_seconds'):
235        def timeout_add_seconds_fake(time_sec, *args):
236                return gobject.timeout_add(time_sec * 1000, *args)
237        gobject.timeout_add_seconds = timeout_add_seconds_fake
238
239import re
240import signal
241import time
242import math
243
244import gtkgui_helpers
245import notify
246import message_control
247
248from chat_control import ChatControlBase
249from chat_control import ChatControl
250from groupchat_control import GroupchatControl
251from groupchat_control import PrivateChatControl
252from atom_window import AtomWindow
253from session import ChatControlSession
254
255import common.sleepy
256
257from common.xmpp import idlequeue
258from common.zeroconf import connection_zeroconf
259from common import nslookup
260from common import proxy65_manager
261from common import socks5
262from common import helpers
263from common import optparser
264from common import dataforms
265
266if verbose: gajim.verbose = True
267del verbose
268
269gajimpaths = common.configpaths.gajimpaths
270
271pid_filename = gajimpaths['PID_FILE']
272config_filename = gajimpaths['CONFIG_FILE']
273
274import traceback
275import errno
276
277import dialogs
278def pid_alive():
279        try:
280                pf = open(pid_filename)
281        except IOError:
282                # probably file not found
283                return False
284
285        try:
286                pid = int(pf.read().strip())
287                pf.close()
288        except Exception:
289                traceback.print_exc()
290                # PID file exists, but something happened trying to read PID
291                # Could be 0.10 style empty PID file, so assume Gajim is running
292                return True
293
294        if os.name == 'nt':
295                try:
296                        from ctypes import (windll, c_ulong, c_int, Structure, c_char, POINTER, pointer, )
297                except Exception:
298                        return True
299
300                class PROCESSENTRY32(Structure):
301                        _fields_ = [
302                                ('dwSize', c_ulong, ),
303                                ('cntUsage', c_ulong, ),
304                                ('th32ProcessID', c_ulong, ),
305                                ('th32DefaultHeapID', c_ulong, ),
306                                ('th32ModuleID', c_ulong, ),
307                                ('cntThreads', c_ulong, ),
308                                ('th32ParentProcessID', c_ulong, ),
309                                ('pcPriClassBase', c_ulong, ),
310                                ('dwFlags', c_ulong, ),
311                                ('szExeFile', c_char*512, ),
312                                ]
313                        def __init__(self):
314                                Structure.__init__(self, 512+9*4)
315
316                k = windll.kernel32
317                k.CreateToolhelp32Snapshot.argtypes = c_ulong, c_ulong,
318                k.CreateToolhelp32Snapshot.restype = c_int
319                k.Process32First.argtypes = c_int, POINTER(PROCESSENTRY32),
320                k.Process32First.restype = c_int
321                k.Process32Next.argtypes = c_int, POINTER(PROCESSENTRY32),
322                k.Process32Next.restype = c_int
323
324                def get_p(p):
325                        h = k.CreateToolhelp32Snapshot(2, 0) # TH32CS_SNAPPROCESS
326                        assert h > 0, 'CreateToolhelp32Snapshot failed'
327                        b = pointer(PROCESSENTRY32())
328                        f = k.Process32First(h, b)
329                        while f:
330                                if b.contents.th32ProcessID == p:
331                                        return b.contents.szExeFile
332                                f = k.Process32Next(h, b)
333
334                if get_p(pid) in ('python.exe', 'gajim.exe'):
335                        return True
336                return False
337        elif sys.platform == 'darwin':
338                try:
339                        from osx import checkPID
340                        return checkPID(pid, 'Gajim.bin')
341                except ImportError:
342                        return
343        try:
344                if not os.path.exists('/proc'):
345                        return True # no /proc, assume Gajim is running
346
347                try:
348                        f = open('/proc/%d/cmdline'% pid)
349                except IOError, e:
350                        if e.errno == errno.ENOENT:
351                                return False # file/pid does not exist
352                        raise
353
354                n = f.read().lower()
355                f.close()
356                if n.find('gajim') < 0:
357                        return False
358                return True # Running Gajim found at pid
359        except Exception:
360                traceback.print_exc()
361
362        # If we are here, pidfile exists, but some unexpected error occured.
363        # Assume Gajim is running.
364        return True
365
366if pid_alive():
367        path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
368        pix = gtk.gdk.pixbuf_new_from_file(path_to_file)
369        gtk.window_set_default_icon(pix) # set the icon to all newly opened wind
370        pritext = _('Gajim is already running')
371        sectext = _('Another instance of Gajim seems to be running\nRun anyway?')
372        dialog = dialogs.YesNoDialog(pritext, sectext)
373        dialog.popup()
374        if dialog.run() != gtk.RESPONSE_YES:
375                sys.exit(3)
376        dialog.destroy()
377        # run anyway, delete pid and useless global vars
378        if os.path.exists(pid_filename):
379                os.remove(pid_filename)
380        del path_to_file
381        del pix
382        del pritext
383        del sectext
384        dialog.destroy()
385
386# Create .gajim dir
387pid_dir =  os.path.dirname(pid_filename)
388if not os.path.exists(pid_dir):
389        check_paths.create_path(pid_dir)
390# Create pid file
391try:
392        f = open(pid_filename, 'w')
393        f.write(str(os.getpid()))
394        f.close()
395except IOError, e:
396        dlg = dialogs.ErrorDialog(_('Disk Write Error'), str(e))
397        dlg.run()
398        dlg.destroy()
399        sys.exit()
400del pid_dir
401del f
402
403def on_exit():
404        # delete pid file on normal exit
405        if os.path.exists(pid_filename):
406                os.remove(pid_filename)
407        # Save config
408        gajim.interface.save_config()
409        if sys.platform == 'darwin':
410                try:
411                        import osx
412                        osx.shutdown()
413                except ImportError:
414                        pass
415
416import atexit
417atexit.register(on_exit)
418
419parser = optparser.OptionsParser(config_filename)
420
421import roster_window
422import profile_window
423import config
424
425class GlibIdleQueue(idlequeue.IdleQueue):
426        '''
427        Extends IdleQueue to use glib io_add_wath, instead of select/poll
428        In another, `non gui' implementation of Gajim IdleQueue can be used safetly.
429        '''
430        def init_idle(self):
431                ''' this method is called at the end of class constructor.
432                Creates a dict, which maps file/pipe/sock descriptor to glib event id'''
433                self.events = {}
434                # time() is already called in glib, we just get the last value
435                # overrides IdleQueue.current_time()
436                self.current_time = lambda: gobject.get_current_time()
437
438        def add_idle(self, fd, flags):
439                ''' this method is called when we plug a new idle object.
440                Start listening for events from fd
441                '''
442                res = gobject.io_add_watch(fd, flags, self._process_events,
443                        priority=gobject.PRIORITY_LOW)
444                # store the id of the watch, so that we can remove it on unplug
445                self.events[fd] = res
446
447        def _process_events(self, fd, flags):
448                try:
449                        return self.process_events(fd, flags)
450                except Exception:
451                        self.remove_idle(fd)
452                        self.add_idle(fd, flags)
453                        raise
454
455        def remove_idle(self, fd):
456                ''' this method is called when we unplug a new idle object.
457                Stop listening for events from fd
458                '''
459                if not fd in self.events:
460                        return
461                gobject.source_remove(self.events[fd])
462                del(self.events[fd])
463
464        def process(self):
465                self.check_time_events()
466
467class PassphraseRequest:
468        def __init__(self, keyid):
469                self.keyid = keyid
470                self.callbacks = []
471                self.dialog_created = False
472                self.completed = False
473
474        def run_callback(self, account, callback):
475                gajim.connections[account].gpg_passphrase(self.passphrase)
476                callback()
477
478        def add_callback(self, account, cb):
479                if self.completed:
480                        self.run_callback(account, cb)
481                else:
482                        self.callbacks.append((account, cb))
483                        if not self.dialog_created:
484                                self.create_dialog(account)
485
486        def complete(self, passphrase):
487                self.passphrase = passphrase
488                self.completed = True
489                if passphrase is not None:
490                        gobject.timeout_add_seconds(30, gajim.interface.forget_gpg_passphrase,
491                                self.keyid)
492                for (account, cb) in self.callbacks:
493                        self.run_callback(account, cb)
494                del self.callbacks
495
496        def create_dialog(self, account):
497                title = _('Passphrase Required')
498                second = _('Enter GPG key passphrase for key %(keyid)s (account '
499                        '%(account)s).') % {'keyid': self.keyid, 'account': account}
500
501                def _cancel():
502                        # user cancelled, continue without GPG
503                        self.complete(None)
504
505                def _ok(passphrase, checked, count):
506                        if gajim.connections[account].test_gpg_passphrase(passphrase):
507                                # passphrase is good
508                                self.complete(passphrase)
509                                return
510
511                        if count < 3:
512                                # ask again
513                                dialogs.PassphraseDialog(_('Wrong Passphrase'),
514                                        _('Please retype your GPG passphrase or press Cancel.'),
515                                        ok_handler=(_ok, count + 1), cancel_handler=_cancel)
516                        else:
517                                # user failed 3 times, continue without GPG
518                                self.complete(None)
519
520                dialogs.PassphraseDialog(title, second, ok_handler=(_ok, 1),
521                        cancel_handler=_cancel)
522                self.dialog_created = True
523
524class Interface:
525
526################################################################################               
527### Methods handling events from connection
528################################################################################       
529
530        def handle_event_roster(self, account, data):
531                #('ROSTER', account, array)
532                # FIXME: Those methods depend to highly on each other
533                # and the order in which they are called
534                self.roster.fill_contacts_and_groups_dicts(data, account)
535                self.roster.add_account_contacts(account)
536                self.roster.fire_up_unread_messages_events(account)
537                if self.remote_ctrl:
538                        self.remote_ctrl.raise_signal('Roster', (account, data))
539
540        def handle_event_warning(self, unused, data):
541                #('WARNING', account, (title_text, section_text))
542                dialogs.WarningDialog(data[0], data[1])
543
544        def handle_event_error(self, unused, data):
545                #('ERROR', account, (title_text, section_text))
546                dialogs.ErrorDialog(data[0], data[1])
547
548        def handle_event_information(self, unused, data):
549                #('INFORMATION', account, (title_text, section_text))
550                dialogs.InformationDialog(data[0], data[1])
551
552        def handle_event_ask_new_nick(self, account, data):
553                #('ASK_NEW_NICK', account, (room_jid,))
554                room_jid = data[0]
555                gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
556                if not gc_control and \
557                room_jid in self.minimized_controls[account]:
558                        gc_control = self.minimized_controls[account][room_jid]
559                if gc_control: # user may close the window before we are here
560                        title = _('Unable to join group chat')
561                        prompt = _('Your desired nickname in group chat %s is in use or '
562                                'registered by another occupant.\nPlease specify another nickname '
563                                'below:') % room_jid
564                        gc_control.show_change_nick_input_dialog(title, prompt)
565
566        def handle_event_http_auth(self, account, data):
567                #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
568                def response(account, iq_obj, answer):
569                        self.dialog.destroy()
570                        gajim.connections[account].build_http_auth_answer(iq_obj, answer)
571
572                def on_yes(is_checked, account, iq_obj):
573                        response(account, iq_obj, 'yes')
574
575                sec_msg = _('Do you accept this request?')
576                if gajim.get_number_of_connected_accounts() > 1:
577                        sec_msg = _('Do you accept this request on account %s?') % account
578                if data[4]:
579                        sec_msg = data[4] + '\n' + sec_msg
580                self.dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for '
581                        '%(url)s (id: %(id)s)') % {'method': data[0], 'url': data[1],
582                        'id': data[2]}, sec_msg, on_response_yes=(on_yes, account, data[3]),
583                        on_response_no=(response, account, data[3], 'no'))
584
585        def handle_event_error_answer(self, account, array):
586                #('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
587                id, jid_from, errmsg, errcode = array
588                if unicode(errcode) in ('403', '406') and id:
589                        # show the error dialog
590                        ft = self.instances['file_transfers']
591                        sid = id
592                        if len(id) > 3 and id[2] == '_':
593                                sid = id[3:]
594                        if sid in ft.files_props['s']:
595                                file_props = ft.files_props['s'][sid]
596                                file_props['error'] = -4
597                                self.handle_event_file_request_error(account,
598                                        (jid_from, file_props, errmsg))
599                                conn = gajim.connections[account]
600                                conn.disconnect_transfer(file_props)
601                                return
602                elif unicode(errcode) == '404':
603                        conn = gajim.connections[account]
604                        sid = id
605                        if len(id) > 3 and id[2] == '_':
606                                sid = id[3:]
607                        if sid in conn.files_props:
608                                file_props = conn.files_props[sid]
609                                self.handle_event_file_send_error(account,
610                                        (jid_from, file_props))
611                                conn.disconnect_transfer(file_props)
612                                return
613
614                ctrl = self.msg_win_mgr.get_control(jid_from, account)
615                if ctrl and ctrl.type_id == message_control.TYPE_GC:
616                        ctrl.print_conversation('Error %s: %s' % (array[2], array[1]))
617
618        def handle_event_con_type(self, account, con_type):
619                # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp'
620                gajim.con_types[account] = con_type
621                self.roster.draw_account(account)
622
623        def handle_event_connection_lost(self, account, array):
624                # ('CONNECTION_LOST', account, [title, text])
625                path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
626                        'connection_lost.png')
627                path = gtkgui_helpers.get_path_to_generic_or_avatar(path)
628                notify.popup(_('Connection Failed'), account, account,
629                        'connection_failed', path, array[0], array[1])
630
631        def unblock_signed_in_notifications(self, account):
632                gajim.block_signed_in_notifications[account] = False
633
634        def handle_event_status(self, account, status): # OUR status
635                #('STATUS', account, status)
636                model = self.roster.status_combobox.get_model()
637                if status == 'offline':
638                        # sensitivity for this menuitem
639                        if gajim.get_number_of_connected_accounts() == 0:
640                                model[self.roster.status_message_menuitem_iter][3] = False
641                        gajim.block_signed_in_notifications[account] = True
642                else:
643                        # 30 seconds after we change our status to sth else than offline
644                        # we stop blocking notifications of any kind
645                        # this prevents from getting the roster items as 'just signed in'
646                        # contacts. 30 seconds should be enough time
647                        gobject.timeout_add_seconds(30, self.unblock_signed_in_notifications, account)
648                        # sensitivity for this menuitem
649                        model[self.roster.status_message_menuitem_iter][3] = True
650
651                # Inform all controls for this account of the connection state change
652                ctrls = self.msg_win_mgr.get_controls()
653                if account in self.minimized_controls:
654                        # Can not be the case when we remove account
655                        ctrls += self.minimized_controls[account].values()
656                for ctrl in ctrls:
657                        if ctrl.account == account:
658                                if status == 'offline'