gui_interface.py 85.3 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
2
3
4
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
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# 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/>.

steve-e's avatar
steve-e committed
34
35
36
37
import os
import sys
import re
import time
38
import json
39
import logging
Philipp Hörist's avatar
Philipp Hörist committed
40
from functools import partial
41
from threading import Thread
42
from datetime import datetime
43
from importlib.util import find_spec
44
from packaging.version import Version as V
steve-e's avatar
steve-e committed
45

46
from gi.repository import Gtk
Yann Leboulanger's avatar
Yann Leboulanger committed
47
from gi.repository import GLib
48
from gi.repository import Gio
49
from gi.repository import Soup
50
51
from nbxmpp import idlequeue
from nbxmpp import Hashes2
52

53
from gajim.common import app
André's avatar
André committed
54
from gajim.common import events
Philipp Hörist's avatar
Philipp Hörist committed
55
from gajim.common.dbus import location
56
from gajim.common.dbus import logind
André's avatar
André committed
57
from gajim.common.dbus import music_track
André's avatar
André committed
58

André's avatar
André committed
59
60
from gajim import gui_menu_builder
from gajim import dialogs
61
from gajim.dialog_messages import get_dialog
62

André's avatar
André committed
63
64
65
from gajim.chat_control_base import ChatControlBase
from gajim.chat_control import ChatControl
from gajim.groupchat_control import GroupchatControl
66
from gajim.privatechat_control import PrivateChatControl
André's avatar
André committed
67
from gajim.message_window import MessageWindowMgr
steve-e's avatar
steve-e committed
68

André's avatar
André committed
69
from gajim.session import ChatControlSession
steve-e's avatar
steve-e committed
70

Philipp Hörist's avatar
Philipp Hörist committed
71
from gajim.common import idle
André's avatar
André committed
72
73
74
75
76
77
from gajim.common.zeroconf import connection_zeroconf
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
78
from gajim.common.helpers import ask_for_status_message
Philipp Hörist's avatar
Philipp Hörist committed
79
from gajim.common.structs import MUCData
80
from gajim.common.nec import NetworkEvent
81
from gajim.common.i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
82
from gajim.common.client import Client
83
from gajim.common.const import Display
Philipp Hörist's avatar
Philipp Hörist committed
84

André's avatar
André committed
85
86
87
88
from gajim.common.file_props import FilesProp

from gajim import roster_window
from gajim.common import ged
89
90
from gajim.common import configpaths
from gajim.common import optparser
steve-e's avatar
steve-e committed
91

Philipp Hörist's avatar
Philipp Hörist committed
92
from gajim.gtk.avatar import AvatarStorage
Philipp Hörist's avatar
Philipp Hörist committed
93
from gajim.gtk.notification import Notification
Daniel Brötzmann's avatar
Daniel Brötzmann committed
94
from gajim.gtk.dialogs import DialogButton
95
96
97
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import WarningDialog
from gajim.gtk.dialogs import InformationDialog
98
from gajim.gtk.dialogs import NewConfirmationDialog
99
from gajim.gtk.dialogs import NewConfirmationCheckDialog
100
from gajim.gtk.dialogs import InputDialog
Daniel Brötzmann's avatar
Daniel Brötzmann committed
101
from gajim.gtk.dialogs import PassphraseDialog
102
from gajim.gtk.dialogs import InvitationReceivedDialog
103
104
105
from gajim.gtk.filechoosers import FileChooserDialog
from gajim.gtk.emoji_data import emoji_data
from gajim.gtk.emoji_data import emoji_ascii_data
106
from gajim.gtk.filetransfer import FileTransfersWindow
Philipp Hörist's avatar
Philipp Hörist committed
107
from gajim.gtk.filetransfer_progress import FileTransferProgress
108
from gajim.gtk.roster_item_exchange import RosterItemExchangeWindow
Philipp Hörist's avatar
Philipp Hörist committed
109
110
from gajim.gtk.util import get_show_in_roster
from gajim.gtk.util import get_show_in_systray
Philipp Hörist's avatar
Philipp Hörist committed
111
from gajim.gtk.util import open_window
112
from gajim.gtk.util import get_app_window
113
from gajim.gtk.util import get_app_windows
114
from gajim.gtk.util import get_color_for_account
115
from gajim.gtk.const import ControlType
116

