gui_interface.py 87.7 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
from gajim import gui_menu_builder
60
from gajim.dialog_messages import get_dialog
61

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

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

Philipp Hörist's avatar
Philipp Hörist committed
70
from gajim.common import idle
André's avatar
André committed
71
72
73
74
75
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
Philipp Hörist's avatar
Philipp Hörist committed
76
from gajim.common.helpers import ask_for_status_message
77
from gajim.common.helpers import get_group_chat_nick
Philipp Hörist's avatar
Philipp Hörist committed
78
from gajim.common.structs import MUCData
79
from gajim.common.structs import OutgoingMessage
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
84
from gajim.common.const import JingleState
Philipp Hörist's avatar
Philipp Hörist committed
85

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

from gajim import roster_window
from gajim.common import ged
91
from gajim.common.exceptions import FileError
steve-e's avatar
steve-e committed
92

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

steve-e's avatar
steve-e committed
116
117
118
119
120
121
122
123
124

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

class Interface:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
    @staticmethod
    def handle_event_read_state_sync(event):
        if event.type.is_groupchat:
            control = app.get_groupchat_control(
                event.account, event.jid.bare)
            if control is None:
                log.warning('Groupchat control not found')
                return

            jid = event.jid.bare
            types = ['printed_gc_msg', 'printed_marked_gc_msg']

        else:
            types = ['chat', 'pm', 'printed_chat', 'printed_pm']
            jid = event.jid

            control = app.interface.msg_win_mgr.get_control(jid, event.account)

        # Compare with control.last_msg_id.
        events_ = app.events.get_events(event.account, jid, types)
        if not events_:
            log.warning('No Events')
            return

        if event.type.is_groupchat:
            id_ = events_[-1].stanza_id or events_[-1].message_id
        else:
            id_ = events_[-1].message_id

        if id_ != event.marker_id:
            return

        if not app.events.remove_events(event.account, jid, types=types):
            # There were events to remove
            if control is not None:
                control.redraw_after_event_removed(event.jid)

283
284
    @staticmethod
    def handle_event_msgsent(obj):
285
286
287
        if not obj.play_sound:
            return

288
289
        enabled = app.settings.get_soundevent_settings('message_sent')['enabled']
        if enabled:
290
291
            if isinstance(obj.jid, list) and len(obj.jid) > 1:
                return
292
293
            helpers.play_sound('message_sent')

294
295
    @staticmethod
    def handle_event_msgnotsent(obj):
296
297
        #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
        msg = _('error while sending %(message)s ( %(error)s )') % {
298
299
                'message': obj.message, 'error': obj.error}
        if not obj.session:
300
301
            # No session. This can happen when sending a message from
            # gajim-remote
302
            log.warning(msg)
303
            return
304
305
        obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
            msg_type='error')
306

307
    def handle_event_subscribe_presence(self, obj):
308
        #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
309
        account = obj.conn.name
310
        if helpers.allow_popup_window(account) or not self.systray_enabled:
311
312
313
314
315
            open_window('SubscriptionRequest',
                        account=account,
                        jid=obj.jid,
                        text=obj.status,
                        user_nick=obj.user_nick)
316
317
            return

318
319
        event = events.SubscriptionRequestEvent(obj.status, obj.user_nick)
        self.add_event(account, obj.jid, event)
320
321
322

        if helpers.allow_showing_notification(account):
            event_type = _('Subscription request')
Philipp Hörist's avatar
Philipp Hörist committed
323
324
325
            app.notification.popup(
                event_type, obj.jid, account, 'subscription_request',
                'gajim-subscription_request', event_type, obj.jid)
326

327
    def handle_event_subscribed_presence(self, event):
328
329
        bare_jid = event.jid.bare
        resource = event.jid.resource
330
331
332
333
334
335
336
337
338
        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)
339
        else:
340
            name = event.jid.localpart
341
            name = name.split('%', 1)[0]
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
            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'),
358
359
            text=_('The contact "%(jid)s" has authorized you'
                   ' to see their status.') % {'jid': event.jid})
