gui_interface.py 131 KB
Newer Older
steve-e's avatar
steve-e committed
1 2 3
# -*- coding:utf-8 -*-
## src/gajim.py
##
Dicson's avatar
Dicson committed
4
## Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
steve-e's avatar
steve-e committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
## Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
## Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
##                    Norman Rasmussen <norman AT rasmussen.co.za>
##                    Stéphan Kochen <stephan AT kochen.nl>
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
##                         Alex Mauer <hawke AT hawkesnest.net>
## Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
##                    Stefan Bethge <stefan AT lanpartei.de>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
##                    James Newton <redshodan AT gmail.com>
## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
##                         Julien Pivotto <roidelapluie AT gmail.com>
##                         Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
import os
import sys
import re
import time
Philipp Hörist's avatar
Philipp Hörist committed
41
import hashlib
steve-e's avatar
steve-e committed
42

43 44
from gi.repository import Gtk
from gi.repository import GdkPixbuf
Yann Leboulanger's avatar
Yann Leboulanger committed
45
from gi.repository import GLib
46
from gi.repository import Gio
47
from gi.repository import Gdk
steve-e's avatar
steve-e committed
48

Philipp Hörist's avatar
Philipp Hörist committed
49 50 51 52 53
try:
    from PIL import Image
except:
    pass

54
from gajim.common import app
André's avatar
André committed
55
from gajim.common import events
steve-e's avatar
steve-e committed
56

André's avatar
André committed
57 58
from gajim.music_track_listener import MusicTrackListener

59
if app.is_installed('GEOCLUE'):
André's avatar
André committed
60 61
    from gajim.common import location_listener

André's avatar
André committed
62 63 64 65 66
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import dialogs
from gajim import notify
from gajim import message_control
67
from gajim.dialog_messages import get_dialog
68 69
from gajim.dialogs import ProgressWindow
from gajim.dialogs import FileChooserDialog
steve-e's avatar
steve-e committed
70

André's avatar
André committed
71 72 73 74 75
from gajim.chat_control_base import ChatControlBase
from gajim.chat_control import ChatControl
from gajim.groupchat_control import GroupchatControl
from gajim.groupchat_control import PrivateChatControl
from gajim.message_window import MessageWindowMgr
Philipp Hörist's avatar
Philipp Hörist committed
76
from gajim.filetransfers_window import FileTransfersWindow
steve-e's avatar
steve-e committed
77

André's avatar
André committed
78 79
from gajim.atom_window import AtomWindow
from gajim.session import ChatControlSession
steve-e's avatar
steve-e committed
80

André's avatar
André committed
81
from gajim.common import sleepy
steve-e's avatar
steve-e committed
82

83
from nbxmpp import idlequeue
84
from nbxmpp import Hashes2
André's avatar
André committed
85 86 87 88 89 90 91 92
from gajim.common.zeroconf import connection_zeroconf
from gajim.common import resolver
from gajim.common import caps_cache
from gajim.common import proxy65_manager
from gajim.common import socks5
from gajim.common import helpers
from gajim.common import passwords
from gajim.common import logging_helpers
Philipp Hörist's avatar
Philipp Hörist committed
93 94
from gajim.common.connection_handlers_events import (
    OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
Philipp Hörist's avatar
Philipp Hörist committed
95 96
    UpdateRosterAvatarEvent, UpdateGCAvatarEvent, UpdateRoomAvatarEvent,
    HTTPUploadProgressEvent)
André's avatar
André committed
97 98 99 100
from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp
from gajim.common import pep
from gajim import emoticons
Philipp Hörist's avatar
Philipp Hörist committed
101
from gajim.common.const import AvatarSize
André's avatar
André committed
102 103 104 105

from gajim import roster_window
from gajim import profile_window
from gajim import config
steve-e's avatar
steve-e committed
106
from threading import Thread
André's avatar
André committed
107
from gajim.common import ged
108
from gajim.common.caps_cache import muc_caps_cache
steve-e's avatar
steve-e committed
109

Philipp Hörist's avatar
Philipp Hörist committed
110
from gajim.common import configpaths
steve-e's avatar
steve-e committed
111

André's avatar
André committed
112
from gajim.common import optparser
Philipp Hörist's avatar
Philipp Hörist committed
113
parser = optparser.OptionsParser(configpaths.get('CONFIG_FILE'))
steve-e's avatar
steve-e committed
114 115 116 117 118 119 120 121 122 123

import logging
log = logging.getLogger('gajim.interface')

class Interface:

################################################################################
### Methods handling events from connection
################################################################################

124 125
    def handle_event_db_error(self, unused, error):
        #('DB_ERROR', account, error)
126 127
        if self.db_error_dialog:
            return
128
        self.db_error_dialog = dialogs.ErrorDialog(_('Database Error'), error)
129 130 131 132
        def destroyed(win):
            self.db_error_dialog = None
        self.db_error_dialog.connect('destroy', destroyed)

133 134
    @staticmethod
    def handle_event_information(obj):
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
        if not obj.popup:
            return

        if obj.dialog_name is not None:
            get_dialog(obj.dialog_name, *obj.args, **obj.kwargs)
            return

        if obj.level == 'error':
            cls = dialogs.ErrorDialog
        elif obj.level == 'warn':
            cls = dialogs.WarningDialog
        elif obj.level == 'info':
            cls = dialogs.InformationDialog
        else:
            return

        cls(obj.pri_txt, GLib.markup_escape_text(obj.sec_txt))