steve-e's avatar
steve-e committed
117

Philipp Hörist's avatar
Philipp Hörist committed
118
parser = optparser.OptionsParser(configpaths.get('CONFIG_FILE'))
steve-e's avatar
steve-e committed
119
120
121
122
123
124
125
126
log = logging.getLogger('gajim.interface')

class Interface:

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

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

136
137
    @staticmethod
    def handle_event_information(obj):
138
139
140
141
142
143
144
145
        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':
146
            cls = ErrorDialog
147
        elif obj.level == 'warn':
148
            cls = WarningDialog
149
        elif obj.level == 'info':
150
            cls = InformationDialog
151
152
153
154
        else:
            return

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

156
157
158
159
    @staticmethod
    def raise_dialog(name, *args, **kwargs):
        get_dialog(name, *args, **kwargs)

160
161
    @staticmethod
    def handle_event_http_auth(obj):
162
163
        # ('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
        def _response(account, answer):
164
165
            obj.conn.get_module('HTTPAuth').build_http_auth_answer(
                obj.stanza, answer)
166

167
        account = obj.conn.name
168
        message = _('HTTP (%(method)s) Authorization '
169
170
171
172
                    'for %(url)s (ID: %(id)s)') % {
                        'method': obj.method,
                        'url': obj.url,
                        'id': obj.iq_id}
173
        sec_msg = _('Do you accept this request?')
174
        if app.get_number_of_connected_accounts() > 1:
175
            sec_msg = _('Do you accept this request (account: %s)?') % account
176
177
        if obj.msg:
            sec_msg = obj.msg + '\n' + sec_msg
178
        message = message + '\n' + sec_msg
179
180

        NewConfirmationDialog(
181
182
183
            _('Authorization Request'),
            _('HTTP Authorization Request'),
            message,
184
185
186
            [DialogButton.make('Cancel',
                               text=_('_No'),
                               callback=_response,
187
                               args=[obj, 'no']),
188
             DialogButton.make('Accept',
189
                               callback=_response,
190
                               args=[obj, 'yes'])]).show()
191

Philipp Hörist's avatar
Philipp Hörist committed
192
193
194
    def handle_event_iq_error(self, event):
        ctrl = self.msg_win_mgr.get_control(event.properties.jid.getBare(),
                                            event.account)
195
        if ctrl and ctrl.is_groupchat:
196
            ctrl.add_info_message('Error: %s' % event.properties.error)
197

198
199
    @staticmethod
    def handle_event_connection_lost(obj):
200
        # ('CONNECTION_LOST', account, [title, text])
201
        account = obj.conn.name
Philipp Hörist's avatar
Philipp Hörist committed
202
203
204
        app.notification.popup(
            _('Connection Failed'), account, account,
            'connection-lost', 'gajim-connection_lost', obj.title, obj.msg)
205

206
207
    @staticmethod
    def unblock_signed_in_notifications(account):
208
        app.block_signed_in_notifications[account] = False
209

210
211
    def handle_event_status(self, event):
        if event.show in ('offline', 'error'):
Philipp Hörist's avatar
Philipp Hörist committed
212
213
214
            # TODO: Close all account windows
            pass

215
216
        if event.show == 'offline':
            app.block_signed_in_notifications[event.account] = True
217
218
219
220
221
        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
222
223
224
225
            GLib.timeout_add_seconds(30,
                                     self.unblock_signed_in_notifications,
                                     event.account)

226
    def handle_event_presence(self, obj):
227
        # 'NOTIFY' (account, (jid, status, status message, resource,
Philipp Hörist's avatar
Philipp Hörist committed
228
        # priority, timestamp))
229
230
        #
        # Contact changed show
231
232
        account = obj.conn.name
        jid = obj.jid
233

234
        if app.jid_is_transport(jid):
235
236
            # It must be an agent

237
238
239
            # transport just signed in/out, don't show
            # popup notifications for 30s
            account_jid = account + '/' + jid
240
            app.block_signed_in_notifications[account_jid] = True