360
361

    def show_unsubscribed_dialog(self, account, contact):
362
363
        def _remove():
            self.roster.on_req_usub(None, [(contact, account)])
364

365
366
        name = contact.get_shown_name()
        jid = contact.jid
Philipp Hörist's avatar
Philipp Hörist committed
367
        ConfirmationDialog(
368
            _('Subscription Removed'),
Yann Leboulanger's avatar
Yann Leboulanger committed
369
370
            _('%(name)s (%(jid)s) has removed subscription from you') % {
                'name': name, 'jid': jid},
371
372
373
374
375
            _('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',
376
                               callback=_remove)]).show()
377
378
379

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

381
    def handle_event_unsubscribed_presence(self, obj):
382
        #('UNSUBSCRIBED', account, jid)
383
        account = obj.conn.name
384
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
385
386
387
388
389
390
391
        if not contact:
            return

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

392
393
        event = events.UnsubscribedEvent(contact)
        self.add_event(account, obj.jid, event)
394
395
396

        if helpers.allow_showing_notification(account):
            event_type = _('Unsubscribed')
Philipp Hörist's avatar
Philipp Hörist committed
397
398
399
            app.notification.popup(
                event_type, obj.jid, account,
                'unsubscribed', 'gajim-unsubscribed',
400
                event_type, obj.jid)
401

Philipp Hörist's avatar
Philipp Hörist committed
402
403
404
    def handle_event_gc_decline(self, event):
        gc_control = self.msg_win_mgr.get_gc_control(str(event.muc),
                                                     event.account)
405
        if gc_control:
Philipp Hörist's avatar
Philipp Hörist committed
406
            if event.reason:
407
                gc_control.add_info_message(
408
                    _('%(jid)s declined the invitation: %(reason)s') % {
409
                        'jid': event.from_, 'reason': event.reason})
410
            else:
411
                gc_control.add_info_message(
412
                    _('%(jid)s declined the invitation') % {
413
                        'jid': event.from_})
Philipp Hörist's avatar
Philipp Hörist committed
414
415

    def handle_event_gc_invitation(self, event):
416
        event = events.GcInvitationtEvent(event)
417

418
419
420
421
422
423
        if (helpers.allow_popup_window(event.account) or
                not self.systray_enabled):
            open_window('GroupChatInvitation',
                        account=event.account,
                        event=event)
            return
Philipp Hörist's avatar
Philipp Hörist committed
424

425
        self.add_event(event.account, str(event.from_), event)
426

Philipp Hörist's avatar
Philipp Hörist committed
427
        if helpers.allow_showing_notification(event.account):
428
            contact_name = event.get_inviter_name()
429
            event_type = _('Group Chat Invitation')
430
431
            text = _('%(contact)s invited you to %(chat)s') % {
                'contact': contact_name, 'chat': event.info.muc_name}
Philipp Hörist's avatar
Philipp Hörist committed
432
            app.notification.popup(event_type,
433
                                   str(event.from_),
Philipp Hörist's avatar
Philipp Hörist committed
434
435
436
437
438
                                   event.account,
                                   'gc-invitation',
                                   'gajim-gc_invitation',
                                   event_type,
                                   text,
439
                                   room_jid=event.muc)
440

441
442
    @staticmethod
    def handle_event_client_cert_passphrase(obj):
443
444
445
446
447
448
449
450
        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
451
452
453
454
        PassphraseDialog(_('Certificate Passphrase Required'),
                         _('Enter the certificate passphrase for account %s') % \
                         obj.conn.name, ok_handler=on_ok,
                         cancel_handler=on_cancel)
455

456
    def handle_event_password_required(self, obj):
457
        #('PASSWORD_REQUIRED', account, None)
458
        account = obj.conn.name
459
460
461
462
463
        if account in self.pass_dialog:
            return
        text = _('Enter your password for account %s') % account

        def on_ok(passphrase, save):
464
            app.settings.set_account_setting(account, 'savepass', save)
Philipp Hörist's avatar
Philipp Hörist committed
465
466
            passwords.save_password(account, passphrase)
            obj.on_password(passphrase)