152

153 154 155 156
    @staticmethod
    def raise_dialog(name, *args, **kwargs):
        get_dialog(name, *args, **kwargs)

lovetox's avatar
lovetox committed
157
    def handle_ask_new_nick(self, account, room_jid, parent_win):
158
        title = _('Unable to join group chat')
lovetox's avatar
lovetox committed
159 160 161 162
        prompt = _('Your desired nickname in group chat\n'
                   '<b>%s</b>\n'
                   'is in use or registered by another occupant.\n'
                   'Please specify another nickname below:') % room_jid
163 164 165
        check_text = _('Always use this nickname when there is a conflict')
        if 'change_nick_dialog' in self.instances:
            self.instances['change_nick_dialog'].add_room(account, room_jid,
166
                prompt)
167 168
        else:
            self.instances['change_nick_dialog'] = dialogs.ChangeNickDialog(
lovetox's avatar
lovetox committed
169
                account, room_jid, title, prompt, transient_for=parent_win)
170

171 172
    @staticmethod
    def handle_event_http_auth(obj):
173
        #('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
174
        def response(account, answer):
175
            obj.conn.build_http_auth_answer(obj.stanza, answer)
176

177 178
        def on_yes(is_checked, obj):
            response(obj, 'yes')
179

180
        account = obj.conn.name
181
        sec_msg = _('Do you accept this request?')
182
        if app.get_number_of_connected_accounts() > 1:
183
            sec_msg = _('Do you accept this request on account %s?') % account
184 185
        if obj.msg:
            sec_msg = obj.msg + '\n' + sec_msg
186
        dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for '
187
            '%(url)s (ID: %(id)s)') % {'method': obj.method, 'url': obj.url,
188 189
            'id': obj.iq_id}, sec_msg, on_response_yes=(on_yes, obj),
            on_response_no=(response, obj, 'no'))
190

191
    def handle_event_iq_error(self, obj):
192
        #('ERROR_ANSWER', account, (id_, fjid, errmsg, errcode))
Yann Leboulanger's avatar
Yann Leboulanger committed
193
        if str(obj.errcode) in ('400', '403', '406') and obj.id_:
194 195
            # show the error dialog
            ft = self.instances['file_transfers']
196 197 198
            sid = obj.id_
            if len(obj.id_) > 3 and obj.id_[2] == '_':
                sid = obj.id_[3:]
zimio's avatar
zimio committed
199 200
            file_props = FilesProp.getFileProp(obj.conn.name, sid)
            if file_props :
Yann Leboulanger's avatar
Yann Leboulanger committed
201
                if str(obj.errcode) == '400':
zimio's avatar
zimio committed
202
                    file_props.error = -3
203
                else:
zimio's avatar
zimio committed
204
                    file_props.error = -4
205
                app.nec.push_incoming_event(FileRequestErrorEvent(None,
Yann Leboulanger's avatar
Yann Leboulanger committed
206
                    conn=obj.conn, jid=obj.jid, file_props=file_props,
207
                    error_msg=obj.errmsg))
208
                obj.conn.disconnect_transfer(file_props)
209
                return
Yann Leboulanger's avatar
Yann Leboulanger committed
210
        elif str(obj.errcode) == '404':
211 212 213
            sid = obj.id_
            if len(obj.id_) > 3 and obj.id_[2] == '_':
                sid = obj.id_[3:]
zimio's avatar
zimio committed
214
            file_props = FilesProp.getFileProp(obj.conn.name, sid)
Yann Leboulanger's avatar
Yann Leboulanger committed
215 216 217 218 219
            if file_props:
                self.handle_event_file_send_error(obj.conn.name, (obj.fjid,
                    file_props))
                obj.conn.disconnect_transfer(file_props)
                return
220

221
        ctrl = self.msg_win_mgr.get_control(obj.fjid, obj.conn.name)
222
        if ctrl and ctrl.type_id == message_control.TYPE_GC:
223
            ctrl.print_conversation('Error %s: %s' % (obj.errcode, obj.errmsg))
224

225 226
    @staticmethod
    def handle_event_connection_lost(obj):
227 228
        # ('CONNECTION_LOST', account, [title, text])
        path = gtkgui_helpers.get_icon_path('gajim-connection_lost', 48)
229
        account = obj.conn.name
230
        notify.popup(_('Connection Failed'), account, account,
231
            'connection-lost', path, obj.title, obj.msg)
232

233 234
    @staticmethod
    def unblock_signed_in_notifications(account):
235
        app.block_signed_in_notifications[account] = False
236

237
    def handle_event_status(self, obj): # OUR status
238
        #('STATUS', account, show)
239 240
        account = obj.conn.name
        if obj.show in ('offline', 'error'):
Dicson's avatar
Dicson committed
241
            for name in list(self.instances[account]['online_dialog'].keys()):
242 243
                # .keys() is needed to not have a dictionary length changed
                # during iteration error
244
                self.instances[account]['online_dialog'][name].destroy()