Yann Leboulanger's avatar
Yann Leboulanger committed
241
242
            GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications,
                account_jid)
243

244
        ctrl = self.msg_win_mgr.get_control(jid, account)
245
        if ctrl and ctrl.session and len(obj.contact_list) > 1:
246
            ctrl.remove_session(ctrl.session)
247

248
249
    @staticmethod
    def handle_event_msgsent(obj):
250
251
252
        # ('MSGSENT', account, (jid, msg))
        # Do not play sound if it is a standalone chatstate message (eg no msg)
        # or if it is a message to more than one recipient
253
        if obj.message and app.config.get_per('soundevents', 'message_sent',
254
        'enabled'):
255
256
            if isinstance(obj.jid, list) and len(obj.jid) > 1:
                return
257
258
            helpers.play_sound('message_sent')

259
260
    @staticmethod
    def handle_event_msgnotsent(obj):
261
262
        #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
        msg = _('error while sending %(message)s ( %(error)s )') % {
263
264
                'message': obj.message, 'error': obj.error}
        if not obj.session:
265
266
            # No session. This can happen when sending a message from
            # gajim-remote
267
            log.warning(msg)
268
            return
269
270
        obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
            msg_type='error')
271

272
    def handle_event_subscribe_presence(self, obj):
273
        #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
274
        account = obj.conn.name
275
        if helpers.allow_popup_window(account) or not self.systray_enabled:
276
277
278
279
280
            open_window('SubscriptionRequest',
                        account=account,
                        jid=obj.jid,
                        text=obj.status,
                        user_nick=obj.user_nick)
281
282
            return

283
284
        event = events.SubscriptionRequestEvent(obj.status, obj.user_nick)
        self.add_event(account, obj.jid, event)
285
286
287

        if helpers.allow_showing_notification(account):
            event_type = _('Subscription request')
Philipp Hörist's avatar
Philipp Hörist committed
288
289
290
            app.notification.popup(
                event_type, obj.jid, account, 'subscription_request',
                'gajim-subscription_request', event_type, obj.jid)
291

292
293
294
295
296
297
298
299
300
301
302
303
    def handle_event_subscribed_presence(self, event):
        bare_jid = event.jid.getBare()
        resource = event.jid.getResource()
        if bare_jid in app.contacts.get_jid_list(event.account):
            contact = app.contacts.get_first_contact_from_jid(event.account,
                                                              bare_jid)
            contact.resource = resource
            self.roster.remove_contact_from_groups(contact.jid,
                                                   event.account,
                                                   [_('Not in contact list'),
                                                    _('Observers')],
                                                   update=False)
304
        else:
305
            name = event.jid.getNode()
306
            name = name.split('%', 1)[0]
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
            contact = app.contacts.create_contact(jid=bare_jid,
                                                  account=event.account,
                                                  name=name,
                                                  groups=[],
                                                  show='online',
                                                  status='online',
                                                  ask='to',
                                                  resource=resource)
            app.contacts.add_contact(event.account, contact)
            self.roster.add_contact(bare_jid, event.account)

        app.notification.popup(
            None,
            bare_jid,
            event.account,
            title=_('Authorization accepted'),
            text=_('The contact "%s" has authorized you'
                   ' to see their status.') % event.jid)
325
326

    def show_unsubscribed_dialog(self, account, contact):
327
328
        def _remove():
            self.roster.on_req_usub(None, [(contact, account)])
329

330
331
        name = contact.get_shown_name()
        jid = contact.jid
332
333
        NewConfirmationDialog(
            _('Subscription Removed'),
Yann Leboulanger's avatar
Yann Leboulanger committed
334
335
            _('%(name)s (%(jid)s) has removed subscription from you') % {
                'name': name, 'jid': jid},
336
337
338
339
340
            _('You will always see this contact as offline.\n'
              'Do you want to remove them from your contact list?'),
            [DialogButton.make('Cancel',
                               text=_('_No')),
             DialogButton.make('Remove',
341
                               callback=_remove)]).show()
342
343
344

        # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does
        # not show deny
345

346
    def handle_event_unsubscribed_presence(self, obj):
