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

Revision 6407, 71.0 kB (checked in by dkirov, 2 years ago)

r6265, r6266, r6267, r6269, r6350, r6366

  • 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#!/bin/sh
2''':'
3exec python -OOt "$0" ${1+"$@"}
4' '''
5##      gajim.py
6##
7## Contributors for this file:
8## - Yann Le Boulanger <asterix@lagaule.org>
9## - Nikos Kouremenos <kourem@gmail.com>
10## - Dimitur Kirov <dkirov@gmail.com>
11## - Travis Shirk <travis@pobox.com>
12##
13## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
14##                         Vincent Hanquez <tab@snarc.org>
15## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
16##                    Vincent Hanquez <tab@snarc.org>
17##                    Nikos Kouremenos <nkour@jabber.org>
18##                    Dimitur Kirov <dkirov@gmail.com>
19##                    Travis Shirk <travis@pobox.com>
20##                    Norman Rasmussen <norman@rasmussen.co.za>
21##
22## This program is free software; you can redistribute it and/or modify
23## it under the terms of the GNU General Public License as published
24## by the Free Software Foundation; version 2 only.
25##
26## This program is distributed in the hope that it will be useful,
27## but WITHOUT ANY WARRANTY; without even the implied warranty of
28## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
29## GNU General Public License for more details.
30##
31
32import sys
33import os
34import urllib
35
36import message_control
37
38from chat_control import ChatControlBase
39
40from common import exceptions
41from common import i18n
42i18n.init()
43_ = i18n._
44
45try:
46        import gtk
47except RuntimeError, msg:
48        if str(msg) == 'could not open display':
49                print >> sys.stderr, _('Gajim needs Xserver to run. Quiting...')
50                sys.exit()
51pritext = ''
52if gtk.pygtk_version < (2, 6, 0):
53        pritext = _('Gajim needs PyGTK 2.6 or above')
54        sectext = _('Gajim needs PyGTK 2.6 or above to run. Quiting...')
55elif gtk.gtk_version < (2, 6, 0):
56        pritext = _('Gajim needs GTK 2.6 or above')
57        sectext = _('Gajim needs GTK 2.6 or above to run. Quiting...')
58
59try:
60        import gtk.glade # check if user has libglade (in pygtk and in gtk)
61except ImportError:
62        pritext = _('GTK+ runtime is missing libglade support')
63        if os.name == 'nt':
64                sectext = _('Please remove your current GTK+ runtime and install the latest stable version from %s') % 'http://gladewin32.sourceforge.net'
65        else:
66                sectext = _('Please make sure that GTK+ and PyGTK have libglade support in your system.')
67
68try:
69        from common import check_paths
70except exceptions.PysqliteNotAvailable, e:
71        pritext = _('Gajim needs PySQLite2 to run')
72        sectext = str(e)
73
74if pritext:
75        dlg = gtk.MessageDialog(None, 
76                                gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL,
77                                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, message_format = pritext)
78
79        dlg.format_secondary_text(sectext)
80        dlg.run()
81        dlg.destroy()
82        sys.exit()
83
84path = os.getcwd()
85if '.svn' in os.listdir(path) or '_svn' in os.listdir(path):
86        # import gtkexcepthook only for those that run svn
87        # those than run with --verbose run from terminal so no need to care
88        # about those
89        import gtkexcepthook
90del path
91
92import gobject
93
94import sre
95import signal
96import getopt
97import time
98import math
99
100import gtkgui_helpers
101import notify
102
103import common.sleepy
104
105from common.xmpp import idlequeue
106from common import nslookup
107from common import proxy65_manager
108from common import socks5
109from common import gajim
110from common import helpers
111from common import optparser
112
113profile = ''
114try:
115        opts, args = getopt.getopt(sys.argv[1:], 'hvp:', ['help', 'verbose',
116                'profile=', 'sm-config-prefix=', 'sm-client-id='])
117except getopt.error, msg:
118        print msg
119        print 'for help use --help'
120        sys.exit(2)
121for o, a in opts:
122        if o in ('-h', '--help'):
123                print 'gajim [--help] [--verbose] [--profile name]'
124                sys.exit()
125        elif o in ('-v', '--verbose'):
126                gajim.verbose = True
127        elif o in ('-p', '--profile'): # gajim --profile name
128                profile = a
129
130config_filename = os.path.expanduser('~/.gajim/config')
131if os.name == 'nt':
132        try:
133                # Documents and Settings\[User Name]\Application Data\Gajim\logs
134                config_filename = os.environ['appdata'] + '/Gajim/config'
135        except KeyError:
136                # win9x so ./config
137                config_filename = 'config'
138
139if profile:
140        config_filename += '.%s' % profile
141
142parser = optparser.OptionsParser(config_filename)
143
144import roster_window
145import systray
146import dialogs
147import vcard
148import config
149
150class MigrateCommand(nslookup.IdleCommand):
151        def __init__(self, on_result):
152                nslookup.IdleCommand.__init__(self, on_result)
153                self.commandtimeout = 10 
154       
155        def _compose_command_args(self):
156                return ['python', 'migrate_logs_to_dot9_db.py', 'dont_wait']
157       
158        def _return_result(self):
159                print self.result
160                if self.result_handler:
161                        self.result_handler(self.result)
162                self.result_handler = None
163
164class GlibIdleQueue(idlequeue.IdleQueue):
165        '''
166        Extends IdleQueue to use glib io_add_wath, instead of select/poll
167        In another, `non gui' implementation of Gajim IdleQueue can be used safetly.
168        '''
169        def init_idle(self):
170                ''' this method is called at the end of class constructor.
171                Creates a dict, which maps file/pipe/sock descriptor to glib event id'''
172                self.events = {}
173                if gtk.pygtk_version >= (2, 8, 0):
174                        # time() is already called in glib, we just get the last value
175                        # overrides IdleQueue.current_time()
176                        self.current_time = lambda: gobject.get_current_time()
177                       
178        def add_idle(self, fd, flags):
179                ''' this method is called when we plug a new idle object.
180                Start listening for events from fd
181                '''
182                res = gobject.io_add_watch(fd, flags, self.process_events, 
183                                                                                priority=gobject.PRIORITY_LOW)
184                # store the id of the watch, so that we can remove it on unplug
185                self.events[fd] = res
186       
187        def remove_idle(self, fd):
188                ''' this method is called when we unplug a new idle object.
189                Stop listening for events from fd
190                '''
191                gobject.source_remove(self.events[fd])
192                del(self.events[fd])
193       
194        def process(self):
195                self.check_time_events()
196       
197class Interface:
198        def handle_event_roster(self, account, data):
199                #('ROSTER', account, array)
200                self.roster.fill_contacts_and_groups_dicts(data, account)
201                self.roster.add_account_contacts(account)
202                self.roster.fire_up_unread_messages_events(account)
203                if self.remote_ctrl:
204                        self.remote_ctrl.raise_signal('Roster', (account, data))
205
206        def handle_event_warning(self, unused, data):
207                #('WARNING', account, (title_text, section_text))
208                dialogs.WarningDialog(data[0], data[1])
209
210        def handle_event_error(self, unused, data):
211                #('ERROR', account, (title_text, section_text))
212                dialogs.ErrorDialog(data[0], data[1])
213
214        def handle_event_information(self, unused, data):
215                #('INFORMATION', account, (title_text, section_text))
216                dialogs.InformationDialog(data[0], data[1])
217               
218        def handle_event_ask_new_nick(self, account, data):
219                #('ASK_NEW_NICK', account, (room_jid, title_text, prompt_text, proposed_nick))
220                room_jid = data[0]
221                title = data[1]
222                prompt = data[2]
223                proposed_nick = data[3]
224                gc_control = self.msg_win_mgr.get_control(room_jid, account)
225                if gc_control: # user may close the window before we are here
226                        gc_control.show_change_nick_input_dialog(title, prompt, proposed_nick)
227
228        def handle_event_http_auth(self, account, data):
229                #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj))
230                def response(widget, account, iq_obj, answer):
231                        self.dialog.destroy()
232                        gajim.connections[account].build_http_auth_answer(iq_obj, answer)
233
234                self.dialog = dialogs.YesNoDialog(_('HTTP (%s) Authorization for %s (id: %s)') \
235                        % (data[0], data[1], data[2]), _('Do you accept this request?'),
236                        on_response_yes = (response, account, data[3], 'yes'),
237                        on_response_no = (response, account, data[3], 'no'))
238
239        def handle_event_error_answer(self, account, array):
240                #('ERROR_ANSWER', account, (id, jid_from. errmsg, errcode))
241                id, jid_from, errmsg, errcode = array
242                if unicode(errcode) in ('403', '406') and id:
243                        # show the error dialog
244                        ft = self.instances['file_transfers']
245                        sid = id
246                        if len(id) > 3 and id[2] == '_':
247                                sid = id[3:]
248                        if ft.files_props['s'].has_key(sid):
249                                file_props = ft.files_props['s'][sid]
250                                file_props['error'] = -4
251                                self.handle_event_file_request_error(account, 
252                                        (jid_from, file_props))
253                                conn = gajim.connections[account]
254                                conn.disconnect_transfer(file_props)
255                                return
256                elif unicode(errcode) == '404':
257                        conn = gajim.connections[account]
258                        sid = id
259                        if len(id) > 3 and id[2] == '_':
260                                sid = id[3:]
261                        if conn.files_props.has_key(sid):
262                                file_props = conn.files_props[sid]
263                                self.handle_event_file_send_error(account, 
264                                        (jid_from, file_props))
265                                conn.disconnect_transfer(file_props)
266                                return
267                ctrl = self.msg_win_mgr.get_control(jid_from, account)
268                if ctrl and ctrl.type_id == message_control.TYPE_GC:
269                        ctrl.print_conversation('Error %s: %s' % (array[2], array[1]))
270
271        def handle_event_con_type(self, account, con_type):
272                # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp'
273                gajim.con_types[account] = con_type
274                self.roster.draw_account(account)
275
276        def unblock_signed_in_notifications(self, account):
277                gajim.block_signed_in_notifications[account] = False
278
279        def handle_event_status(self, account, status): # OUR status
280                #('STATUS', account, status)
281                model = self.roster.status_combobox.get_model()
282                if status == 'offline':
283                        # sensitivity for this menuitem
284                        if gajim.get_number_of_connected_accounts() == 0:
285                                model[self.roster.status_message_menuitem_iter][3] = False
286                        gajim.block_signed_in_notifications[account] = True
287                else:
288                        # 30 seconds after we change our status to sth else than offline
289                        # we stop blocking notifications of any kind
290                        # this prevents from getting the roster items as 'just signed in'
291                        # contacts. 30 seconds should be enough time
292                        gobject.timeout_add(30000, self.unblock_signed_in_notifications, account)
293                        # sensitivity for this menuitem
294                        model[self.roster.status_message_menuitem_iter][3] = True
295
296                # Inform all controls for this account of the connection state change
297                for ctrl in self.msg_win_mgr.get_controls():
298                        if ctrl.account == account:
299                                if status == 'offline':
300                                        ctrl.got_disconnected()
301                                else:
302                                        # Other code rejoins all GCs, so we don't do it here
303                                        if not ctrl.type_id == message_control.TYPE_GC:
304                                                ctrl.got_connected()
305                                ctrl.parent_win.redraw_tab(ctrl)
306
307                self.roster.on_status_changed(account, status)
308                if account in self.show_vcard_when_connect:
309                        self.edit_own_details(account)
310                if self.remote_ctrl:
311                        self.remote_ctrl.raise_signal('AccountPresence', (status, account))
312       
313        def edit_own_details(self, account):
314                jid = gajim.get_jid_from_account(account)
315                if not self.instances[account]['infos'].has_key(jid):
316                        self.instances[account]['infos'][jid] = \
317                                vcard.VcardWindow(jid, account, True)
318                        gajim.connections[account].request_vcard(jid)
319
320        def handle_event_notify(self, account, array):
321                # 'NOTIFY' (account, (jid, status, status message, resource, priority,
322                # keyID, timestamp))
323                # if we're here it means contact changed show
324                statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd',
325                        'invisible']
326                old_show = 0
327                new_show = statuss.index(array[1])
328                status_message = array[2]
329                jid = array[0].split('/')[0]
330                keyID = array[5]
331                attached_keys = gajim.config.get_per('accounts', account,
332                        'attached_gpg_keys').split()
333                if jid in attached_keys:
334                        keyID = attached_keys[attached_keys.index(jid) + 1]
335                resource = array[3]
336                if not resource:
337                        resource = ''
338                priority = array[4]
339                if gajim.jid_is_transport(jid):
340                        # It must be an agent
341                        ji = jid.replace('@', '')
342                else:
343                        ji = jid
344
345                # Update contact
346                jid_list = gajim.contacts.get_jid_list(account)
347                if ji in jid_list:
348                        lcontact = gajim.contacts.get_contacts_from_jid(account, ji)
349                        contact1 = None
350                        resources = []
351                        for c in lcontact:
352                                resources.append(c.resource)
353                                if c.resource == resource:
354                                        contact1 = c
355                                        break
356                        if contact1:
357                                if contact1.show in statuss:
358                                        old_show = statuss.index(contact1.show)
359                                if old_show == new_show and contact1.status == status_message and \
360                                        contact1.priority == priority: # no change
361                                        return
362                        else:
363                                contact1 = gajim.contacts.get_first_contact_from_jid(account, ji)
364                                if contact1.show in statuss:
365                                        old_show = statuss.index(contact1.show)
366                                if (resources != [''] and (len(lcontact) != 1 or 
367                                        lcontact[0].show != 'offline')) and jid.find('@') > 0:
368                                        old_show = 0
369                                        contact1 = gajim.contacts.copy_contact(contact1)
370                                        lcontact.append(contact1)
371                                contact1.resource = resource
372                        if contact1.jid.find('@') > 0 and len(lcontact) == 1: # It's not an agent
373                                if old_show == 0 and new_show > 1:
374                                        if not contact1.jid in gajim.newly_added[account]:
375                                                gajim.newly_added[account].append(contact1.jid)
376                                        if contact1.jid in gajim.to_be_removed[account]:
377                                                gajim.to_be_removed[account].remove(contact1.jid)
378                                        gobject.timeout_add(5000, self.roster.remove_newly_added,
379                                                contact1.jid, account)
380                                elif old_show > 1 and new_show == 0 and gajim.connections[account].\
381                                        connected > 1:
382                                        if not contact1.jid in gajim.to_be_removed[account]:
383                                                gajim.to_be_removed[account].append(contact1.jid)
384                                        if contact1.jid in gajim.newly_added[account]:
385                                                gajim.newly_added[account].remove(contact1.jid)
386                                        self.roster.draw_contact(contact1.jid, account)
387                                        gobject.timeout_add(5000, self.roster.really_remove_contact,
388                                                contact1, account)
389                        contact1.show = array[1]
390                        contact1.status = status_message
391                        contact1.priority = priority
392                        contact1.keyID = keyID
393                        timestamp = array[6]
394                        if timestamp:
395                                contact1.last_status_time = timestamp
396                        elif not gajim.block_signed_in_notifications[account]:
397                                # We're connected since more that 30 seconds
398                                contact1.last_status_time = time.localtime()
399                if gajim.jid_is_transport(jid):
400                        # It must be an agent
401                        if ji in jid_list:
402                                # Update existing iter
403                                self.roster.draw_contact(ji, account)
404                elif jid == gajim.get_jid_from_account(account):
405                        # It's another of our resources.  We don't need to see that!
406                        return
407                elif ji in jid_list:
408                        # It isn't an agent
409                        # reset chatstate if needed:
410                        # (when contact signs out or has errors)
411                        if array[1] in ('offline', 'error'):
412                                contact1.our_chatstate = contact1.chatstate = \
413                                        contact1.composing_jep = None
414                                gajim.connections[account].remove_transfers_for_contact(contact1)
415                        self.roster.chg_contact_status(contact1, array[1], status_message,
416                                account)
417                        # play sound
418                        if old_show < 2 and new_show > 1:
419                                if gajim.config.get_per('soundevents', 'contact_connected',
420                                        'enabled') and not gajim.block_signed_in_notifications[account]:
421                                        helpers.play_sound('contact_connected')
422                                if not gajim.awaiting_events[account].has_key(jid) and \
423                                        gajim.config.get('notify_on_signin') and \
424                                        not gajim.block_signed_in_notifications[account]:
425                                        if helpers.allow_showing_notification(account):
426                                                transport_name = gajim.get_transport_name_from_jid(jid)
427                                                img = None
428                                                if transport_name:
429                                                        img = os.path.join(gajim.DATA_DIR, 'iconsets',
430                                                                'transports', transport_name, '48x48',
431                                                                'online.png')
432                                                if not img or not os.path.isfile(img):
433                                                        iconset = gajim.config.get('iconset')
434                                                        img = os.path.join(gajim.DATA_DIR, 'iconsets',
435                                                                        iconset, '48x48', 'online.png')
436                                                path = gtkgui_helpers.get_path_to_generic_or_avatar(img,
437                                                        jid = jid, suffix = '_notif_size_colored.png')
438                                                title = _('%(nickname)s Signed In') % \
439                                                        {'nickname': gajim.get_name_from_jid(account, jid)}
440                                                text = ''
441                                                if status_message:
442                                                        text = status_message
443                                                notify.notify(_('Contact Signed In'), jid, account,
444                                                        path_to_image = path, title = title, text = text)
445
446                                if self.remote_ctrl:
447                                        self.remote_ctrl.raise_signal('ContactPresence',
448                                                (account, array))
449                               
450                        elif old_show > 1 and new_show < 2:
451                                if gajim.config.get_per('soundevents', 'contact_disconnected',
452                                                'enabled'):
453                                        helpers.play_sound('contact_disconnected')
454                                if not gajim.awaiting_events[account].has_key(jid) and \
455                                        gajim.config.get('notify_on_signout'):
456                                        if helpers.allow_showing_notification(account):
457                                                transport_name = gajim.get_transport_name_from_jid(jid)
458                                                img = None
459                                                if transport_name:
460                                                        img = os.path.join(gajim.DATA_DIR, 'iconsets',
461                                                                'transports', transport_name, '48x48',
462                                                                'offline.png')
463                                                if not img or not os.path.isfile(img):
464                                                        iconset = gajim.config.get('iconset')
465                                                        img = os.path.join(gajim.DATA_DIR, 'iconsets',
466                                                                        iconset, '48x48', 'offline.png')
467                                                path = gtkgui_helpers.get_path_to_generic_or_avatar(img,
468                                                        jid = jid, suffix = '_notif_size_bw.png')
469                                                title = _('%(nickname)s Signed Out') % \
470                                                        {'nickname': gajim.get_name_from_jid(account, jid)}
471                                                text = ''
472                                                if status_message:
473                                                        text = status_message
474                                                notify.notify(_('Contact Signed Out'), jid, account,
475                                                        path_to_image = path, title = title, text = text)
476
477                                if self.remote_ctrl:
478                                        self.remote_ctrl.raise_signal('ContactAbsence', (account, array))
479                                # FIXME: stop non active file transfers
480                else:
481                        # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) doesn't follow the JEP
482                        # remove in 2007
483                        # It's maybe a GC_NOTIFY (specialy for MSN gc)
484                        self.handle_event_gc_notify(account, (jid, array[1], status_message,
485                                array[3], None, None, None, None, None, None, None))
486                       
487
488        def handle_event_msg(self, account, array):
489                # 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
490                # chatstate))
491
492                full_jid_with_resource = array[0]
493                jid = gajim.get_jid_without_resource(full_jid_with_resource)
494                resource = gajim.get_resource_from_jid(full_jid_with_resource)
495
496                message = array[1]
497                msg_type = array[4]
498                chatstate = array[6]
499                msg_id = array[7]
500                composing_jep = array[8]
501                if gajim.jid_is_transport(jid):
502                        jid = jid.replace('@', '')
503
504                groupchat_control = self.msg_win_mgr.get_control(jid, account)
505                pm = False
506                if groupchat_control and groupchat_control.type_id == \
507                message_control.TYPE_GC:
508                        # It's a Private message
509                        pm = True
510
511                chat_control = None
512                jid_of_control = full_jid_with_resource
513                highest_contact = gajim.contacts.get_contact_with_highest_priority(
514                        account, jid)
515                # Look for a chat control that has the given resource, or default to one
516                # without resource
517                ctrl = self.msg_win_mgr.get_control(full_jid_with_resource, account)
518                if ctrl:
519                        chat_control = ctrl
520                elif not pm and (not highest_contact or not highest_contact.resource):
521                        # unknow contact or offline message
522                        jid_of_control = jid
523                        chat_control = self.msg_win_mgr.get_control(jid, account)
524                elif highest_contact and resource != highest_contact.resource:
525                        jid_of_control = full_jid_with_resource
526                        chat_control = None
527                elif not pm:
528                        jid_of_control = jid
529                        chat_control = self.msg_win_mgr.get_control(jid, account)
530
531                # Handle chat states 
532                contact = gajim.contacts.get_contact(account, jid, resource)
533                if contact and isinstance(contact, list):
534                        contact = contact[0]
535                if contact:
536                        contact.composing_jep = composing_jep
537                        if chat_control and chat_control.type_id == message_control.TYPE_CHAT:
538                                if chatstate is not None:
539                                        # other peer sent us reply, so he supports jep85 or jep22
540                                        contact.chatstate = chatstate
541                                        if contact.our_chatstate == 'ask': # we were jep85 disco?
542                                                contact.our_chatstate = 'active' # no more
543                                        chat_control.handle_incoming_chatstate()
544                                elif contact.chatstate != 'active':
545                                        # got no valid jep85 answer, peer does not support it
546                                        contact.chatstate = False
547                        elif chatstate == 'active':
548                                # Brand new message, incoming. 
549                                contact.our_chatstate = chatstate
550                                contact.chatstate = chatstate
551                                if msg_id: # Do not overwrite an existing msg_id with None
552                                        contact.msg_id = msg_id
553
554                # THIS MUST BE AFTER chatstates handling
555                # AND BEFORE playsound (else we here sounding on chatstates!)
556                if not message: # empty message text
557                        return
558
559                if gajim.config.get('ignore_unknown_contacts') and \
560                        not gajim.contacts.get_contact(account, jid) and not pm:
561                        return
562
563                first = False
564                if not chat_control and not gajim.awaiting_events[account].has_key(
565                jid_of_control):
566                        # It's a first message and not a Private Message
567                        first = True
568
569                if gajim.config.get_per('soundevents', 'first_message_received',
570                        'enabled') and first:
571                        helpers.play_sound('first_message_received')
572                elif gajim.config.get_per('soundevents', 'next_message_received',
573                        'enabled'):
574                        helpers.play_sound('next_message_received')
575
576                if pm:
577                        room_jid = jid
578                        nick = resource
579                        if first:
580                                if gajim.config.get('notify_on_new_message') and \
581                                helpers.allow_showing_notification(account):
582                                        room_name, t = gajim.get_room_name_and_server_from_room_jid(
583                                                room_jid)
584                                        img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events',
585                                                'priv_msg_recv.png')
586                                        path = gtkgui_helpers.get_path_to_generic_or_avatar(img)
587                                        title = _('New Private Message from room %s