245 246 247
                if name in self.instances[account]['online_dialog']:
                    # destroy handler may have already removed it
                    del self.instances[account]['online_dialog'][name]
248 249
            for request in self.gpg_passphrase.values():
                if request:
250
                    request.interrupt(account=account)
251 252
            if account in self.pass_dialog:
                self.pass_dialog[account].window.destroy()
253
        if obj.show == 'offline':
254
            app.block_signed_in_notifications[account] = True
255 256 257 258 259
        else:
            # 30 seconds after we change our status to sth else than offline
            # we stop blocking notifications of any kind
            # this prevents from getting the roster items as 'just signed in'
            # contacts. 30 seconds should be enough time
Yann Leboulanger's avatar
Yann Leboulanger committed
260 261
            GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications,
                account)
262

263 264
        if account in self.show_vcard_when_connect and obj.show not in (
        'offline', 'error'):
265
            self.edit_own_details(account)
Philipp Hörist's avatar
Philipp Hörist committed
266
            self.show_vcard_when_connect.remove(account)
267 268 269 270

    def edit_own_details(self, account):
        if 'profile' not in self.instances[account]:
            self.instances[account]['profile'] = \
271
            profile_window.ProfileWindow(account, app.interface.roster.window)
272

273 274
    @staticmethod
    def handle_gc_error(gc_control, pritext, sectext):
Yann Leboulanger's avatar
Yann Leboulanger committed
275
        if gc_control and gc_control.autorejoin is not None:
276 277 278 279 280 281 282
            if gc_control.error_dialog:
                gc_control.error_dialog.destroy()
            def on_close(dummy):
                gc_control.error_dialog.destroy()
                gc_control.error_dialog = None
            gc_control.error_dialog = dialogs.ErrorDialog(pritext, sectext,
                on_response_ok=on_close, on_response_cancel=on_close)
283 284 285 286
            gc_control.error_dialog.set_modal(False)
            if gc_control.parent_win:
                gc_control.error_dialog.set_transient_for(
                    gc_control.parent_win.window)
287
        else:
288 289 290 291
            d = dialogs.ErrorDialog(pritext, sectext)
            if gc_control and gc_control.parent_win:
                d.set_transient_for(gc_control.parent_win.window)
            d.set_modal(False)
292 293 294

    def handle_gc_password_required(self, account, room_jid, nick):
        def on_ok(text):
295 296
            app.connections[account].join_gc(nick, room_jid, text)
            app.gc_passwords[room_jid] = text
297
            gc_control.error_dialog = None
298 299 300

        def on_cancel():
            # get and destroy window
301
            if room_jid in app.interface.minimized_controls[account]:
302 303 304 305 306
                self.roster.on_disconnect(None, room_jid, account)
            else:
                win = self.msg_win_mgr.get_window(room_jid, account)
                ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
                win.remove_tab(ctrl, 3)
307
            gc_control.error_dialog = None
308

309 310 311 312 313
        gc_control = self.msg_win_mgr.get_gc_control(room_jid, account)
        if gc_control:
            if gc_control.error_dialog:
                gc_control.error_dialog.destroy()

Yann Leboulanger's avatar
Yann Leboulanger committed
314 315 316 317 318
            gc_control.error_dialog = dialogs.InputDialog(_('Password Required'),
                _('A Password is required to join the room %s. Please type it.') % \
                room_jid, is_modal=False, ok_handler=on_ok,
                cancel_handler=on_cancel)
            gc_control.error_dialog.input_entry.set_visibility(False)
319 320 321

    def handle_event_gc_presence(self, obj):
        gc_control = obj.gc_control
lovetox's avatar
lovetox committed
322
        parent_win = None
323
        if gc_control and gc_control.parent_win:
lovetox's avatar
lovetox committed
324
            parent_win = gc_control.parent_win.window
325 326 327 328 329
        if obj.ptype == 'error':
            if obj.errcode == '503':
                # maximum user number reached
                self.handle_gc_error(gc_control,
                    _('Unable to join group chat'),
330
                    _('<b>%s</b> is full')\
331
                    % obj.room_jid)
332 333 334 335 336 337 338
            elif (obj.errcode == '401') or (obj.errcon == 'not-authorized'):
                # password required to join
                self.handle_gc_password_required(obj.conn.name, obj.room_jid,
                    obj.nick)
            elif (obj.errcode == '403') or (obj.errcon == 'forbidden'):
                # we are banned
                self.handle_gc_error(gc_control, _('Unable to join group chat'),
339 340
                    _('You are banned from group chat <b>%s</b>.') % \
                    obj.room_jid)
341 342
            elif (obj.errcode == '404') or (obj.errcon in ('item-not-found',
            'remote-server-not-found')):
Annika Sommer's avatar
Annika Sommer committed
343 344 345 346
                # remote server does not exist
                if (obj.errcon == 'remote-server-not-found'):
                    self.handle_gc_error(gc_control, _('Unable to join group chat'),
                    _('Remote server <b>%s</b> does not exist.') % obj.room_jid)
347
                # group chat does not exist
Annika Sommer's avatar
Annika Sommer committed
348 349
                else:
                    self.handle_gc_error(gc_control, _('Unable to join group chat'),
350
                    _('Group chat <b>%s</b> does not exist.') % obj.room_jid)