347
        #('UNSUBSCRIBED', account, jid)
348
        account = obj.conn.name
349
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
350
351
352
353
354
355
356
        if not contact:
            return

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

357
358
        event = events.UnsubscribedEvent(contact)
        self.add_event(account, obj.jid, event)
359
360
361

        if helpers.allow_showing_notification(account):
            event_type = _('Unsubscribed')
Philipp Hörist's avatar
Philipp Hörist committed
362
363
364
            app.notification.popup(
                event_type, obj.jid, account,
                'unsubscribed', 'gajim-unsubscribed',
365
                event_type, obj.jid)
366

Philipp Hörist's avatar
Philipp Hörist committed
367
368
369
    def handle_event_gc_decline(self, event):
        gc_control = self.msg_win_mgr.get_gc_control(str(event.muc),
                                                     event.account)
370
        if gc_control:
Philipp Hörist's avatar
Philipp Hörist committed
371
            if event.reason:
372
                gc_control.add_info_message(
373
                    _('%(jid)s declined the invitation: %(reason)s') % {
374
                        'jid': event.from_, 'reason': event.reason})
375
            else:
376
                gc_control.add_info_message(
377
                    _('%(jid)s declined the invitation') % {
378
                        'jid': event.from_})
Philipp Hörist's avatar
Philipp Hörist committed
379
380
381

    def handle_event_gc_invitation(self, event):
        if helpers.allow_popup_window(event.account) or not self.systray_enabled:
382
            InvitationReceivedDialog(event.account, event)
383
384
            return

Philipp Hörist's avatar
Philipp Hörist committed
385
386
387
388
389
        from_ = str(event.from_)
        muc = str(event.muc)

        event_ = events.GcInvitationtEvent(event)
        self.add_event(event.account, from_, event_)
390

Philipp Hörist's avatar
Philipp Hörist committed
391
        if helpers.allow_showing_notification(event.account):
392
            event_type = _('Group Chat Invitation')
Philipp Hörist's avatar
Philipp Hörist committed
393
394
395
396
397
398
399
400
401
402
            text = _('You are invited to {room} by {user}').format(room=muc,
                                                                   user=from_)
            app.notification.popup(event_type,
                                   from_,
                                   event.account,
                                   'gc-invitation',
                                   'gajim-gc_invitation',
                                   event_type,
                                   text,
                                   room_jid=muc)
403

404
405
    @staticmethod
    def handle_event_client_cert_passphrase(obj):
406
407
408
409
410
411
412
413
        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)

Daniel Brötzmann's avatar
Daniel Brötzmann committed
414
415
416
417
        PassphraseDialog(_('Certificate Passphrase Required'),
                         _('Enter the certificate passphrase for account %s') % \
                         obj.conn.name, ok_handler=on_ok,
                         cancel_handler=on_cancel)
418

419
    def handle_event_password_required(self, obj):
420
        #('PASSWORD_REQUIRED', account, None)
421
        account = obj.conn.name
422
423
424
425
426
        if account in self.pass_dialog:
            return
        text = _('Enter your password for account %s') % account

        def on_ok(passphrase, save):
Philipp Hörist's avatar
Philipp Hörist committed
427
428
429
            app.config.set_per('accounts', account, 'savepass', save)
            passwords.save_password(account, passphrase)
            obj.on_password(passphrase)
430
431
432
433
434
            del self.pass_dialog[account]

        def on_cancel():
            del self.pass_dialog[account]

Daniel Brötzmann's avatar
Daniel Brötzmann committed
435
        self.pass_dialog[account] = PassphraseDialog(
436
437
            _('Password Required'), text, _('Save password'), ok_handler=on_ok,
            cancel_handler=on_cancel)
438

439
    def handle_event_roster_info(self, obj):
440
        #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
441
        account = obj.conn.name
442
        contacts = app.contacts.get_contacts(account, obj.jid)
443
444
445
        if (not obj.sub or obj.sub == 'none') and \
        (not obj.ask or obj.ask == 'none') and not obj.nickname and \
        not obj.groups:
446
447
            # contact removed us.
            if contacts:
448
                self.roster.remove_contact(obj.jid, account, backend=True)
