root/branches/gajim_0.11/src/gajim.py

Revision 7984, 79.8 kB (checked in by asterix, 22 months ago)

mrege diff from trunk

  • 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##      gajim.py
3##
4##
5## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
6## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
7## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
8## Copyright (C) 2005 Travis Shirk <travis@pobox.com>
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20import sys
21import os
22import urllib
23
24from common import i18n
25
26import message_control
27
28from chat_control import ChatControlBase
29from atom_window import AtomWindow
30
31from common import exceptions
32from common.zeroconf import connection_zeroconf
33from common import dbus_support
34
35if os.name == 'posix': # dl module is Unix Only
36        try: # rename the process name to gajim
37                import dl
38                libc = dl.open('/lib/libc.so.6')
39                libc.call('prctl', 15, 'gajim\0', 0, 0, 0)
40        except:
41                pass
42
43try:
44        import gtk
45except RuntimeError, msg:
46        if str(msg) == 'could not open display':
47                print >> sys.stderr, _('Gajim needs X server to run. Quiting...')
48                sys.exit()
49pritext = ''
50if gtk.pygtk_version < (2, 6, 0):
51        pritext = _('Gajim needs PyGTK 2.6 or above')
52        sectext = _('Gajim needs PyGTK 2.6 or above to run. Quiting...')
53elif gtk.gtk_version < (2, 6, 0):
54        pritext = _('Gajim needs GTK 2.6 or above')
55        sectext = _('Gajim needs GTK 2.6 or above to run. Quiting...')
56
57try:
58        import gtk.glade # check if user has libglade (in pygtk and in gtk)
59except ImportError:
60        pritext = _('GTK+ runtime is missing libglade support')
61        if os.name == 'nt':
62                sectext = _('Please remove your current GTK+ runtime and install the latest stable version from %s') % 'http://gladewin32.sourceforge.net'
63        else:
64                sectext = _('Please make sure that GTK+ and PyGTK have libglade support in your system.')
65
66try:
67        from common import check_paths
68except exceptions.PysqliteNotAvailable, e:
69        pritext = _('Gajim needs PySQLite2 to run')
70        sectext = str(e)
71
72if os.name == 'nt':
73        try:
74                import winsound # windows-only built-in module for playing wav
75                import win32api # do NOT remove. we req this module
76        except:
77                pritext = _('Gajim needs pywin32 to run')
78                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'
79
80if pritext:
81        dlg = gtk.MessageDialog(None, 
82                gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
83                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
84
85        dlg.format_secondary_text(sectext)
86        dlg.run()
87        dlg.destroy()
88        sys.exit()
89
90del pritext
91
92path = os.getcwd()
93if '.svn' in os.listdir(path) or '_svn' in os.listdir(path):
94        # import gtkexcepthook only for those that run svn
95        # those than run with --verbose run from terminal so no need to care
96        # about those
97        import gtkexcepthook
98del path
99
100import gobject
101
102import re
103import signal
104import getopt
105import time
106import math
107
108import gtkgui_helpers
109import notify
110
111import common.sleepy
112
113from common.xmpp import idlequeue
114from common import nslookup
115from common import proxy65_manager
116from common import socks5
117from common import gajim
118from common import helpers
119from common import optparser
120
121profile = ''
122try:
123        opts, args = getopt.getopt(sys.argv[1:], 'hvp:', ['help', 'verbose',
124                'profile=', 'sm-config-prefix=', 'sm-client-id='])
125except getopt.error, msg:
126        print msg
127        print 'for help use --help'
128        sys.exit(2)
129for o, a in opts:
130        if o in ('-h', '--help'):
131                print 'gajim [--help] [--verbose] [--profile name]'
132                sys.exit()
133        elif o in ('-v', '--verbose'):
134                gajim.verbose = True
135        elif o in ('-p', '--profile'): # gajim --profile name
136                profile = a
137del opts
138del args
139
140import locale
141profile = unicode(profile, locale.getpreferredencoding())
142
143import common.configpaths
144common.configpaths.init_profile(profile)
145del profile
146gajimpaths = common.configpaths.gajimpaths
147
148pid_filename = gajimpaths['PID_FILE']
149config_filename = gajimpaths['CONFIG_FILE']
150
151import traceback
152import errno
153
154import dialogs
155def pid_alive():
156        try:
157                pf = open(pid_filename)
158        except:
159                # probably file not found
160                return False
161
162        try:
163                pid = int(pf.read().strip())
164                pf.close()
165        except:
166                traceback.print_exc()
167                # PID file exists, but something happened trying to read PID
168                # Could be 0.10 style empty PID file, so assume Gajim is running
169                return True
170
171        if os.name == 'nt':
172                try:
173                        from ctypes import (windll, c_ulong, c_int, Structure, c_char, POINTER, pointer, )
174                except:
175                        return True
176
177                class PROCESSENTRY32(Structure):
178                        _fields_ = [
179                                ('dwSize', c_ulong, ),
180                                ('cntUsage', c_ulong, ),
181                                ('th32ProcessID', c_ulong, ),
182                                ('th32DefaultHeapID', c_ulong, ),
183                                ('th32ModuleID', c_ulong, ),
184                                ('cntThreads', c_ulong, ),
185                                ('th32ParentProcessID', c_ulong, ),
186                                ('pcPriClassBase', c_ulong, ),
187                                ('dwFlags', c_ulong, ),
188                                ('szExeFile', c_char*512, ),
189                                ]
190                        def __init__(self):
191                                Structure.__init__(self, 512+9*4)
192
193                k = windll.kernel32
194                k.CreateToolhelp32Snapshot.argtypes = c_ulong, c_ulong,
195                k.CreateToolhelp32Snapshot.restype = c_int
196                k.Process32First.argtypes = c_int, POINTER(PROCESSENTRY32),
197                k.Process32First.restype = c_int
198                k.Process32Next.argtypes = c_int, POINTER(PROCESSENTRY32),
199                k.Process32Next.restype = c_int
200
201                def get_p(p):
202                        h = k.CreateToolhelp32Snapshot(2, 0) # TH32CS_SNAPPROCESS
203                        assert h > 0, 'CreateToolhelp32Snapshot failed'
204                        b = pointer(PROCESSENTRY32())
205                        f = k.Process32First(h, b)
206                        while f:
207                                if b.contents.th32ProcessID == p:
208                                        return b.contents.szExeFile
209                                f = k.Process32Next(h, b)
210
211                if get_p(pid) == 'python.exe':
212                        return True
213                return False
214        try:
215                if not os.path.exists('/proc'):
216                        return True # no /proc, assume Gajim is running
217
218                try:
219                        f = open('/proc/%d/cmdline'% pid) 
220                except IOError, e:
221                        if e.errno == errno.ENOENT:
222                                return False # file/pid does not exist
223                        raise 
224
225                n = f.read().lower()
226                f.close()
227                if n.find('gajim') < 0:
228                        return False
229                return True # Running Gajim found at pid
230        except:
231                traceback.print_exc()
232
233        # If we are here, pidfile exists, but some unexpected error occured.
234        # Assume Gajim is running.
235        return True
236
237if pid_alive():
238        path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
239        pix = gtk.gdk.pixbuf_new_from_file(path_to_file)
240        gtk.window_set_default_icon(pix) # set the icon to all newly opened wind
241        pritext = _('Gajim is already running')
242        sectext = _('Another instance of Gajim seems to be running\nRun anyway?')
243        dialog = dialogs.YesNoDialog(pritext, sectext)
244        if dialog.get_response() != gtk.RESPONSE_YES:
245                sys.exit(3)
246        # run anyway, delete pid and useless global vars
247        if os.path.exists(pid_filename):
248                os.remove(pid_filename)
249        del path_to_file
250        del pix
251        del pritext
252        del sectext
253        dialog.destroy()
254
255# Create .gajim dir
256pid_dir =  os.path.dirname(pid_filename)
257if not os.path.exists(pid_dir):
258        check_paths.create_path(pid_dir)
259# Create pid file
260f = open(pid_filename, 'w')
261f.write(str(os.getpid()))
262f.close()
263del pid_dir
264del f
265
266def on_exit():
267        # delete pid file on normal exit
268        if os.path.exists(pid_filename):
269                os.remove(pid_filename)
270
271import atexit
272atexit.register(on_exit)
273
274parser = optparser.OptionsParser(config_filename)
275
276import roster_window
277import profile_window
278import config
279
280class GlibIdleQueue(idlequeue.IdleQueue):
281        '''
282        Extends IdleQueue to use glib io_add_wath, instead of select/poll
283        In another, `non gui' implementation of Gajim IdleQueue can be used safetly.
284        '''
285        def init_idle(self):
286                ''' this method is called at the end of class constructor.
287                Creates a dict, which maps file/pipe/sock descriptor to glib event id'''
288                self.events = {}
289                if gtk.pygtk_version >= (2, 8, 0):
290                        # time() is already called in glib, we just get the last value
291                        # overrides IdleQueue.current_time()
292                        self.current_time = lambda: gobject.get_current_time()
293                       
294        def add_idle(self, fd, flags):
295                ''' this method is called when we plug a new idle object.
296                Start listening for events from fd
297                '''
298                res = gobject.io_add_watch(fd, flags, self.process_events, 
299                        priority=gobject.PRIORITY_LOW)
300                # store the id of the watch, so that we can remove it on unplug
301                self.events[fd] = res
302       
303        def remove_idle(self, fd):
304                ''' this method is called when we unplug a new idle object.
305                Stop listening for events from fd
306                '''
307                gobject.source_remove(self.events[fd])
308                del(self.events[fd])
309       
310        def process(self):
311                self.check_time_events()
312       
313class Interface:
314        def handle_event_roster(self, account, data):
315                #('ROSTER', account, array)
316                self.roster.fill_contacts_and_groups_dicts(data, account)
317                self.roster.add_account_contacts(account)
318                self.roster.fire_up_unread_messages_events(account)
319                if self.remote_ctrl:
320                        self.remote_ctrl.raise_signal('Roster', (account, data))
321
322        def handle_event_warning(self, unused, data):
323                #('WARNING', account, (title_text, section_text))
324                dialogs.WarningDialog(data[0], data[1])
325
326        def handle_event_error(self, unused, data):
327                #('ERROR', account, (title_text, section_text))
328                dialogs.ErrorDialog(data[0], data[1])
329
330        def handle_event_information(self, unused, data):
331                #('INFORMATION', account, (title_text, section_text))
332                dialogs.InformationDialog(data[0], data[1])
333               
334        def handle_event_ask_new_nick(self, account, data):
335                #('ASK_NEW_NICK', account, (room_jid, title_text, prompt_text, proposed_nick))
336                room_jid = data[0]
337                title = data[1]
338                prompt = data[2]
339                proposed_nick = data[3]
340                gc_control = self.msg_win_mgr.get_control(room_jid, account)
341                if gc_control: # user may close the window before we are here
342                        gc_control.show_change_nick_input_dialog(title, prompt, proposed_nick)
343
344        def handle_event_http_auth(self, account, data):
345                #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj))
346                def response(widget, account, iq_obj, answer):
347                        self.dialog.destroy()
348                        gajim.connections[account].build_http_auth_answer(iq_obj, answer)
349
350                self.dialog = dialogs.YesNoDialog(_('HTTP (%s) Authorization for %s (id: %s)') \
351                        % (data[0], data[1], data[2]), _('Do you accept this request?'),
352                        on_response_yes = (response, account, data[3], 'yes'),
353                        on_response_no = (response, account, data[3], 'no'))
354
355        def handle_event_error_answer(self, account, array):
356                #('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
357                id, jid_from, errmsg, errcode = array
358                if unicode(errcode) in ('403', '406') and id:
359                        # show the error dialog
360                        ft = self.instances['file_transfers']
361                        sid = id
362                        if len(id) > 3 and id[2] == '_':
363                                sid = id[3:]
364                        if ft.files_props['s'].has_key(sid):
365                                file_props = ft.files_props['s'][sid]
366                                file_props['error'] = -4
367                                self.handle_event_file_request_error(account, 
368                                        (jid_from, file_props, errmsg))
369                                conn = gajim.connections[account]
370                                conn.disconnect_transfer(file_props)
371                                return
372                elif unicode(errcode) == '404':
373                        conn = gajim.connections[account]
374                        sid = id
375                        if len(id) > 3 and id[2] == '_':
376                                sid = id[3:]
377                        if conn.files_props.has_key(sid):
378                                file_props = conn.files_props[sid]
379                                self.handle_event_file_send_error(account, 
380                                        (jid_from, file_props))
381                                conn.disconnect_transfer(file_props)
382                                return
383                ctrl = self.msg_win_mgr.get_control(jid_from, account)
384                if ctrl and ctrl.type_id == message_control.TYPE_GC:
385                        ctrl.print_conversation('Error %s: %s' % (array[2], array[1]))
386
387        def handle_event_con_type(self, account, con_type):
388                # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp'
389                gajim.con_types[account] = con_type
390                self.roster.draw_account(account)
391
392        def handle_event_connection_lost(self, account, array):
393                # ('CONNECTION_LOST', account, [title, text])
394                path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
395                        'connection_lost.png')
396                path = gtkgui_helpers.get_path_to_generic_or_avatar(path)
397                notify.popup(_('Connection Failed'), account, account,
398                        'connection_failed', path, array[0], array[1])
399
400        def unblock_signed_in_notifications(self, account):
401                gajim.block_signed_in_notifications[account] = False
402
403        def handle_event_status(self, account, status): # OUR status
404                #('STATUS', account, status)
405                model = self.roster.status_combobox.get_model()
406                if status == 'offline':
407                        # sensitivity for this menuitem
408                        if gajim.get_number_of_connected_accounts() == 0:
409                                model[self.roster.status_message_menuitem_iter][3] = False
410                        gajim.block_signed_in_notifications[account] = True
411                else:
412                        # 30 seconds after we change our status to sth else than offline
413                        # we stop blocking notifications of any kind
414                        # this prevents from getting the roster items as 'just signed in'
415                        # contacts. 30 seconds should be enough time
416                        gobject.timeout_add(30000, self.unblock_signed_in_notifications, account)
417                        # sensitivity for this menuitem
418                        model[self.roster.status_message_menuitem_iter][3] = True
419
420                # Inform all controls for this account of the connection state change
421                for ctrl in self.msg_win_mgr.get_controls():
422                        if ctrl.account == account:
423                                if status == 'offline' or (status == 'invisible' and \
424                                                gajim.connections[account].is_zeroconf):
425                                        ctrl.got_disconnected()
426                                else:
427                                        # Other code rejoins all GCs, so we don't do it here
428                                        if not ctrl.type_id == message_control.TYPE_GC:
429                                                ctrl.got_connected()
430                                ctrl.parent_win.redraw_tab(ctrl)
431
432                self.roster.on_status_changed(account, status)
433                if account in self.show_vcard_when_connect:
434                        self.edit_own_details(account)
435                if self.remote_ctrl:
436                        self.remote_ctrl.raise_signal('AccountPresence', (status, account))
437       
438        def edit_own_details(self, account):
439                jid = gajim.get_jid_from_account(account)
440                if not self.instances[account].has_key('profile'):
441                        self.instances[account]['profile'] = \
442                                profile_window.ProfileWindow(account)
443                        gajim.connections[account].request_vcard(jid)
444
445        def handle_event_notify(self, account, array):
446                # 'NOTIFY' (account, (jid, status, status message, resource, priority,
447                # keyID, timestamp))
448                # if we're here it means contact changed show
449                statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd',
450                        'invisible']
451                # Ignore invalid show
452                if array[1] not in statuss:
453                        return
454                old_show = 0
455                new_show = statuss.index(array[1])
456                status_message = array[2]
457                jid = array[0].split('/')[0]
458                keyID = array[5]
459                attached_keys = gajim.config.get_per('accounts', account,
460                        'attached_gpg_keys').split()
461                if jid in attached_keys:
462                        keyID = attached_keys[attached_keys.index(jid) + 1]
463                resource = array[3]
464                if not resource:
465                        resource = ''
466                priority = array[4]
467                if gajim.jid_is_transport(jid):
468                        # It must be an agent
469                        ji = jid.replace('@', '')
470                else:
471                        ji = jid
472
473                # Update contact
474                jid_list = gajim.contacts.get_jid_list(account)
475                if ji in jid_list or jid == gajim.get_jid_from_account(account):
476                        lcontact = gajim.contacts.get_contacts_from_jid(account, ji)
477                        contact1 = None
478                        resources = []
479                        for c in lcontact:
480                                resources.append(c.resource)
481                                if c.resource == resource:
482                                        contact1 = c
483                                        break
484                        if contact1:
485                                if contact1.show in statuss:
486                                        old_show = statuss.index(contact1.show)
487                                if old_show == new_show and contact1.status == status_message and \
488                                        contact1.priority == priority: # no change
489                                        return
490                        else:
491                                contact1 = gajim.contacts.get_first_contact_from_jid(account, ji)
492                                if not contact1:
493                                        # presence of another resource of our jid
494                                        if resource == gajim.connections[account].server_resource:
495                                                return
496                                        contact1 = gajim.contacts.create_contact(jid = ji,
497                                                name = gajim.nicks[account], groups = [],
498                                                show = array[1], status = status_message, sub = 'both',
499                                                ask = 'none', priority = priority, keyID = keyID,
500                                                resource = resource)
501                                        old_show = 0
502                                        gajim.contacts.add_contact(account, contact1)
503                                        lcontact.append(contact1)
504                                        self.roster.add_self_contact(account)
505                                elif contact1.show in statuss:
506                                        old_show = statuss.index(contact1.show)
507                                if (resources != [''] and (len(lcontact) != 1 or 
508                                lcontact[0].show != 'offline')) and jid.find('@') > 0:
509                                        old_show = 0
510                                        contact1 = gajim.contacts.copy_contact(contact1)
511                                        lcontact.append(contact1)
512                                contact1.resource = resource
513                        if contact1.jid.find('@') > 0 and len(lcontact) == 1:
514                                # It's not an agent
515                                if old_show == 0 and new_show > 1:
516                                        if not contact1.jid in gajim.newly_added[account]:
517                                                gajim.newly_added[account].append(contact1.jid)
518                                        if contact1.jid in gajim.to_be_removed[account]:
519                                                gajim.to_be_removed[account].remove(contact1.jid)
520                                        gobject.timeout_add(5000, self.roster.remove_newly_added,
521                                                contact1.jid, account)
522                                elif old_show > 1 and new_show == 0 and gajim.connections[account].\
523                                        connected > 1:
524                                        if not contact1.jid in gajim.to_be_removed[account]:
525                                                gajim.to_be_removed[account].append(contact1.jid)
526                                        if contact1.jid in gajim.newly_added[account]:
527                                                gajim.newly_added[account].remove(contact1.jid)
528                                        self.roster.draw_contact(contact1.jid, account)
529                                        gobject.timeout_add(5000, self.roster.really_remove_contact,
530                                                contact1, account)
531                        contact1.show = array[1]
532                        contact1.status = status_message
533                        contact1.priority = priority
534                        contact1.keyID = keyID
535                        timestamp = array[6]
536                        if timestamp:
537                                contact1.last_status_time = timestamp
538                        elif not gajim.block_signed_in_notifications[account]:
539                                # We're connected since more that 30 seconds
540                                contact1.last_status_time = time.localtime()
541                if gajim.jid_is_transport(jid):
542                        # It must be an agent
543                        if ji in jid_list:
544                                # Update existing iter
545                                self.roster.draw_contact(ji, account)
546                                self.roster.draw_group(_('Transports'), account)
547                                if new_show > 1 and ji in gajim.transport_avatar[account]:
548                                        # transport just signed in. request avatars
549                                        for jid_ in gajim.transport_avatar[account][ji]:
550                                                gajim.connections[account].request_vcard(jid_)
551                                # transport just signed in/out, don't show popup notifications
552                                # for 30s
553                                account_ji = account + '/' + ji
554                                gajim.block_signed_in_notifications[account_ji] = True
555                                gobject.timeout_add(30000, self.unblock_signed_in_notifications,
556                                        account_ji)
557                        locations = (self.instances, self.instances[account])
558                        for location in locations:
559                                if location.has_key('add_contact'):
560                                        if old_show == 0 and new_show > 1:
561                                                location['add_contact'].transport_signed_in(jid)
562                                                break
563                                        elif old_show > 1 and new_show == 0:
564                                                location['add_contact'].transport_signed_out(jid)
565                                                break
566                elif ji in jid_list:
567                        # It isn't an agent
568                        # reset chatstate if needed:
569                        # (when contact signs out or has errors)
570                        if array[1] in ('offline', 'error'):
571                                contact1.our_chatstate = contact1.chatstate = \
572                                        contact1.composing_jep = None
573                                gajim.connections[account].remove_transfers_for_contact(contact1)
574                        self.roster.chg_contact_status(contact1, array[1], status_message,
575                                account)
576                        # Notifications
577                        if old_show < 2 and new_show > 1:
578                                notify.notify('contact_connected', jid, account, status_message)
579                                if self.remote_ctrl:
580                                        self.remote_ctrl.raise_signal('ContactPresence',
581                                                (account, array))
582
583                        elif old_show > 1 and new_show < 2:
584                                notify.notify('contact_disconnected', jid, account, status_message)
585                                if self.remote_ctrl:
586                                        self.remote_ctrl.raise_signal('ContactAbsence', (account, array))
587                                # FIXME: stop non active file transfers
588                        elif new_show > 1: # Status change (not connected/disconnected or error (<1))
589                                notify.notify('status_change', jid, account, [new_show,
590                                        status_message])
591                else:
592                        # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) doesn't follow the JEP
593                        # remove in 2007
594                        # It's maybe a GC_NOTIFY (specialy for MSN gc)
595                        self.handle_event_gc_notify(account, (jid, array[1], status_message,
596                                array[3], None, None, None, None, None, None, None))
597                       
598
599        def handle_event_msg(self, account, array):
600                # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
601                # chatstate, msg_id, composing_jep, user_nick, xhtml))
602                # user_nick is JEP-0172
603
604                full_jid_with_resource = array[0]
605                jid = gajim.get_jid_without_resource(full_jid_with_resource)
606                resource = gajim.get_resource_from_jid(full_jid_with_resource)
607
608                message = array[1]
609                encrypted = array[3]
610                msg_type = array[4]
611                subject = array