351 352
            elif (obj.errcode == '405') or (obj.errcon == 'not-allowed'):
                self.handle_gc_error(gc_control, _('Unable to join group chat'),
353
                    _('Group chat creation is not permitted.'))
354
            elif (obj.errcode == '406') or (obj.errcon == 'not-acceptable'):
355 356 357
                self.handle_gc_error(gc_control, _('Unable to join groupchat'),
                    _('You must use your registered nickname in <b>%s</b>.')\
                    % obj.room_jid)
358 359 360 361 362 363
            elif (obj.errcode == '407') or (obj.errcon == \
            'registration-required'):
                self.handle_gc_error(gc_control, _('Unable to join group chat'),
                    _('You are not in the members list in groupchat %s.') % \
                    obj.room_jid)
            elif (obj.errcode == '409') or (obj.errcon == 'conflict'):
lovetox's avatar
lovetox committed
364
                self.handle_ask_new_nick(obj.conn.name, obj.room_jid, parent_win)
365 366 367
            elif gc_control:
                gc_control.print_conversation('Error %s: %s' % (obj.errcode,
                    obj.errmsg))
368 369
            if gc_control and gc_control.autorejoin:
                gc_control.autorejoin = False
370

371 372
    @staticmethod
    def handle_event_gc_message(obj):
373 374 375 376 377 378 379 380 381
        if not obj.stanza.getTag('body'): # no <body>
            # It could be a voice request. See
            # http://www.xmpp.org/extensions/xep-0045.html#voiceapprove
            if obj.msg_obj.form_node:
                dialogs.SingleMessageWindow(obj.conn.name, obj.fjid,
                    action='receive', from_whom=obj.fjid,
                    subject='', message='', resource='', session=None,
                    form_node=obj.msg_obj.form_node)

382
    def handle_event_presence(self, obj):
383 384 385 386
        # 'NOTIFY' (account, (jid, status, status message, resource,
        # priority, # keyID, timestamp, contact_nickname))
        #
        # Contact changed show
387 388 389 390 391
        account = obj.conn.name
        jid = obj.jid
        show = obj.show
        status = obj.status
        resource = obj.resource or ''
392

393
        jid_list = app.contacts.get_jid_list(account)
394

395 396 397 398 399 400
        # unset custom status
        if (obj.old_show == 0 and obj.new_show > 1) or \
        (obj.old_show > 1 and obj.new_show == 0 and obj.conn.connected > 1):
            if account in self.status_sent_to_users and \
            jid in self.status_sent_to_users[account]:
                del self.status_sent_to_users[account][jid]
401

402
        if app.jid_is_transport(jid):
403 404
            # It must be an agent

405 406 407
            # transport just signed in/out, don't show
            # popup notifications for 30s
            account_jid = account + '/' + jid
408
            app.block_signed_in_notifications[account_jid] = True
Yann Leboulanger's avatar
Yann Leboulanger committed
409 410
            GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications,
                account_jid)
411

412
        highest = app.contacts.get_contact_with_highest_priority(account, jid)
413 414
        is_highest = (highest and highest.resource == resource)

415
        ctrl = self.msg_win_mgr.get_control(jid, account)
416
        if ctrl and ctrl.session and len(obj.contact_list) > 1:
417
            ctrl.remove_session(ctrl.session)
418

419 420 421 422
    def handle_event_msgerror(self, obj):
        #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[session]))
        account = obj.conn.name
        jids = obj.fjid.split('/', 1)
423 424
        jid = jids[0]

425
        session = obj.session
426 427 428 429 430 431 432 433 434 435 436 437 438 439

        gc_control = self.msg_win_mgr.get_gc_control(jid, account)
        if not gc_control and \
        jid in self.minimized_controls[account]:
            gc_control = self.minimized_controls[account][jid]
        if gc_control and gc_control.type_id != message_control.TYPE_GC:
            gc_control = None
        if gc_control:
            if len(jids) > 1: # it's a pm
                nick = jids[1]

                if session:
                    ctrl = session.control
                else:
440
                    ctrl = self.msg_win_mgr.get_control(obj.fjid, account)
441 442 443 444 445 446 447 448 449

                if not ctrl:
                    tv = gc_control.list_treeview
                    model = tv.get_model()
                    iter_ = gc_control.get_contact_iter(nick)
                    if iter_:
                        show = model[iter_][3]
                    else:
                        show = 'offline'
450
                    gc_c = app.contacts.create_gc_contact(room_jid=jid,
451
                        account=account, name=nick, show=show)
452 453
                    ctrl = self.new_private_chat(gc_c, account, session)

454
                ctrl.contact.our_chatstate = False
455
                ctrl.print_conversation(_('Error %(code)s: %(msg)s') % {
456
                    'code': obj.error_code, 'msg': obj.error_msg}, 'status')
457 458 459
                return

            gc_control.print_conversation(_('Error %(code)s: %(msg)s') % {
460
                'code': obj.error_code, 'msg': obj.error_msg}, 'status')
461 462
            if gc_control.parent_win and \
            gc_control.parent_win.get_active_jid() == jid:
463 464 465
                gc_control.set_subject(gc_control.subject)
            return

466
        if app.jid_is_transport(jid):
467
            jid = jid.replace('@', '')
468 469
        msg = obj.error_msg
        if obj.msg:
470
            msg = _('error while sending %(message)s ( %(error)s )') % {
471
                    'message': obj.msg, 'error': msg}
472
        if session:
473
            session.roster_message(jid, msg, obj.time_, msg_type='error')
474

475 476
    @staticmethod
    def handle_event_msgsent(obj):
477 478
        #('MSGSENT', account, (jid, msg, keyID))
        # do not play sound when standalone chatstate message (eg no msg)
479
        if obj.message and app.config.get_per('soundevents', 'message_sent',
480
        'enabled'):
481 482
            helpers.play_sound('message_sent')

483 484
    @staticmethod
    def handle_event_msgnotsent(obj):
485 486
        #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
        msg = _('error while sending %(message)s ( %(error)s )') % {
487 488
                'message': obj.message, 'error': obj.error}
        if not obj.session:
489 490
            # No session. This can happen when sending a message from
            # gajim-remote
491
            log.warning(msg)
492
            return
493 494
        obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
            msg_type='error')
495

496
    def handle_event_subscribe_presence(self, obj):
497
        #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
498
        account = obj.conn.name
499
        if helpers.allow_popup_window(account) or not self.systray_enabled:
500
            if obj.jid in self.instances[account]['sub_request']:
Philipp Hörist's avatar
Philipp Hörist committed
501
                self.instances[account]['sub_request'][obj.jid].destroy()
502 503
            self.instances[account]['sub_request'][obj.jid] = \
                dialogs.SubscriptionRequestWindow(obj.jid, obj.status, account,
504
                obj.user_nick)
505 506
            return

507 508
        event = events.SubscriptionRequestEvent(obj.status, obj.user_nick)
        self.add_event(account, obj.jid, event)
509 510

        if helpers.allow_showing_notification(account):
511 512
            path = gtkgui_helpers.get_icon_path('gajim-subscription_request',
                48)
513
            event_type = _('Subscription request')
514 515
            notify.popup(event_type, obj.jid, account, 'subscription_request',
                path, event_type, obj.jid)
516

517
    def handle_event_subscribed_presence(self, obj):
518
        #('SUBSCRIBED', account, (jid, resource))
519
        account = obj.conn.name
520 521
        if obj.jid in app.contacts.get_jid_list(account):
            c = app.contacts.get_first_contact_from_jid(account, obj.jid)
522
            c.resource = obj.resource
523
            self.roster.remove_contact_from_groups(c.jid, account,
524
                [_('Not in Roster'), _('Observers')], update=False)
525 526
        else:
            keyID = ''
527
            attached_keys = app.config.get_per('accounts', account,
528
                'attached_gpg_keys').split()
529 530 531
            if obj.jid in attached_keys:
                keyID = attached_keys[attached_keys.index(obj.jid) + 1]
            name = obj.jid.split('@', 1)[0]
532
            name = name.split('%', 1)[0]
533
            contact1 = app.contacts.create_contact(jid=obj.jid,
534 535
                account=account, name=name, groups=[], show='online',
                status='online', ask='to', resource=obj.resource, keyID=keyID)
536
            app.contacts.add_contact(account, contact1)
537
            self.roster.add_contact(obj.jid, account)
538
        dialogs.InformationDialog(_('Authorization accepted'),
539
            _('The contact "%s" has authorized you to see his or her status.')
540
            % obj.jid)
541 542 543 544 545 546 547

    def show_unsubscribed_dialog(self, account, contact):
        def on_yes(is_checked, list_):
            self.roster.on_req_usub(None, list_)
        list_ = [(contact, account)]
        dialogs.YesNoDialog(
                _('Contact "%s" removed subscription from you') % contact.jid,
josch's avatar
josch committed
548 549
                _('You will always see them as offline.\nDo you want to '
                        'remove them from your contact list?'),
550 551 552 553
                on_response_yes=(on_yes, list_))
            # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does
            # not show deny

554
    def handle_event_unsubscribed_presence(self, obj):
555
        #('UNSUBSCRIBED', account, jid)
556
        account = obj.conn.name
557
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
558 559 560 561 562 563 564
        if not contact:
            return

        if helpers.allow_popup_window(account) or not self.systray_enabled:
            self.show_unsubscribed_dialog(account, contact)
            return

565 566
        event = events.UnsubscribedEvent(contact)
        self.add_event(account, obj.jid, event)
567 568 569 570

        if helpers.allow_showing_notification(account):
            path = gtkgui_helpers.get_icon_path('gajim-unsubscribed', 48)
            event_type = _('Unsubscribed')
571 572
            notify.popup(event_type, obj.jid, account, 'unsubscribed', path,
                event_type, obj.jid)
573

574 575
    @staticmethod
    def handle_event_register_agent_info(obj):
576 577
        # ('REGISTER_AGENT_INFO', account, (agent, infos, is_form))
        # info in a dataform if is_form is True
578 579 580
        if obj.is_form or 'instructions' in obj.config:
            config.ServiceRegistrationWindow(obj.agent, obj.config,
                obj.conn.name, obj.is_form)