449
450
                return
        elif not contacts:
451
            if obj.sub == 'remove':
452
453
                return
            # Add new contact to roster
Philipp Hörist's avatar
Philipp Hörist committed
454

455
            contact = app.contacts.create_contact(jid=obj.jid,
456
                account=account, name=obj.nickname, groups=obj.groups,
Philipp Hörist's avatar
Philipp Hörist committed
457
                show='offline', sub=obj.sub, ask=obj.ask,
Philipp Hörist's avatar
Philipp Hörist committed
458
                avatar_sha=obj.avatar_sha)
459
            app.contacts.add_contact(account, contact)
460
            self.roster.add_contact(obj.jid, account)
461
462
463
        else:
            # If contact has changed (sub, ask or group) update roster
            # Mind about observer status changes:
464
465
            #   According to xep 0162, a contact is not an observer anymore when
            #   we asked for auth, so also remove him if ask changed
466
            old_groups = contacts[0].groups
467
468
469
470
            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
471
            update = False
472
473
            if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
            or old_groups != obj.groups:
474
                # c.get_shown_groups() has changed. Reflect that in
475
                # roster_window
476
                self.roster.remove_contact(obj.jid, account, force=True)
477
                update = True
478
            for contact in contacts:
479
480
481
482
                contact.name = obj.nickname or ''
                contact.sub = obj.sub
                contact.ask = obj.ask
                contact.groups = obj.groups or []
483
484
485
486
487
488
            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)
489
490
        if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
        'from', 'both'):
Philipp Hörist's avatar
Philipp Hörist committed
491
            self.instances[account]['sub_request'][obj.jid].destroy()
492

Philipp Hörist's avatar
Philipp Hörist committed
493
    def handle_event_file_send_error(self, event):
494
        ft = self.instances['file_transfers']
Philipp Hörist's avatar
Philipp Hörist committed
495
        ft.set_status(event.file_props, 'stop')
496

Philipp Hörist's avatar
Philipp Hörist committed
497
498
        if helpers.allow_popup_window(event.account):
            ft.show_send_error(event.file_props)
499
500
            return

Philipp Hörist's avatar
Philipp Hörist committed
501
502
        event = events.FileSendErrorEvent(event.file_props)
        self.add_event(event.account, event.jid, event)
503

Philipp Hörist's avatar
Philipp Hörist committed
504
        if helpers.allow_showing_notification(event.account):
505
            event_type = _('File Transfer Error')
Philipp Hörist's avatar
Philipp Hörist committed
506
            app.notification.popup(
Philipp Hörist's avatar
Philipp Hörist committed
507
                event_type, event.jid, event.account,
508
                'file-send-error', 'dialog-error',
Philipp Hörist's avatar
Philipp Hörist committed
509
                event_type, event.file_props.name)
510

511
    def handle_event_file_request_error(self, obj):
512
513
        # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
514
515
        ft.set_status(obj.file_props, 'stop')
        errno = obj.file_props.error
516

517
        if helpers.allow_popup_window(obj.conn.name):
518
            if errno in (-4, -5):
519
                ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
520
            else:
521
                ft.show_request_error(obj.file_props)
522
523
524
            return

        if errno in (-4, -5):
525
            event_class = events.FileErrorEvent
526
527
            msg_type = 'file-error'
        else:
528
            event_class = events.FileRequestErrorEvent
529
530
            msg_type = 'file-request-error'

531
532
        event = event_class(obj.file_props)
        self.add_event(obj.conn.name, obj.jid, event)
533

534
        if helpers.allow_showing_notification(obj.conn.name):
535
            # Check if we should be notified
536
            event_type = _('File Transfer Error')
Philipp Hörist's avatar
Philipp Hörist committed
537
            app.notification.popup(
538
539
540
541
542
543
544
                event_type,
                obj.jid,
                obj.conn.name,
                msg_type,
                'dialog-error',
                title=event_type,
                text=obj.file_props.name)
545

546
547
    def handle_event_file_request(self, obj):
        account = obj.conn.name
548
        if obj.jid not in app.contacts.get_jid_list(account):