467
468
469
470
471
            del self.pass_dialog[account]

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

Daniel Brötzmann's avatar
Daniel Brötzmann committed
472
        self.pass_dialog[account] = PassphraseDialog(
473
474
            _('Password Required'), text, _('Save password'), ok_handler=on_ok,
            cancel_handler=on_cancel)
475

476
    def handle_event_roster_info(self, obj):
477
        #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
478
        account = obj.conn.name
479
        contacts = app.contacts.get_contacts(account, obj.jid)
480
481
482
        if (not obj.sub or obj.sub == 'none') and \
        (not obj.ask or obj.ask == 'none') and not obj.nickname and \
        not obj.groups:
483
484
            # contact removed us.
            if contacts:
485
                self.roster.remove_contact(obj.jid, account, backend=True)
486
487
                return
        elif not contacts:
488
            if obj.sub == 'remove':
489
490
                return
            # Add new contact to roster
Philipp Hörist's avatar
Philipp Hörist committed
491

492
            contact = app.contacts.create_contact(jid=obj.jid,
493
                account=account, name=obj.nickname, groups=obj.groups,
Philipp Hörist's avatar
Philipp Hörist committed
494
                show='offline', sub=obj.sub, ask=obj.ask,
Philipp Hörist's avatar
Philipp Hörist committed
495
                avatar_sha=obj.avatar_sha)
496
            app.contacts.add_contact(account, contact)
497
            self.roster.add_contact(obj.jid, account)
498
499
500
        else:
            # If contact has changed (sub, ask or group) update roster
            # Mind about observer status changes:
501
502
            #   According to xep 0162, a contact is not an observer anymore when
            #   we asked for auth, so also remove him if ask changed
503
            old_groups = contacts[0].groups
504
505
506
507
            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
508
            update = False
509
510
            if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
            or old_groups != obj.groups:
511
                # c.get_shown_groups() has changed. Reflect that in
512
                # roster_window
513
                self.roster.remove_contact(obj.jid, account, force=True)
514
                update = True
515
            for contact in contacts:
516
517
518
519
                contact.name = obj.nickname or ''
                contact.sub = obj.sub
                contact.ask = obj.ask
                contact.groups = obj.groups or []
520
521
522
523
524
525
            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)
526
527
        if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
        'from', 'both'):
Philipp Hörist's avatar
Philipp Hörist committed
528
            self.instances[account]['sub_request'][obj.jid].destroy()
529

Philipp Hörist's avatar
Philipp Hörist committed
530
    def handle_event_file_send_error(self, event):
531
        ft = self.instances['file_transfers']
Philipp Hörist's avatar
Philipp Hörist committed
532
        ft.set_status(event.file_props, 'stop')
533

Philipp Hörist's avatar
Philipp Hörist committed
534
535
        if helpers.allow_popup_window(event.account):
            ft.show_send_error(event.file_props)
536
537
            return

Philipp Hörist's avatar
Philipp Hörist committed
538
539
        event = events.FileSendErrorEvent(event.file_props)
        self.add_event(event.account, event.jid, event)
540

Philipp Hörist's avatar
Philipp Hörist committed
541
        if helpers.allow_showing_notification(event.account):
542
            event_type = _('File Transfer Error')
Philipp Hörist's avatar
Philipp Hörist committed
543
            app.notification.popup(
Philipp Hörist's avatar
Philipp Hörist committed
544
                event_type, event.jid, event.account,
545
                'file-send-error', 'dialog-error',
Philipp Hörist's avatar
Philipp Hörist committed
546
                event_type, event.file_props.name)
547

548
    def handle_event_file_request_error(self, obj):
549
550
        # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
551
552
        ft.set_status(obj.file_props, 'stop')
        errno = obj.file_props.error
553

554
        if helpers.allow_popup_window(obj.conn.name):
555
            if errno in (-4, -5):
556
                ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
557
            else:
558
                ft.show_request_error(obj.file_props)
559
560
561
            return

        if errno in (-4, -5):
562
            event_class = events.FileErrorEvent
