gui_interface.py 84.4 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
79
from gajim.common.helpers import get_group_chat_nick
Philipp Hörist's avatar
Philipp Hörist committed
80
from gajim.common.structs import MUCData
81
from gajim.common.nec import NetworkEvent
82
from gajim.common.i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
83
from gajim.common.client import Client
Philipp Hörist's avatar
Philipp Hörist committed
84
from gajim.common.settings import Settings
85
from gajim.common.const import Display
Philipp Hörist's avatar
Philipp Hörist committed
86

André's avatar
André committed
87
88
89
90
from gajim.common.file_props import FilesProp

from gajim import roster_window
from gajim.common import ged
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
Philipp Hörist's avatar
Philipp Hörist committed
98
99
from gajim.gtk.dialogs import ConfirmationDialog
from gajim.gtk.dialogs import ConfirmationCheckDialog
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
118
119
120
121
122
123
124
125

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

class Interface:

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

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

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

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

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

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

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

Philipp Hörist's avatar
Philipp Hörist committed
179
        ConfirmationDialog(
180
181
182
            _('Authorization Request'),
            _('HTTP Authorization Request'),
            message,
183
184
185
            [DialogButton.make('Cancel',
                               text=_('_No'),
                               callback=_response,
186
                               args=[obj, 'no']),
187
             DialogButton.make('Accept',
188
                               callback=_response,
189
                               args=[obj, 'yes'])]).show()
190

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

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

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

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

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

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

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

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

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

247
248
    @staticmethod
    def handle_event_msgsent(obj):
249
250
251
        if not obj.play_sound:
            return

252
253
        enabled = app.settings.get_soundevent_settings('message_sent')['enabled']
        if enabled:
254
255
            if isinstance(obj.jid, list) and len(obj.jid) > 1:
                return
256
257
            helpers.play_sound('message_sent')

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

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

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

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

291
    def handle_event_subscribed_presence(self, event):
292
293
        bare_jid = event.jid.bare
        resource = event.jid.resource
294
295
296
297
298
299
300
301
302
        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)
303
        else:
304
            name = event.jid.localpart
305
            name = name.split('%', 1)[0]
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
            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)
324
325

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

329
330
        name = contact.get_shown_name()
        jid = contact.jid
Philipp Hörist's avatar
Philipp Hörist committed
331
        ConfirmationDialog(
332
            _('Subscription Removed'),
Yann Leboulanger's avatar
Yann Leboulanger committed
333
334
            _('%(name)s (%(jid)s) has removed subscription from you') % {
                'name': name, 'jid': jid},
335
336
337
338
339
            _('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',
340
                               callback=_remove)]).show()
341
342
343

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

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

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

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

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

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

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

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

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

Philipp Hörist's avatar
Philipp Hörist committed
390
        if helpers.allow_showing_notification(event.account):
391
            event_type = _('Group Chat Invitation')
Philipp Hörist's avatar
Philipp Hörist committed
392
393
394
395
396
397
398
399
400
401
            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)
402

403
404
    @staticmethod
    def handle_event_client_cert_passphrase(obj):
405
406
407
408
409
410
411
412
        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
413
414
415
416
        PassphraseDialog(_('Certificate Passphrase Required'),
                         _('Enter the certificate passphrase for account %s') % \
                         obj.conn.name, ok_handler=on_ok,
                         cancel_handler=on_cancel)
417

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

708
709
        if event_type == '':
            # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
710
711
712
713
            # 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
714
715
716
            return

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

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

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

Philipp Hörist's avatar
Philipp Hörist committed
776
777
        if (app.settings.get('notify_on_file_complete') and
                (app.settings.get('autopopupaway') or
Philipp Hörist's avatar
Philipp Hörist committed
778
                app.connections[account].status in ('online', 'chat'))):
779
780
            # 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
781
            app.notification.popup(
782
783
784
785
786
787
788
                event_type,
                jid,
                account,
                msg_type,
                icon_name=icon_name,
                title=event_type,
                text=txt)
789

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

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

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

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

807
        # enable location listener
808
        if (pep_supported and app.is_installed('GEOCLUE') and
809
                app.settings.get_account_setting(account, 'publish_location')):
Philipp Hörist's avatar
Philipp Hörist committed
810
            location.enable()
811

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

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

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