549
550
            contact = app.contacts.create_not_in_roster_contact(
                jid=obj.jid, account=account)
551
            app.contacts.add_contact(account, contact)
552
            self.roster.add_contact(obj.jid, account)
553
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
554
        if obj.file_props.session_type == 'jingle':
555
556
557
            request = \
                obj.stanza.getTag('jingle').getTag('content').getTag(
                    'description').getTag('request')
558
            if request:
559
                # If we get a request instead
560
561
                ft_win = self.instances['file_transfers']
                ft_win.add_transfer(account, contact, obj.file_props)
562
                return
563
        if helpers.allow_popup_window(account):
564
565
            self.instances['file_transfers'].show_file_request(
                account, contact, obj.file_props)
566
            return
567
568
        event = events.FileRequestEvent(obj.file_props)
        self.add_event(account, obj.jid, event)
569
        if helpers.allow_showing_notification(account):
570
            txt = _('%s wants to send you a file.') % app.get_name_from_jid(
571
                account, obj.jid)
572
            event_type = _('File Transfer Request')
Philipp Hörist's avatar
Philipp Hörist committed
573
            app.notification.popup(
574
575
576
577
578
579
580
                event_type,
                obj.jid,
                account,
                'file-request',
                icon_name='document-send',
                title=event_type,
                text=txt)
581

582
583
    @staticmethod
    def handle_event_file_error(title, message):
584
        ErrorDialog(title, message)
585
586
587

    def handle_event_file_progress(self, account, file_props):
        if time.time() - self.last_ftwindow_update > 0.5:
588
            # Update ft window every 500ms
589
            self.last_ftwindow_update = time.time()
590
591
            self.instances['file_transfers'].set_progress(
                file_props.type_, file_props.sid, file_props.received_len)
Yann Leboulanger's avatar
Yann Leboulanger committed
592

593
    def __compare_hashes(self, account, file_props):
594
595
        session = app.connections[account].get_module(
            'Jingle').get_jingle_session(jid=None, sid=file_props.sid)
596
        ft_win = self.instances['file_transfers']
597
        h = Hashes2()
598
        try:
599
            file_ = open(file_props.file_name, 'rb')
600
        except Exception:
601
            return
zimio's avatar
zimio committed
602
        hash_ = h.calculateHash(file_props.algo, file_)
Yann Leboulanger's avatar
Yann Leboulanger committed
603
        file_.close()
604
605
        # If the hash we received and the hash of the file are the same,
        # then the file is not corrupt
Yann Leboulanger's avatar
Yann Leboulanger committed
606
        jid = file_props.sender
zimio's avatar
zimio committed
607
        if file_props.hash_ == hash_:
Yann Leboulanger's avatar
Yann Leboulanger committed
608
609
            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
            GLib.idle_add(ft_win.set_status, file_props, 'ok')
610
        else:
611
            # Wrong hash, we need to get the file again!
zimio's avatar
zimio committed
612
            file_props.error = -10
Yann Leboulanger's avatar
Yann Leboulanger committed
613
614
            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
            GLib.idle_add(ft_win.set_status, file_props, 'hash_error')
615
616
617
        # End jingle session
        if session:
            session.end_session()
618
619
620

    def handle_event_file_rcv_completed(self, account, file_props):
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
621
        if file_props.error == 0:
622
623
            ft.set_progress(
                file_props.type_, file_props.sid, file_props.received_len)
624
            jid = app.get_jid_without_resource(str(file_props.receiver))
625
            app.nec.push_incoming_event(
626
627
628
629
                NetworkEvent('file-transfer-completed',
                             file_props=file_props,
                             jid=jid))

630
        else:
zimio's avatar
zimio committed
631
            ft.set_status(file_props, 'stop')
632
633
        if not file_props.completed and (file_props.stalled or
                file_props.paused):
634
            return
Yann Leboulanger's avatar
Yann Leboulanger committed
635

636
        if file_props.type_ == 'r':  # We receive a file
637
            app.socks5queue.remove_receiver(file_props.sid, True, True)
638
            if file_props.session_type == 'jingle':
639
                if file_props.hash_ and file_props.error == 0:
640
641
                    # We compare hashes in a new thread
                    self.hashThread = Thread(target=self.__compare_hashes,
642
                                             args=(account, file_props))
643
644
                    self.hashThread.start()
                else:
645
646
                    # We didn't get the hash, sender probably doesn't
                    # support that
647
648
                    jid = file_props.sender
                    self.popup_ft_result(account, jid, file_props)
649
650
                    if file_props.error == 0:
                        ft.set_status(file_props, 'ok')
651
652
653
654
                    session = \
                        app.connections[account].get_module(
                            'Jingle').get_jingle_session(jid=None,
                                                         sid=file_props.sid)
655
                    # End jingle session
656
657
                    # TODO: Only if there are no other parallel downloads in
                    # this session
658
659
                    if session:
                        session.end_session()
660
        else:  # We send a file
Yann Leboulanger's avatar
Yann Leboulanger committed
661
            jid = file_props.receiver
662
            app.socks5queue.remove_sender(file_props.sid, True, True)
663
            self.popup_ft_result(account, jid, file_props)
664

665
666
    def popup_ft_result(self, account, jid, file_props):
        ft = self.instances['file_transfers']
667
        if helpers.allow_popup_window(account):
zimio's avatar
zimio committed
668
            if file_props.error == 0:
669
                if app.config.get('notify_on_file_complete'):
670
                    ft.show_completed(jid, file_props)
zimio's avatar
zimio committed
671
            elif file_props.error == -1:
672
673
674
675
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('Remote Contact Stopped Transfer'))
zimio's avatar
zimio committed
676
            elif file_props.error == -6:
677
678
679
680
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('Error Opening File'))
zimio's avatar
zimio committed
681
            elif file_props.error == -10:
682
683
684
685
                ft.show_hash_error(
                    jid,
                    file_props,
                    account)
686
            elif file_props.error == -12:
687
688
689
690
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('SSL Certificate Error'))
691
692
693
694
            return

        msg_type = ''
        event_type = ''
695
696
        if (file_props.error == 0 and
                app.config.get('notify_on_file_complete')):
697
            event_class = events.FileCompletedEvent
698
699
            msg_type = 'file-completed'
            event_type = _('File Transfer Completed')
zimio's avatar
zimio committed
700
        elif file_props.error in (-1, -6):
701
            event_class = events.FileStoppedEvent
702
703
            msg_type = 'file-stopped'
            event_type = _('File Transfer Stopped')
704
        elif file_props.error == -10:
705
            event_class = events.FileHashErrorEvent
706
707
            msg_type = 'file-hash-error'
            event_type = _('File Transfer Failed')
Yann Leboulanger's avatar
Yann Leboulanger committed
708

709
710
        if event_type == '':
            # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
711
712
713
714
            # this should never happen but it does. see process_result() in
            # socks5.py
            # who calls this func (sth is really wrong unless this func is also
            # registered as progress_cb
715
716
717
            return

        if msg_type:
718
719
            event = event_class(file_props)
            self.add_event(account, jid, event)
720
721

        if file_props is not None:
zimio's avatar
zimio committed
722
            if file_props.type_ == 'r':
723
                # Get the name of the sender, as it is in the roster
Yann Leboulanger's avatar
Yann Leboulanger committed
724
                sender = file_props.sender.split('/')[0]
725
726
                name = app.contacts.get_first_contact_from_jid(
                    account, sender).get_shown_name()
zimio's avatar
zimio committed
727
                filename = os.path.basename(file_props.file_name)
728

729
                if event_type == _('File Transfer Completed'):
730
731
732
                    txt = _('%(filename)s received from %(name)s.') % {
                        'filename': filename,
                        'name': name}
733
                    icon_name = 'emblem-default'
734
                elif event_type == _('File Transfer Stopped'):
735
                    txt = _('File transfer of %(filename)s from %(name)s '
736
737
738
                            'stopped.') % {
                                'filename': filename,
                                'name': name}
739
                    icon_name = 'process-stop'
740
                else: # File transfer hash error
741
                    txt = _('File transfer of %(filename)s from %(name)s '
742
743
744
                            'failed.') % {
                                'filename': filename,
                                'name': name}