581
        else:
582 583
            dialogs.ErrorDialog(_('Contact with "%s" cannot be established') % \
                obj.agent, _('Check your connection or try again later.'))
584

585 586 587
    def handle_event_gc_config(self, obj):
        #('GC_CONFIG', account, (jid, form_node))  config is a dict
        account = obj.conn.name
588 589
        if obj.jid in app.automatic_rooms[account]:
            if 'continue_tag' in app.automatic_rooms[account][obj.jid]:
590
                # We're converting chat to muc. allow participants to invite
591
                for f in obj.dataform.iter_fields():
592 593 594 595 596 597 598 599
                    if f.var == 'muc#roomconfig_allowinvites':
                        f.value = True
                    elif f.var == 'muc#roomconfig_publicroom':
                        f.value = False
                    elif f.var == 'muc#roomconfig_membersonly':
                        f.value = True
                    elif f.var == 'public_list':
                        f.value = False
600
                obj.conn.send_gc_config(obj.jid, obj.dataform.get_purged())
601
                user_list = {}
602
                for jid in app.automatic_rooms[account][obj.jid]['invities']:
603 604
                    user_list[jid] = {'affiliation': 'member'}
                obj.conn.send_gc_affiliation_list(obj.jid, user_list)
605 606
            else:
                # use default configuration
607
                obj.conn.send_gc_config(obj.jid, obj.form_node)
608 609 610
            # invite contacts
            # check if it is necessary to add <continue />
            continue_tag = False
611
            if 'continue_tag' in app.automatic_rooms[account][obj.jid]:
612
                continue_tag = True
613 614
            if 'invities' in app.automatic_rooms[account][obj.jid]:
                for jid in app.automatic_rooms[account][obj.jid]['invities']:
615
                    obj.conn.send_invite(obj.jid, jid,
616
                        continue_tag=continue_tag)
617 618 619 620 621 622
                    gc_control = self.msg_win_mgr.get_gc_control(obj.jid,
                        account)
                    if gc_control:
                        gc_control.print_conversation(
                            _('%(jid)s has been invited in this room') % {
                            'jid': jid}, graphics=False)
623
            del app.automatic_rooms[account][obj.jid]
624 625 626
        elif obj.jid not in self.instances[account]['gc_config']:
            self.instances[account]['gc_config'][obj.jid] = \
                config.GroupchatConfigWindow(account, obj.jid, obj.dataform)
627

628
    def handle_event_gc_affiliation(self, obj):
629
        #('GC_AFFILIATION', account, (room_jid, users_dict))
630 631 632 633
        account = obj.conn.name
        if obj.jid in self.instances[account]['gc_config']:
            self.instances[account]['gc_config'][obj.jid].\
                affiliation_list_received(obj.users_dict)
634

635 636 637 638
    def handle_event_gc_decline(self, obj):
        account = obj.conn.name
        gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, account)
        if gc_control:
639 640 641 642 643 644 645 646
            if obj.reason:
                gc_control.print_conversation(
                    _('%(jid)s declined the invitation: %(reason)s') % {
                    'jid': obj.jid_from, 'reason': obj.reason}, graphics=False)
            else:
                gc_control.print_conversation(
                    _('%(jid)s declined the invitation') % {
                    'jid': obj.jid_from}, graphics=False)
647

648
    def handle_event_gc_invitation(self, obj):
649
        #('GC_INVITATION', (room_jid, jid_from, reason, password, is_continued))
650
        account = obj.conn.name
651
        if helpers.allow_popup_window(account) or not self.systray_enabled:
652 653 654
            dialogs.InvitationReceivedDialog(account, obj.room_jid,
                obj.jid_from, obj.password, obj.reason,
                is_continued=obj.is_continued)
655 656
            return

657 658 659
        event = events.GcInvitationtEvent(obj.room_jid, obj.reason,
            obj.password, obj.is_continued, obj.jid_from)
        self.add_event(account, obj.jid_from, event)
660 661 662 663

        if helpers.allow_showing_notification(account):
            path = gtkgui_helpers.get_icon_path('gajim-gc_invitation', 48)
            event_type = _('Groupchat Invitation')
664 665
            notify.popup(event_type, obj.jid_from, account, 'gc-invitation',
                path, event_type, obj.room_jid)
666

667 668 669 670 671
    def forget_gpg_passphrase(self, keyid):
        if keyid in self.gpg_passphrase:
            del self.gpg_passphrase[keyid]
        return False

672
    def handle_event_bad_gpg_passphrase(self, obj):
673
        #('BAD_PASSPHRASE', account, ())
674
        if obj.use_gpg_agent:
Yann Leboulanger's avatar
Yann Leboulanger committed
675 676 677
            sectext = _('You configured Gajim to use OpenPGP agent, but there '
                'is no OpenPGP agent running or it returned a wrong passphrase.'
                '\n')
678 679
            sectext += _('You are currently connected without your OpenPGP '
                'key.')
680
            dialogs.WarningDialog(_('Wrong passphrase'), sectext)
681
        else:
682
            path = gtkgui_helpers.get_icon_path('gtk-dialog-warning', 48)
683
            account = obj.conn.name