563
564
            msg_type = 'file-error'
        else:
565
            event_class = events.FileRequestErrorEvent
566
567
            msg_type = 'file-request-error'

568
569
        event = event_class(obj.file_props)
        self.add_event(obj.conn.name, obj.jid, event)
570

571
        if helpers.allow_showing_notification(obj.conn.name):
572
            # Check if we should be notified
573
            event_type = _('File Transfer Error')
Philipp Hörist's avatar
Philipp Hörist committed
574
            app.notification.popup(
575
576
577
578
579
580
581
                event_type,
                obj.jid,
                obj.conn.name,
                msg_type,
                'dialog-error',
                title=event_type,
                text=obj.file_props.name)
582

583
584
    def handle_event_file_request(self, obj):
        account = obj.conn.name
585
        if obj.jid not in app.contacts.get_jid_list(account):
586
587
            contact = app.contacts.create_not_in_roster_contact(
                jid=obj.jid, account=account)
588
            app.contacts.add_contact(account, contact)
589
            self.roster.add_contact(obj.jid, account)
590
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
591
        if obj.file_props.session_type == 'jingle':
592
593
594
            request = \
                obj.stanza.getTag('jingle').getTag('content').getTag(
                    'description').getTag('request')
595
            if request:
596
                # If we get a request instead
597
598
                ft_win = self.instances['file_transfers']
                ft_win.add_transfer(account, contact, obj.file_props)
599
                return
600
        if helpers.allow_popup_window(account):
601
602
            self.instances['file_transfers'].show_file_request(
                account, contact, obj.file_props)
603
            return
604
605
        event = events.FileRequestEvent(obj.file_props)
        self.add_event(account, obj.jid, event)
606
        if helpers.allow_showing_notification(account):
607
            txt = _('%s wants to send you a file.') % app.get_name_from_jid(
608
                account, obj.jid)
609
            event_type = _('File Transfer Request')
Philipp Hörist's avatar
Philipp Hörist committed
610
            app.notification.popup(
611
612
613
614
615
616
617
                event_type,
                obj.jid,
                account,
                'file-request',
                icon_name='document-send',
                title=event_type,
                text=txt)
618

619
620
    @staticmethod
    def handle_event_file_error(title, message):
621
        ErrorDialog(title, message)
622
623
624

    def handle_event_file_progress(self, account, file_props):
        if time.time() - self.last_ftwindow_update > 0.5:
625
            # Update ft window every 500ms
626
            self.last_ftwindow_update = time.time()
627
628
            self.instances['file_transfers'].set_progress(
                file_props.type_, file_props.sid, file_props.received_len)
Yann Leboulanger's avatar
Yann Leboulanger committed
629

630
    def __compare_hashes(self, account, file_props):
631
632
        session = app.connections[account].get_module(
            'Jingle').get_jingle_session(jid=None, sid=file_props.sid)
633
        ft_win = self.instances['file_transfers']
634
        h = Hashes2()
635
        try:
636
            file_ = open(file_props.file_name, 'rb')
637
        except Exception:
638
            return
zimio's avatar
zimio committed
639
        hash_ = h.calculateHash(file_props.algo, file_)
Yann Leboulanger's avatar
Yann Leboulanger committed
640
        file_.close()
641
642
        # 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
643
        jid = file_props.sender
zimio's avatar
zimio committed
644
        if file_props.hash_ == hash_:
Yann Leboulanger's avatar
Yann Leboulanger committed
645
646
            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
            GLib.idle_add(ft_win.set_status, file_props, 'ok')
647
        else:
648
            # Wrong hash, we need to get the file again!
zimio's avatar
zimio committed
649
            file_props.error = -10
Yann Leboulanger's avatar
Yann Leboulanger committed
650
651
            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
            GLib.idle_add(ft_win.set_status, file_props, 'hash_error')
652
653
654
        # End jingle session
        if session:
            session.end_session()
655
656
657

    def handle_event_file_rcv_completed(self, account, file_props):
        ft = self.instances['file_transfers']
zimio's avatar
zimio committed
658
        if file_props.error == 0:
659
660
            ft.set_progress(
                file_props.type_, file_props.sid, file_props.received_len)
661
            jid = app.get_jid_without_resource(str(file_props.receiver))
662
            app.nec.push_incoming_event(
663
664
665
666
                NetworkEvent('file-transfer-completed',
                             file_props=file_props,
                             jid=jid))

667
        else:
zimio's avatar
zimio committed
668
            ft.set_status(file_props, 'stop')
669
670
        if not file_props.completed and (file_props.stalled or
                file_props.paused):
671
            return
Yann Leboulanger's avatar
Yann Leboulanger committed
672

673
        if file_props.type_ == 'r':  # We receive a file
674
            app.socks5queue.remove_receiver(file_props.sid, True, True)
675
            if file_props.session_type == 'jingle':
676
                if file_props.hash_ and file_props.error == 0:
677
678
                    # We compare hashes in a new thread
                    self.hashThread = Thread(target=self.__compare_hashes,
679
                                             args=(account, file_props))
680
681
                    self.hashThread.start()
                else:
682
683
                    # We didn't get the hash, sender probably doesn't
                    # support that
684
685
                    jid = file_props.sender
                    self.popup_ft_result(account, jid, file_props)
686
687
                    if file_props.error == 0:
                        ft.set_status(file_props, 'ok')
688
689
690
691
                    session = \
                        app.connections[account].get_module(
                            'Jingle').get_jingle_session(jid=None,
                                                         sid=file_props.sid)
692
                    # End jingle session
693
694
                    # TODO: Only if there are no other parallel downloads in
                    # this session
695
696
                    if session:
                        session.end_session()
697
        else:  # We send a file
Yann Leboulanger's avatar
Yann Leboulanger committed
698
            jid = file_props.receiver
699
            app.socks5queue.remove_sender(file_props.sid, True, True)
700
            self.popup_ft_result(account, jid, file_props)
701

702
703
    def popup_ft_result(self, account, jid, file_props):
        ft = self.instances['file_transfers']
704
        if helpers.allow_popup_window(account):
zimio's avatar
zimio committed
705
            if file_props.error == 0:
Philipp Hörist's avatar
Philipp Hörist committed
706
                if app.settings.get('notify_on_file_complete'):
707
                    ft.show_completed(jid, file_props)
zimio's avatar
zimio committed
708
            elif file_props.error == -1:
709
710
711
712
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('Remote Contact Stopped Transfer'))
zimio's avatar
zimio committed
713
            elif file_props.error == -6:
714
715
716
717
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('Error Opening File'))
zimio's avatar
zimio committed
718
            elif file_props.error == -10:
719
720
721
722
                ft.show_hash_error(
                    jid,
                    file_props,
                    account)
723
            elif file_props.error == -12:
724
725
726
727
                ft.show_stopped(
                    jid,
                    file_props,
                    error_msg=_('SSL Certificate Error'))
728
729
730
731
            return

        msg_type = ''
        event_type = ''
732
        if (file_props.error == 0 and
Philipp Hörist's avatar
Philipp Hörist committed
733
                app.settings.get('notify_on_file_complete')):
734
            event_class = events.FileCompletedEvent
735
736
            msg_type = 'file-completed'
            event_type = _('File Transfer Completed')
zimio's avatar
zimio committed
737
        elif file_props.error in (-1, -6):
738
            event_class = events.FileStoppedEvent
739
740
            msg_type = 'file-stopped'
            event_type = _('File Transfer Stopped')
741
        elif file_props.error == -10:
742
            event_class = events.FileHashErrorEvent
743
744
            msg_type = 'file-hash-error'
            event_type = _('File Transfer Failed')
Yann Leboulanger's avatar
Yann Leboulanger committed
745

746
747
        if event_type == '':
            # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
748
749
750
751
            # 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
752
753
754
            return

        if msg_type:
755
756
            event = event_class(file_props)
            self.add_event(account, jid, event)
757
758

        if file_props is not None:
zimio's avatar
zimio committed
759
            if file_props.type_ == 'r':