745
                    icon_name = 'process-stop'
746
            else:
zimio's avatar
zimio committed
747
                receiver = file_props.receiver
748
749
750
                if hasattr(receiver, 'jid'):
                    receiver = receiver.jid
                receiver = receiver.split('/')[0]
751
752
753
                # Get the name of the contact, as it is in the roster
                name = app.contacts.get_first_contact_from_jid(
                    account, receiver).get_shown_name()
zimio's avatar
zimio committed
754
                filename = os.path.basename(file_props.file_name)
755
                if event_type == _('File Transfer Completed'):
756
757
758
759
                    txt = _('You successfully sent %(filename)s to '
                            '%(name)s.') % {
                                'filename': filename,
                                'name': name}
760
                    icon_name = 'emblem-default'
761
                elif event_type == _('File Transfer Stopped'):
762
                    txt = _('File transfer of %(filename)s to %(name)s '
763
764
765
                            'stopped.') % {
                                'filename': filename,
                                'name': name}
766
                    icon_name = 'process-stop'
767
                else: # File transfer hash error
768
                    txt = _('File transfer of %(filename)s to %(name)s '
769
770
771
                            'failed.') % {
                                'filename': filename,
                                'name': name}
772
                    icon_name = 'process-stop'
773
774
        else:
            txt = ''
Philipp Hörist's avatar
Philipp Hörist committed
775
            icon_name = None
776

777
778
        if (app.config.get('notify_on_file_complete') and
                (app.config.get('autopopupaway') or
Philipp Hörist's avatar
Philipp Hörist committed
779
                app.connections[account].status in ('online', 'chat'))):
780
781
            # We want to be notified and we are online/chat or we don't mind
            # to be bugged when away/na/busy
Philipp Hörist's avatar
Philipp Hörist committed
782
            app.notification.popup(
783
784
785
786
787
788
789
                event_type,
                jid,
                account,
                msg_type,
                icon_name=icon_name,
                title=event_type,
                text=txt)
790

791
    def handle_event_signed_in(self, obj):
792
793
794
795
796
        """
        SIGNED_IN event is emitted when we sign in, so handle it
        """
        # ('SIGNED_IN', account, ())
        # block signed in notifications for 30 seconds
797
798
799

        # Add our own JID into the DB
        app.logger.insert_jid(obj.conn.get_own_jid().getStripped())
800
        account = obj.conn.name
801
        app.block_signed_in_notifications[account] = True
Philipp Hörist's avatar
Philipp Hörist committed
802

803
804
        pep_supported = obj.conn.get_module('PEP').supported

Philipp Hörist's avatar
Philipp Hörist committed
805
        if obj.conn.get_module('MAM').available:
Philipp Hörist's avatar
Philipp Hörist committed
806
            obj.conn.get_module('MAM').request_archive_on_signin()
807

808
        # enable location listener
809
        if (pep_supported and app.is_installed('GEOCLUE') and
André's avatar
André committed
810
                app.config.get_per('accounts', account, 'publish_location')):
Philipp Hörist's avatar
Philipp Hörist committed
811
            location.enable()
812

Philipp Hörist's avatar
Philipp Hörist committed
813
814
815
        if ask_for_status_message(obj.conn.status, signin=True):
            open_window('StatusChange', status=obj.conn.status)

816
    @staticmethod
Philipp Hörist's avatar
Philipp Hörist committed
817
    def show_httpupload_progress(transfer):
Philipp Hörist's avatar
Philipp Hörist committed
818
        FileTransferProgress(transfer)
819
820

    def send_httpupload(self, chat_control):
Philipp Hörist's avatar
Philipp Hörist committed
821
822
823
824
        accept_cb = partial(self.on_file_dialog_ok, chat_control)
        FileChooserDialog(accept_cb,
                          select_multiple=True,
                          transient_for=chat_control.parent_win.window)
825
826

    @staticmethod
Philipp Hörist's avatar
Philipp Hörist committed
827
    def on_file_dialog_ok(chat_control, paths):
828
        con = app.connections[chat_control.account]
Philipp Hörist's avatar
Philipp Hörist committed
829
        for path in paths:
Philipp Hörist's avatar