684
            notify.popup('warning', account, account, '', path,
685
                _('Wrong OpenPGP passphrase'),
686 687
                _('You are currently connected without your OpenPGP key.'))
        self.forget_gpg_passphrase(obj.keyID)
688

689 690
    @staticmethod
    def handle_event_client_cert_passphrase(obj):
691 692 693 694 695 696 697 698 699
        def on_ok(passphrase, checked):
            obj.conn.on_client_cert_passphrase(passphrase, obj.con, obj.port,
                obj.secure_tuple)

        def on_cancel():
            obj.conn.on_client_cert_passphrase('', obj.con, obj.port,
                obj.secure_tuple)

        dialogs.PassphraseDialog(_('Certificate Passphrase Required'),
700
            _('Enter the certificate passphrase for account %s') % \
701 702
            obj.conn.name, ok_handler=on_ok, cancel_handler=on_cancel)

703
    def handle_event_gpg_password_required(self, obj):
704
        #('GPG_PASSWORD_REQUIRED', account, (callback,))
705 706
        if obj.keyid in self.gpg_passphrase:
            request = self.gpg_passphrase[obj.keyid]
707
        else:
708 709 710
            request = PassphraseRequest(obj.keyid)
            self.gpg_passphrase[obj.keyid] = request
        request.add_callback(obj.conn.name, obj.callback)
711

712 713
    @staticmethod
    def handle_event_gpg_trust_key(obj):
714 715 716
        #('GPG_ALWAYS_TRUST', account, callback)
        def on_yes(checked):
            if checked:
717
                obj.conn.gpg.always_trust.append(obj.keyID)
718
            obj.callback(True)
719 720

        def on_no():
721
            obj.callback(False)
722

723
        dialogs.YesNoDialog(_('Untrusted OpenPGP key'), _('The OpenPGP key '
Yann Leboulanger's avatar
Yann Leboulanger committed
724 725
            'used to encrypt this chat is not trusted. Do you really want to '
            'encrypt this message?'), checktext=_('_Do not ask me again'),
726
            on_response_yes=on_yes, on_response_no=on_no)
727

728
    def handle_event_password_required(self, obj):
729
        #('PASSWORD_REQUIRED', account, None)
730
        account = obj.conn.name
731 732 733 734 735 736
        if account in self.pass_dialog:
            return
        text = _('Enter your password for account %s') % account

        def on_ok(passphrase, save):
            if save:
737
                app.config.set_per('accounts', account, 'savepass', True)
738
                passwords.save_password(account, passphrase)
739
            obj.conn.set_password(passphrase)
740 741 742 743 744 745 746 747
            del self.pass_dialog[account]

        def on_cancel():
            self.roster.set_state(account, 'offline')
            self.roster.update_status_combobox()
            del self.pass_dialog[account]

        self.pass_dialog[account] = dialogs.PassphraseDialog(
748 749
            _('Password Required'), text, _('Save password'), ok_handler=on_ok,
            cancel_handler=on_cancel)
750

751 752 753
    def handle_oauth2_credentials(self, obj):
        account = obj.conn.name
        def on_ok(refresh):
754
            app.config.set_per('accounts', account, 'oauth2_refresh_token',
755
                refresh)
756 757
            st = app.config.get_per('accounts', account, 'last_status')
            msg = helpers.from_one_line(app.config.get_per('accounts',
758
                account, 'last_status_msg'))
759
            app.interface.roster.send_status(account, st, msg)
760 761 762
            del self.pass_dialog[account]

        def on_cancel():
763
            app.config.set_per('accounts', account, 'oauth2_refresh_token',
764 765 766 767 768 769 770 771 772 773 774
                '')
            self.roster.set_state(account, 'offline')
            self.roster.update_status_combobox()
            del self.pass_dialog[account]

        instruction = _('Please copy / paste the refresh token from the website'
            ' that has just been opened.')
        self.pass_dialog[account] = dialogs.InputTextDialog(
            _('Oauth2 Credentials'), instruction, is_modal=False,
            ok_handler=on_ok, cancel_handler=on_cancel)

775
    def handle_event_roster_info(self, obj):
776
        #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
777
        account = obj.conn.name
778
        contacts = app.contacts.get_contacts(account, obj.jid)
779 780 781
        if (not obj.sub or obj.sub == 'none') and \
        (not obj.ask or obj.ask == 'none') and not obj.nickname and \
        not obj.groups:
782 783
            # contact removed us.
            if contacts:
784
                self.roster.remove_contact(obj.jid, account, backend=True)
785 786
                return
        elif not contacts:
787
            if obj.sub == 'remove':
788 789
                return
            # Add new contact to roster
790
            keyID = ''
791
            attached_keys = app.config.get_per('accounts', account,
792 793 794
                'attached_gpg_keys').split()
            if obj.jid in attached_keys:
                keyID = attached_keys[attached_keys.index(obj.jid) + 1]
795
            contact = app.contacts.create_contact(jid=obj.jid,
796
                account=account, name=obj.nickname, groups=obj.groups,
Philipp Hörist's avatar
Philipp Hörist committed
797 798
                show='offline', sub=obj.sub, ask=obj.ask, keyID=keyID,
                avatar_sha=obj.avatar_sha)