760
                # Get the name of the sender, as it is in the roster
Yann Leboulanger's avatar
Yann Leboulanger committed
761
                sender = file_props.sender.split('/')[0]
762
763
                name = app.contacts.get_first_contact_from_jid(
                    account, sender).get_shown_name()
zimio's avatar
zimio committed
764
                filename = os.path.basename(file_props.file_name)
765

766
                if event_type == _('File Transfer Completed'):
767
768
769
                    txt = _('%(filename)s received from %(name)s.') % {
                        'filename': filename,
                        'name': name}
770
                    icon_name = 'emblem-default'
771
                elif event_type == _('File Transfer Stopped'):
772
                    txt = _('File transfer of %(filename)s from %(name)s '
773
774
775
                            'stopped.') % {
                                'filename': filename,
                                'name': name}
776
                    icon_name = 'process-stop'
777
                else: # File transfer hash error
778
                    txt = _('File transfer of %(filename)s from %(name)s '
779
780
781
                            'failed.') % {
                                'filename': filename,
                                'name': name}
782
                    icon_name = 'process-stop'
783
            else:
zimio's avatar
zimio committed
784
                receiver = file_props.receiver
785
786
787
                if hasattr(receiver, 'jid'):
                    receiver = receiver.jid
                receiver = receiver.split('/')[0]
788
789
790
                # 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
791
                filename = os.path.basename(file_props.file_name)
792
                if event_type == _('File Transfer Completed'):
793
794
795
796
                    txt = _('You successfully sent %(filename)s to '
                            '%(name)s.') % {
                                'filename': filename,
                                'name': name}
797
                    icon_name = 'emblem-default'
798
                elif event_type == _('File Transfer Stopped'):
799
                    txt = _('File transfer of %(filename)s to %(name)s '
800
801
802
                            'stopped.') % {
                                'filename': filename,
                                'name': name}
803
                    icon_name = 'process-stop'
804
                else: # File transfer hash error
805
                    txt = _('File transfer of %(filename)s to %(name)s '
806
807
808
                            'failed.') % {
                                'filename': filename,
                                'name': name}
809
                    icon_name = 'process-stop'
810
811
        else:
            txt = ''
Philipp Hörist's avatar
Philipp Hörist committed
812
            icon_name = None
813

Philipp Hörist's avatar
Philipp Hörist committed
814
815
        if (app.settings.get('notify_on_file_complete') and
                (app.settings.get('autopopupaway') or
Philipp Hörist's avatar
Philipp Hörist committed
816
                app.connections[account].status in ('online', 'chat'))):
817
818
            # 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
819
            app.notification.popup(
820
821
822
823
824
825
826
                event_type,
                jid,
                account,
                msg_type,
                icon_name=icon_name,
                title=event_type,
                text=txt)
827

828
    def handle_event_signed_in(self, obj):
829
830
831
832
833
        """
        SIGNED_IN event is emitted when we sign in, so handle it
        """
        # ('SIGNED_IN', account, ())
        # block signed in notifications for 30 seconds
834
835

        # Add our own JID into the DB
836
        app.storage.archive.insert_jid(obj.conn.get_own_jid().bare)
837
        account = obj.conn.name
838
        app.block_signed_in_notifications[account] = True
Philipp Hörist's avatar
Philipp Hörist committed
839

840
841
        pep_supported = obj.conn.get_module('PEP').supported

Philipp Hörist's avatar
Philipp Hörist committed
842
        if obj.conn.get_module('MAM').available:
Philipp Hörist's avatar
Philipp Hörist committed
843
            obj.conn.get_module('MAM').request_archive_on_signin()
844

845
        # enable location listener
846
        if (pep_supported and app.is_installed('GEOCLUE') and
847
                app.settings.get_account_setting(account, 'publish_location')):
Philipp Hörist's avatar
Philipp Hörist committed
848
            location.enable()
849

Philipp Hörist's avatar
Philipp Hörist committed
850
851
852
        if ask_for_status_message(obj.conn.status, signin=True):
            open_window('StatusChange', status=obj.conn.status)

853
854
855
856