799
            app.contacts.add_contact(account, contact)
800
            self.roster.add_contact(obj.jid, account)
801 802 803
        else:
            # If contact has changed (sub, ask or group) update roster
            # Mind about observer status changes:
804 805
            #   According to xep 0162, a contact is not an observer anymore when
            #   we asked for auth, so also remove him if ask changed
806
            old_groups = contacts[0].groups
807 808 809 810
            if obj.sub == 'remove':
                # another of our instance removed a contact. Remove it here too
                self.roster.remove_contact(obj.jid, account, backend=True)
                return
811
            update = False
812 813
            if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
            or old_groups != obj.groups:
814
                # c.get_shown_groups() has changed. Reflect that in
815
                # roster_window
816
                self.roster.remove_contact(obj.jid, account, force=True)
817
                update = True
818
            for contact in contacts:
819 820 821 822
                contact.name = obj.nickname or ''
                contact.sub = obj.sub
                contact.ask = obj.ask
                contact.groups = obj.groups or []
823 824 825 826 827 828
            if update:
                self.roster.add_contact(obj.jid, account)
                # Refilter and update old groups
                for group in old_groups:
                    self.roster.draw_group(group, account)
                self.roster.draw_contact(obj.jid, account)
829 830
        if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
        'from', 'both'):
Philipp Hörist's avatar
Philipp Hörist committed
831
            self.instances[account]['sub_request'][obj.jid].destroy()
832

833
    def handle_event_bookmarks(self, obj):
834 835 836 837
        # ('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}])
        # We received a bookmark item from the server (JEP48)
        # Auto join GC windows if neccessary

838
        gui_menu_builder.build_bookmark_menu(obj.conn.name)
839
        invisible_show = app.SHOW_LIST.index('invisible')
840
        # do not autojoin if we are invisible
841
        if obj.conn.connected == invisible_show:
842 843
            return

844
        GLib.idle_add(self.auto_join_bookmarks, obj.conn.name)
845 846 847 848 849

    def handle_event_file_send_error(self, account, array):
        jid = array[0]
        file_props = array[1]
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
850
        ft.set_status(file_props, 'stop')
851 852 853 854 855

        if helpers.allow_popup_window(account):
            ft.show_send_error(file_props)
            return

856 857
        event = events.FileSendErrorEvent(file_props)
        self.add_event(account, jid, event)
858 859 860 861 862

        if helpers.allow_showing_notification(account):
            path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
            event_type = _('File Transfer Error')
            notify.popup(event_type, jid, account, 'file-send-error', path,
zimio's avatar
zimio committed
863
                event_type, file_props.name)
864

865
    def handle_event_file_request_error(self, obj):
866 867
        # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
868 869
        ft.set_status(obj.file_props, 'stop')
        errno = obj.file_props.error
870

871
        if helpers.allow_popup_window(obj.conn.name):
872
            if errno in (-4, -5):
873
                ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
874
            else:
875
                ft.show_request_error(obj.file_props)
876 877 878
            return

        if errno in (-4, -5):
879
            event_class = events.FileErrorEvent
880 881
            msg_type = 'file-error'
        else:
882
            event_class = events.FileRequestErrorEvent
883 884
            msg_type = 'file-request-error'

885 886
        event = event_class(obj.file_props)
        self.add_event(obj.conn.name, obj.jid, event)
887

888
        if helpers.allow_showing_notification(obj.conn.name):
889 890 891
            # check if we should be notified
            path = gtkgui_helpers.get_icon_path('gajim-ft_error', 48)
            event_type = _('File Transfer Error')
892
            notify.popup(event_type, obj.jid, obj.conn.name, msg_type, path,
zimio's avatar
zimio committed
893
                title=event_type, text=obj.file_props.name)
894

895 896
    def handle_event_file_request(self, obj):
        account = obj.conn.name
897
        if obj.jid not in app.contacts.get_jid_list(account):
898
            keyID = ''
899
            attached_keys = app.config.get_per('accounts', account,
900 901 902
                'attached_gpg_keys').split()
            if obj.jid in attached_keys:
                keyID = attached_keys[attached_keys.index(obj.jid) + 1]
903
            contact = app.contacts.create_not_in_roster_contact(jid=obj.jid,
904
                account=account, keyID=keyID)
905
            app.contacts.add_contact(account, contact)
906
            self.roster.add_contact(obj.jid, account)
907
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
908 909 910 911
        if obj.file_props.session_type == 'jingle':
            request = obj.stanza.getTag('jingle').getTag('content')\
                        .getTag('description').getTag('request')
            if request:
912
                # If we get a request instead
913 914
                ft_win = self.instances['file_transfers']
                ft_win.add_transfer(account, contact, obj.file_props)
915
                return
916 917
        if helpers.allow_popup_window(account):
            self.instances['file_transfers'].show_file_request(account, contact,
918
                obj.file_props)
919
            return
920 921
        event = events.FileRequestEvent(obj.file_props)
        self.add_event(account, obj.jid, event)
922 923
        if helpers.allow_showing_notification(account):
            path = gtkgui_helpers.get_icon_path('gajim-ft_request', 48)
924
            txt = _('%s wants to send you a file.') % app.get_name_from_jid(