1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
2# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
3# Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
4#                    Norman Rasmussen <norman AT rasmussen.co.za>
5#                    Stéphan Kochen <stephan AT kochen.nl>
6# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
7#                         Alex Mauer <hawke AT hawkesnest.net>
8# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
9#                         Nikos Kouremenos <kourem AT gmail.com>
10# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
11#                    Stefan Bethge <stefan AT lanpartei.de>
12# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
13# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
14#                    James Newton <redshodan AT gmail.com>
15# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
16#                         Julien Pivotto <roidelapluie AT gmail.com>
17#                         Stephan Erb <steve-e AT h3c.de>
18# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
19#
20# This file is part of Gajim.
21#
22# Gajim is free software; you can redistribute it and/or modify
23# it under the terms of the GNU General Public License as published
24# by the Free Software Foundation; version 3 only.
25#
26# Gajim is distributed in the hope that it will be useful,
27# but WITHOUT ANY WARRANTY; without even the implied warranty of
28# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29# GNU General Public License for more details.
30#
31# You should have received a copy of the GNU General Public License
32# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
33
34import os
35import sys
36import time
37import json
38import logging
39from functools import partial
40from threading import Thread
41from datetime import datetime
42from importlib.util import find_spec
43from packaging.version import Version as V
44
45from gi.repository import Gtk
46from gi.repository import GLib
47from gi.repository import Gio
48from gi.repository import Soup
49from nbxmpp import idlequeue
50from nbxmpp import Hashes2
51
52from gajim.common import app
53from gajim.common import events
54from gajim.common.dbus import location
55from gajim.common.dbus import logind
56from gajim.common.dbus import music_track
57
58from gajim import gui_menu_builder
59from gajim.dialog_messages import get_dialog
60
61from gajim.chat_control_base import ChatControlBase
62from gajim.chat_control import ChatControl
63from gajim.groupchat_control import GroupchatControl
64from gajim.privatechat_control import PrivateChatControl
65from gajim.message_window import MessageWindowMgr
66
67from gajim.session import ChatControlSession
68
69from gajim.common import idle
70from gajim.common.zeroconf import connection_zeroconf
71from gajim.common import proxy65_manager
72from gajim.common import socks5
73from gajim.common import helpers
74from gajim.common import passwords
75from gajim.common.helpers import ask_for_status_message
76from gajim.common.helpers import get_group_chat_nick
77from gajim.common.structs import MUCData
78from gajim.common.structs import OutgoingMessage
79from gajim.common.nec import NetworkEvent
80from gajim.common.i18n import _
81from gajim.common.client import Client
82from gajim.common.const import Display
83from gajim.common.const import JingleState
84
85from gajim.common.file_props import FilesProp
86from gajim.common.connection_handlers_events import InformationEvent
87
88from gajim import roster_window
89from gajim.common import ged
90from gajim.common.exceptions import FileError
91
92from gajim.gui.avatar import AvatarStorage
93from gajim.gui.notification import Notification
94from gajim.gui.dialogs import DialogButton
95from gajim.gui.dialogs import ErrorDialog
96from gajim.gui.dialogs import WarningDialog
97from gajim.gui.dialogs import InformationDialog
98from gajim.gui.dialogs import ConfirmationDialog
99from gajim.gui.dialogs import ConfirmationCheckDialog
100from gajim.gui.dialogs import InputDialog
101from gajim.gui.dialogs import PassphraseDialog
102from gajim.gui.filechoosers import FileChooserDialog
103from gajim.gui.filetransfer import FileTransfersWindow
104from gajim.gui.filetransfer_progress import FileTransferProgress
105from gajim.gui.roster_item_exchange import RosterItemExchangeWindow
106from gajim.gui.util import get_show_in_roster
107from gajim.gui.util import get_show_in_systray
108from gajim.gui.util import open_window
109from gajim.gui.util import get_app_window
110from gajim.gui.util import get_app_windows
111from gajim.gui.util import get_color_for_account
112from gajim.gui.const import ControlType
113
114
115log = logging.getLogger('gajim.interface')
116
117class Interface:
118
119################################################################################
120### Methods handling events from connection
121################################################################################
122
123    def handle_event_db_error(self, unused, error):
124        #('DB_ERROR', account, error)
125        if self.db_error_dialog:
126            return
127        self.db_error_dialog = ErrorDialog(_('Database Error'), error)
128        def destroyed(win):
129            self.db_error_dialog = None
130        self.db_error_dialog.connect('destroy', destroyed)
131
132    @staticmethod
133    def handle_event_information(obj):
134        if not obj.popup:
135            return
136
137        if obj.dialog_name is not None:
138            get_dialog(obj.dialog_name, *obj.args, **obj.kwargs)
139            return
140
141        if obj.level == 'error':
142            cls = ErrorDialog
143        elif obj.level == 'warn':
144            cls = WarningDialog
145        elif obj.level == 'info':
146            cls = InformationDialog
147        else:
148            return
149
150        cls(obj.pri_txt, GLib.markup_escape_text(obj.sec_txt))
151
152    @staticmethod
153    def raise_dialog(name, *args, **kwargs):
154        get_dialog(name, *args, **kwargs)
155
156    @staticmethod
157    def handle_event_http_auth(obj):
158        # ('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
159        def _response(account, answer):
160            obj.conn.get_module('HTTPAuth').build_http_auth_answer(
161                obj.stanza, answer)
162
163        account = obj.conn.name
164        message = _('HTTP (%(method)s) Authorization '
165                    'for %(url)s (ID: %(id)s)') % {
166                        'method': obj.method,
167                        'url': obj.url,
168                        'id': obj.iq_id}
169        sec_msg = _('Do you accept this request?')
170        if app.get_number_of_connected_accounts() > 1:
171            sec_msg = _('Do you accept this request (account: %s)?') % account
172        if obj.msg:
173            sec_msg = obj.msg + '\n' + sec_msg
174        message = message + '\n' + sec_msg
175
176        ConfirmationDialog(
177            _('Authorization Request'),
178            _('HTTP Authorization Request'),
179            message,
180            [DialogButton.make('Cancel',
181                               text=_('_No'),
182                               callback=_response,
183                               args=[obj, 'no']),
184             DialogButton.make('Accept',
185                               callback=_response,
186                               args=[obj, 'yes'])]).show()
187
188    def handle_event_iq_error(self, event):
189        ctrl = self.msg_win_mgr.get_control(event.properties.jid.bare,
190                                            event.account)
191        if ctrl and ctrl.is_groupchat:
192            ctrl.add_info_message('Error: %s' % event.properties.error)
193
194    @staticmethod
195    def handle_event_connection_lost(obj):
196        # ('CONNECTION_LOST', account, [title, text])
197        account = obj.conn.name
198        app.notification.popup(
199            _('Connection Failed'), account, account,
200            'connection-lost', 'gajim-connection_lost', obj.title, obj.msg)
201
202    @staticmethod
203    def unblock_signed_in_notifications(account):
204        app.block_signed_in_notifications[account] = False
205
206    def handle_event_status(self, event):
207        if event.show in ('offline', 'error'):
208            # TODO: Close all account windows
209            pass
210
211        if event.show == 'offline':
212            app.block_signed_in_notifications[event.account] = True
213        else:
214            # 30 seconds after we change our status to sth else than offline
215            # we stop blocking notifications of any kind
216            # this prevents from getting the roster items as 'just signed in'
217            # contacts. 30 seconds should be enough time
218            GLib.timeout_add_seconds(30,
219                                     self.unblock_signed_in_notifications,
220                                     event.account)
221
222    def handle_event_presence(self, obj):
223        # 'NOTIFY' (account, (jid, status, status message, resource,
224        # priority, timestamp))
225        #
226        # Contact changed show
227        account = obj.conn.name
228        jid = obj.jid
229
230        if app.jid_is_transport(jid):
231            # It must be an agent
232
233            # transport just signed in/out, don't show
234            # popup notifications for 30s
235            account_jid = account + '/' + jid
236            app.block_signed_in_notifications[account_jid] = True
237            GLib.timeout_add_seconds(30, self.unblock_signed_in_notifications,
238                account_jid)
239
240        ctrl = self.msg_win_mgr.get_control(jid, account)
241        if ctrl and ctrl.session and len(obj.contact_list) > 1:
242            ctrl.remove_session(ctrl.session)
243
244    @staticmethod
245    def handle_event_read_state_sync(event):
246        if event.type.is_groupchat:
247            control = app.get_groupchat_control(
248                event.account, event.jid.bare)
249            if control is None:
250                log.warning('Groupchat control not found')
251                return
252
253            jid = event.jid.bare
254            types = ['printed_gc_msg', 'printed_marked_gc_msg']
255
256        else:
257            types = ['chat', 'pm', 'printed_chat', 'printed_pm']
258            jid = event.jid
259
260            control = app.interface.msg_win_mgr.get_control(jid, event.account)
261
262        # Compare with control.last_msg_id.
263        events_ = app.events.get_events(event.account, jid, types)
264        if not events_:
265            log.warning('No Events')
266            return
267
268        if event.type.is_groupchat:
269            id_ = events_[-1].stanza_id or events_[-1].message_id
270        else:
271            id_ = events_[-1].message_id
272
273        if id_ != event.marker_id:
274            return
275
276        if not app.events.remove_events(event.account, jid, types=types):
277            # There were events to remove
278            if control is not None:
279                control.redraw_after_event_removed(event.jid)
280
281    @staticmethod
282    def handle_event_msgsent(obj):
283        if not obj.play_sound:
284            return
285
286        enabled = app.settings.get_soundevent_settings('message_sent')['enabled']
287        if enabled:
288            if isinstance(obj.jid, list) and len(obj.jid) > 1:
289                return
290            helpers.play_sound('message_sent')
291
292    @staticmethod
293    def handle_event_msgnotsent(obj):
294        #('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session))
295        msg = _('error while sending %(message)s ( %(error)s )') % {
296                'message': obj.message, 'error': obj.error}
297        if not obj.session:
298            # No session. This can happen when sending a message from
299            # gajim-remote
300            log.warning(msg)
301            return
302        obj.session.roster_message(obj.jid, msg, obj.time_, obj.conn.name,
303            msg_type='error')
304
305    def handle_event_subscribe_presence(self, obj):
306        #('SUBSCRIBE', account, (jid, text, user_nick)) user_nick is JEP-0172
307        account = obj.conn.name
308        if helpers.allow_popup_window(account) or not self.systray_enabled:
309            open_window('SubscriptionRequest',
310                        account=account,
311                        jid=obj.jid,
312                        text=obj.status,
313                        user_nick=obj.user_nick)
314            return
315
316        event = events.SubscriptionRequestEvent(obj.status, obj.user_nick)
317        self.add_event(account, obj.jid, event)
318
319        if helpers.allow_showing_notification(account):
320            event_type = _('Subscription request')
321            app.notification.popup(
322                event_type, obj.jid, account, 'subscription_request',
323                'gajim-subscription_request', event_type, obj.jid)
324
325    def handle_event_subscribed_presence(self, event):
326        bare_jid = event.jid.bare
327        resource = event.jid.resource
328        if bare_jid in app.contacts.get_jid_list(event.account):
329            contact = app.contacts.get_first_contact_from_jid(event.account,
330                                                              bare_jid)
331            contact.resource = resource
332            self.roster.remove_contact_from_groups(contact.jid,
333                                                   event.account,
334                                                   [_('Not in contact list'),
335                                                    _('Observers')],
336                                                   update=False)
337        else:
338            name = event.jid.localpart
339            name = name.split('%', 1)[0]
340            contact = app.contacts.create_contact(jid=bare_jid,
341                                                  account=event.account,
342                                                  name=name,
343                                                  groups=[],
344                                                  show='online',
345                                                  status='online',
346                                                  ask='to',
347                                                  resource=resource)
348            app.contacts.add_contact(event.account, contact)
349            self.roster.add_contact(bare_jid, event.account)
350
351        app.notification.popup(
352            None,
353            bare_jid,
354            event.account,
355            title=_('Authorization accepted'),
356            text=_('The contact "%(jid)s" has authorized you'
357                   ' to see their status.') % {'jid': event.jid})
358
359    def show_unsubscribed_dialog(self, account, contact):
360        def _remove():
361            self.roster.on_req_usub(None, [(contact, account)])
362
363        name = contact.get_shown_name()
364        jid = contact.jid
365        ConfirmationDialog(
366            _('Subscription Removed'),
367            _('%(name)s (%(jid)s) has removed subscription from you') % {
368                'name': name, 'jid': jid},
369            _('You will always see this contact as offline.\n'
370              'Do you want to remove them from your contact list?'),
371            [DialogButton.make('Cancel',
372                               text=_('_No')),
373             DialogButton.make('Remove',
374                               callback=_remove)]).show()
375
376        # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does
377        # not show deny
378
379    def handle_event_unsubscribed_presence(self, obj):
380        #('UNSUBSCRIBED', account, jid)
381        account = obj.conn.name
382        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
383        if not contact:
384            return
385
386        if helpers.allow_popup_window(account) or not self.systray_enabled:
387            self.show_unsubscribed_dialog(account, contact)
388            return
389
390        event = events.UnsubscribedEvent(contact)
391        self.add_event(account, obj.jid, event)
392
393        if helpers.allow_showing_notification(account):
394            event_type = _('Unsubscribed')
395            app.notification.popup(
396                event_type, obj.jid, account,
397                'unsubscribed', 'gajim-unsubscribed',
398                event_type, obj.jid)
399
400    def handle_event_gc_decline(self, event):
401        gc_control = self.msg_win_mgr.get_gc_control(str(event.muc),
402                                                     event.account)
403        if gc_control:
404            if event.reason:
405                gc_control.add_info_message(
406                    _('%(jid)s declined the invitation: %(reason)s') % {
407                        'jid': event.from_, 'reason': event.reason})
408            else:
409                gc_control.add_info_message(
410                    _('%(jid)s declined the invitation') % {
411                        'jid': event.from_})
412
413    def handle_event_gc_invitation(self, event):
414        event = events.GcInvitationtEvent(event)
415
416        if (helpers.allow_popup_window(event.account) or
417                not self.systray_enabled):
418            open_window('GroupChatInvitation',
419                        account=event.account,
420                        event=event)
421            return
422
423        self.add_event(event.account, str(event.from_), event)
424
425        if helpers.allow_showing_notification(event.account):
426            contact_name = event.get_inviter_name()
427            event_type = _('Group Chat Invitation')
428            text = _('%(contact)s invited you to %(chat)s') % {
429                'contact': contact_name, 'chat': event.info.muc_name}
430            app.notification.popup(event_type,
431                                   str(event.from_),
432                                   event.account,
433                                   'gc-invitation',
434                                   'gajim-gc_invitation',
435                                   event_type,
436                                   text,
437                                   room_jid=event.muc)
438
439    @staticmethod
440    def handle_event_client_cert_passphrase(obj):
441        def on_ok(passphrase, checked):
442            obj.conn.on_client_cert_passphrase(passphrase, obj.con, obj.port,
443                obj.secure_tuple)
444
445        def on_cancel():
446            obj.conn.on_client_cert_passphrase('', obj.con, obj.port,
447                obj.secure_tuple)
448
449        PassphraseDialog(_('Certificate Passphrase Required'),
450                         _('Enter the certificate passphrase for account %s') % \
451                         obj.conn.name, ok_handler=on_ok,
452                         cancel_handler=on_cancel)
453
454    def handle_event_password_required(self, obj):
455        #('PASSWORD_REQUIRED', account, None)
456        account = obj.conn.name
457        if account in self.pass_dialog:
458            return
459        text = _('Enter your password for account %s') % account
460
461        def on_ok(passphrase, save):
462            app.settings.set_account_setting(account, 'savepass', save)
463            passwords.save_password(account, passphrase)
464            obj.on_password(passphrase)
465            del self.pass_dialog[account]
466
467        def on_cancel():
468            del self.pass_dialog[account]
469
470        self.pass_dialog[account] = PassphraseDialog(
471            _('Password Required'), text, _('Save password'), ok_handler=on_ok,
472            cancel_handler=on_cancel)
473
474    def handle_event_roster_info(self, obj):
475        #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
476        account = obj.conn.name
477        contacts = app.contacts.get_contacts(account, obj.jid)
478        if (not obj.sub or obj.sub == 'none') and \
479        (not obj.ask or obj.ask == 'none') and not obj.nickname and \
480        not obj.groups:
481            # contact removed us.
482            if contacts:
483                self.roster.remove_contact(obj.jid, account, backend=True)
484                return
485        elif not contacts:
486            if obj.sub == 'remove':
487                return
488            # Add new contact to roster
489
490            contact = app.contacts.create_contact(jid=obj.jid,
491                account=account, name=obj.nickname, groups=obj.groups,
492                show='offline', sub=obj.sub, ask=obj.ask,
493                avatar_sha=obj.avatar_sha)
494            app.contacts.add_contact(account, contact)
495            self.roster.add_contact(obj.jid, account)
496        else:
497            # If contact has changed (sub, ask or group) update roster
498            # Mind about observer status changes:
499            #   According to xep 0162, a contact is not an observer anymore when
500            #   we asked for auth, so also remove him if ask changed
501            old_groups = contacts[0].groups
502            if obj.sub == 'remove':
503                # another of our instance removed a contact. Remove it here too
504                self.roster.remove_contact(obj.jid, account, backend=True)
505                return
506            update = False
507            if contacts[0].sub != obj.sub or contacts[0].ask != obj.ask\
508            or old_groups != obj.groups:
509                # c.get_shown_groups() has changed. Reflect that in
510                # roster_window
511                self.roster.remove_contact(obj.jid, account, force=True)
512                update = True
513            for contact in contacts:
514                contact.name = obj.nickname or ''
515                contact.sub = obj.sub
516                contact.ask = obj.ask
517                contact.groups = obj.groups or []
518            if update:
519                self.roster.add_contact(obj.jid, account)
520                # Refilter and update old groups
521                for group in old_groups:
522                    self.roster.draw_group(group, account)
523                self.roster.draw_contact(obj.jid, account)
524        if obj.jid in self.instances[account]['sub_request'] and obj.sub in (
525        'from', 'both'):
526            self.instances[account]['sub_request'][obj.jid].destroy()
527
528    def handle_event_file_send_error(self, event):
529        ft = self.instances['file_transfers']
530        ft.set_status(event.file_props, 'stop')
531
532        if helpers.allow_popup_window(event.account):
533            ft.show_send_error(event.file_props)
534            return
535
536        event = events.FileSendErrorEvent(event.file_props)
537        self.add_event(event.account, event.jid, event)
538
539        if helpers.allow_showing_notification(event.account):
540            event_type = _('File Transfer Error')
541            app.notification.popup(
542                event_type, event.jid, event.account,
543                'file-send-error', 'dialog-error',
544                event_type, event.file_props.name)
545
546    def handle_event_file_request_error(self, obj):
547        # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
548        ft = self.instances['file_transfers']
549        ft.set_status(obj.file_props, 'stop')
550        errno = obj.file_props.error
551
552        if helpers.allow_popup_window(obj.conn.name):
553            if errno in (-4, -5):
554                ft.show_stopped(obj.jid, obj.file_props, obj.error_msg)
555            else:
556                ft.show_request_error(obj.file_props)
557            return
558
559        if errno in (-4, -5):
560            event_class = events.FileErrorEvent
561            msg_type = 'file-error'
562        else:
563            event_class = events.FileRequestErrorEvent
564            msg_type = 'file-request-error'
565
566        event = event_class(obj.file_props)
567        self.add_event(obj.conn.name, obj.jid, event)
568
569        if helpers.allow_showing_notification(obj.conn.name):
570            # Check if we should be notified
571            event_type = _('File Transfer Error')
572            app.notification.popup(
573                event_type,
574                obj.jid,
575                obj.conn.name,
576                msg_type,
577                'dialog-error',
578                title=event_type,
579                text=obj.file_props.name)
580
581    def handle_event_file_request(self, obj):
582        account = obj.conn.name
583        if obj.jid not in app.contacts.get_jid_list(account):
584            contact = app.contacts.create_not_in_roster_contact(
585                jid=obj.jid, account=account)
586            app.contacts.add_contact(account, contact)
587            self.roster.add_contact(obj.jid, account)
588        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
589        if obj.file_props.session_type == 'jingle':
590            request = \
591                obj.stanza.getTag('jingle').getTag('content').getTag(
592                    'description').getTag('request')
593            if request:
594                # If we get a request instead
595                ft_win = self.instances['file_transfers']
596                ft_win.add_transfer(account, contact, obj.file_props)
597                return
598        if helpers.allow_popup_window(account):
599            self.instances['file_transfers'].show_file_request(
600                account, contact, obj.file_props)
601            return
602        event = events.FileRequestEvent(obj.file_props)
603        self.add_event(account, obj.jid, event)
604        if helpers.allow_showing_notification(account):
605            txt = _('%s wants to send you a file.') % app.get_name_from_jid(
606                account, obj.jid)
607            event_type = _('File Transfer Request')
608            app.notification.popup(
609                event_type,
610                obj.jid,
611                account,
612                'file-request',
613                icon_name='document-send',
614                title=event_type,
615                text=txt)
616
617    @staticmethod
618    def handle_event_file_error(title, message):
619        ErrorDialog(title, message)
620
621    def handle_event_file_progress(self, account, file_props):
622        if time.time() - self.last_ftwindow_update > 0.5:
623            # Update ft window every 500ms
624            self.last_ftwindow_update = time.time()
625            self.instances['file_transfers'].set_progress(
626                file_props.type_, file_props.sid, file_props.received_len)
627
628    def __compare_hashes(self, account, file_props):
629        session = app.connections[account].get_module(
630            'Jingle').get_jingle_session(jid=None, sid=file_props.sid)
631        ft_win = self.instances['file_transfers']
632        h = Hashes2()
633        try:
634            file_ = open(file_props.file_name, 'rb')
635        except Exception:
636            return
637        hash_ = h.calculateHash(file_props.algo, file_)
638        file_.close()
639        # If the hash we received and the hash of the file are the same,
640        # then the file is not corrupt
641        jid = file_props.sender
642        if file_props.hash_ == hash_:
643            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
644            GLib.idle_add(ft_win.set_status, file_props, 'ok')
645        else:
646            # Wrong hash, we need to get the file again!
647            file_props.error = -10
648            GLib.idle_add(self.popup_ft_result, account, jid, file_props)
649            GLib.idle_add(ft_win.set_status, file_props, 'hash_error')
650        # End jingle session
651        if session:
652            session.end_session()
653
654    def handle_event_file_rcv_completed(self, account, file_props):
655        ft = self.instances['file_transfers']
656        if file_props.error == 0:
657            ft.set_progress(
658                file_props.type_, file_props.sid, file_props.received_len)
659            jid = app.get_jid_without_resource(str(file_props.receiver))
660            app.nec.push_incoming_event(
661                NetworkEvent('file-transfer-completed',
662                             file_props=file_props,
663                             jid=jid))
664
665        else:
666            ft.set_status(file_props, 'stop')
667        if not file_props.completed and (file_props.stalled or
668                file_props.paused):
669            return
670
671        if file_props.type_ == 'r':  # We receive a file
672            app.socks5queue.remove_receiver(file_props.sid, True, True)
673            if file_props.session_type == 'jingle':
674                if file_props.hash_ and file_props.error == 0:
675                    # We compare hashes in a new thread
676                    self.hashThread = Thread(target=self.__compare_hashes,
677                                             args=(account, file_props))
678                    self.hashThread.start()
679                else:
680                    # We didn't get the hash, sender probably doesn't
681                    # support that
682                    jid = file_props.sender
683                    self.popup_ft_result(account, jid, file_props)
684                    if file_props.error == 0:
685                        ft.set_status(file_props, 'ok')
686                    session = \
687                        app.connections[account].get_module(
688                            'Jingle').get_jingle_session(jid=None,
689                                                         sid=file_props.sid)
690                    # End jingle session
691                    # TODO: Only if there are no other parallel downloads in
692                    # this session
693                    if session:
694                        session.end_session()
695        else:  # We send a file
696            jid = file_props.receiver
697            app.socks5queue.remove_sender(file_props.sid, True, True)
698            self.popup_ft_result(account, jid, file_props)
699
700    def popup_ft_result(self, account, jid, file_props):
701        ft = self.instances['file_transfers']
702        if helpers.allow_popup_window(account):
703            if file_props.error == 0:
704                if app.settings.get('notify_on_file_complete'):
705                    ft.show_completed(jid, file_props)
706            elif file_props.error == -1:
707                ft.show_stopped(
708                    jid,
709                    file_props,
710                    error_msg=_('Remote Contact Stopped Transfer'))
711            elif file_props.error == -6:
712                ft.show_stopped(
713                    jid,
714                    file_props,
715                    error_msg=_('Error Opening File'))
716            elif file_props.error == -10:
717                ft.show_hash_error(
718                    jid,
719                    file_props,
720                    account)
721            elif file_props.error == -12:
722                ft.show_stopped(
723                    jid,
724                    file_props,
725                    error_msg=_('SSL Certificate Error'))
726            return
727
728        msg_type = ''
729        event_type = ''
730        if (file_props.error == 0 and
731                app.settings.get('notify_on_file_complete')):
732            event_class = events.FileCompletedEvent
733            msg_type = 'file-completed'
734            event_type = _('File Transfer Completed')
735        elif file_props.error in (-1, -6):
736            event_class = events.FileStoppedEvent
737            msg_type = 'file-stopped'
738            event_type = _('File Transfer Stopped')
739        elif file_props.error == -10:
740            event_class = events.FileHashErrorEvent
741            msg_type = 'file-hash-error'
742            event_type = _('File Transfer Failed')
743
744        if event_type == '':
745            # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
746            # this should never happen but it does. see process_result() in
747            # socks5.py
748            # who calls this func (sth is really wrong unless this func is also
749            # registered as progress_cb
750            return
751
752        if msg_type:
753            event = event_class(file_props)
754            self.add_event(account, jid, event)
755
756        if file_props is not None:
757            if file_props.type_ == 'r':
758                # Get the name of the sender, as it is in the roster
759                sender = file_props.sender.split('/')[0]
760                name = app.contacts.get_first_contact_from_jid(
761                    account, sender).get_shown_name()
762                filename = os.path.basename(file_props.file_name)
763
764                if event_type == _('File Transfer Completed'):
765                    txt = _('%(filename)s received from %(name)s.') % {
766                        'filename': filename,
767                        'name': name}
768                    icon_name = 'emblem-default'
769                elif event_type == _('File Transfer Stopped'):
770                    txt = _('File transfer of %(filename)s from %(name)s '
771                            'stopped.') % {
772                                'filename': filename,
773                                'name': name}
774                    icon_name = 'process-stop'
775                else: # File transfer hash error
776                    txt = _('File transfer of %(filename)s from %(name)s '
777                            'failed.') % {
778                                'filename': filename,
779                                'name': name}
780                    icon_name = 'process-stop'
781            else:
782                receiver = file_props.receiver
783                if hasattr(receiver, 'jid'):
784                    receiver = receiver.jid
785                receiver = receiver.split('/')[0]
786                # Get the name of the contact, as it is in the roster
787                name = app.contacts.get_first_contact_from_jid(
788                    account, receiver).get_shown_name()
789                filename = os.path.basename(file_props.file_name)
790                if event_type == _('File Transfer Completed'):
791                    txt = _('You successfully sent %(filename)s to '
792                            '%(name)s.') % {
793                                'filename': filename,
794                                'name': name}
795                    icon_name = 'emblem-default'
796                elif event_type == _('File Transfer Stopped'):
797                    txt = _('File transfer of %(filename)s to %(name)s '
798                            'stopped.') % {
799                                'filename': filename,
800                                'name': name}
801                    icon_name = 'process-stop'
802                else: # File transfer hash error
803                    txt = _('File transfer of %(filename)s to %(name)s '
804                            'failed.') % {
805                                'filename': filename,
806                                'name': name}
807                    icon_name = 'process-stop'
808        else:
809            txt = ''
810            icon_name = None
811
812        if (app.settings.get('notify_on_file_complete') and
813                (app.settings.get('autopopupaway') or
814                app.connections[account].status in ('online', 'chat'))):
815            # We want to be notified and we are online/chat or we don't mind
816            # to be bugged when away/na/busy
817            app.notification.popup(
818                event_type,
819                jid,
820                account,
821                msg_type,
822                icon_name=icon_name,
823                title=event_type,
824                text=txt)
825
826    def handle_event_signed_in(self, obj):
827        """
828        SIGNED_IN event is emitted when we sign in, so handle it
829        """
830        # ('SIGNED_IN', account, ())
831        # block signed in notifications for 30 seconds
832
833        # Add our own JID into the DB
834        app.storage.archive.insert_jid(obj.conn.get_own_jid().bare)
835        account = obj.conn.name
836        app.block_signed_in_notifications[account] = True
837
838        pep_supported = obj.conn.get_module('PEP').supported
839
840        if obj.conn.get_module('MAM').available:
841            obj.conn.get_module('MAM').request_archive_on_signin()
842
843        # enable location listener
844        if (pep_supported and app.is_installed('GEOCLUE') and
845                app.settings.get_account_setting(account, 'publish_location')):
846            location.enable()
847
848        if ask_for_status_message(obj.conn.status, signin=True):
849            open_window('StatusChange', status=obj.conn.status)
850
851    def send_httpupload(self, chat_control, path=None):
852        if path is not None:
853            self._send_httpupload(chat_control, path)
854            return
855
856        accept_cb = partial(self.on_file_dialog_ok, chat_control)
857        FileChooserDialog(accept_cb,
858                          select_multiple=True,
859                          transient_for=chat_control.parent_win.window)
860
861    def on_file_dialog_ok(self, chat_control, paths):
862        for path in paths:
863            self._send_httpupload(chat_control, path)
864
865    def _send_httpupload(self, chat_control, path):
866        con = app.connections[chat_control.account]
867        try:
868            transfer = con.get_module('HTTPUpload').make_transfer(
869                path,
870                chat_control.encryption,
871                chat_control.contact,
872                chat_control.is_groupchat)
873        except FileError as error:
874            app.nec.push_incoming_event(InformationEvent(
875                None, dialog_name='open-file-error2', args=error))
876            return
877
878        transfer.connect('cancel', self._on_cancel_upload)
879        transfer.connect('state-changed',
880                         self._on_http_upload_state_changed)
881        FileTransferProgress(transfer)
882        con.get_module('HTTPUpload').start_transfer(transfer)
883
884    @staticmethod
885    def _on_http_upload_state_changed(transfer, _signal_name, state):
886        if state.is_finished:
887            uri = transfer.get_transformed_uri()
888
889            type_ = 'chat'
890            if transfer.is_groupchat:
891                type_ = 'groupchat'
892
893            message = OutgoingMessage(account=transfer.account,
894                                      contact=transfer.contact,
895                                      message=uri,
896                                      type_=type_,
897                                      oob_url=uri)
898
899            client = app.get_client(transfer.account)
900            client.send_message(message)
901
902    @staticmethod
903    def _on_cancel_upload(transfer, _signal_name):
904        client = app.get_client(transfer.account)
905        client.get_module('HTTPUpload').cancel_transfer(transfer)
906
907    @staticmethod
908    def handle_event_metacontacts(obj):
909        app.contacts.define_metacontacts(obj.conn.name, obj.meta_list)
910
911    def handle_event_zc_name_conflict(self, obj):
912        def _on_ok(new_name):
913            app.settings.set_account_setting(obj.conn.name, 'name', new_name)
914            obj.conn.username = new_name
915            obj.conn.change_status(obj.conn.status, obj.conn.status_message)
916
917        def _on_cancel(*args):
918            obj.conn.change_status('offline', '')
919
920        InputDialog(
921            _('Username Conflict'),
922            _('Username Conflict'),
923            _('Please enter a new username for your local account'),
924            [DialogButton.make('Cancel',
925                               callback=_on_cancel),
926             DialogButton.make('Accept',
927                               text=_('_OK'),
928                               callback=_on_ok)],
929            input_str=obj.alt_name,
930            transient_for=self.roster.window).show()
931
932    def handle_event_jingleft_cancel(self, obj):
933        ft = self.instances['file_transfers']
934        file_props = None
935        # get the file_props of our session
936        file_props = FilesProp.getFileProp(obj.conn.name, obj.sid)
937        if not file_props:
938            return
939        ft.set_status(file_props, 'stop')
940        file_props.error = -4 # is it the right error code?
941        ft.show_stopped(obj.jid, file_props, 'Peer cancelled ' +
942                            'the transfer')
943
944    # Jingle AV handling
945    def handle_event_jingle_incoming(self, event):
946        # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
947        # data...))
948        # TODO: conditional blocking if peer is not in roster
949
950        account = event.conn.name
951        content_types = []
952        for item in event.contents:
953            content_types.append(item.media)
954        # check type of jingle session
955        if 'audio' in content_types or 'video' in content_types:
956            # a voip session...
957            # we now handle only voip, so the only thing we will do here is
958            # not to return from function
959            pass
960        else:
961            # unknown session type... it should be declined in common/jingle.py
962            return
963
964        notification_event = events.JingleIncomingEvent(
965            event.fjid, event.sid, content_types)
966
967        ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
968                or self.msg_win_mgr.get_control(event.jid, account))
969        if ctrl:
970            if 'audio' in content_types:
971                ctrl.set_jingle_state(
972                    'audio',
973                    JingleState.CONNECTION_RECEIVED,
974                    event.sid)
975            if 'video' in content_types:
976                ctrl.set_jingle_state(
977                    'video',
978                    JingleState.CONNECTION_RECEIVED,
979                    event.sid)
980            ctrl.add_call_received_message(notification_event)
981
982        if helpers.allow_popup_window(account):
983            app.interface.new_chat_from_jid(account, event.fjid)
984            ctrl.add_call_received_message(notification_event)
985            return
986
987        self.add_event(account, event.fjid, notification_event)
988
989        if helpers.allow_showing_notification(account):
990            heading = _('Incoming Call')
991            contact = app.get_name_from_jid(account, event.jid)
992            text = _('%s is calling') % contact
993            app.notification.popup(
994                heading,
995                event.fjid,
996                account,
997                'jingle-incoming',
998                icon_name='call-start-symbolic',
999                title=heading,
1000                text=text)
1001
1002    def handle_event_jingle_connected(self, event):
1003        # ('JINGLE_CONNECTED', account, (peerjid, sid, media))
1004        if event.media in ('audio', 'video'):
1005            account = event.conn.name
1006            ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
1007                    or self.msg_win_mgr.get_control(event.jid, account))
1008            if ctrl:
1009                con = app.connections[account]
1010                session = con.get_module('Jingle').get_jingle_session(
1011                    event.fjid, event.sid)
1012
1013                if event.media == 'audio':
1014                    content = session.get_content('audio')
1015                    ctrl.set_jingle_state(
1016                        'audio',
1017                        JingleState.CONNECTED,
1018                        event.sid)
1019                if event.media == 'video':
1020                    content = session.get_content('video')
1021                    ctrl.set_jingle_state(
1022                        'video',
1023                        JingleState.CONNECTED,
1024                        event.sid)
1025
1026                # Now, accept the content/sessions.
1027                # This should be done after the chat control is running
1028                if not session.accepted:
1029                    session.approve_session()
1030                for content in event.media:
1031                    session.approve_content(content)
1032
1033    def handle_event_jingle_disconnected(self, event):
1034        # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason))
1035        account = event.conn.name
1036        ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
1037                or self.msg_win_mgr.get_control(event.jid, account))
1038        if ctrl:
1039            if event.media is None:
1040                ctrl.stop_jingle(sid=event.sid, reason=event.reason)
1041            if event.media == 'audio':
1042                ctrl.set_jingle_state(
1043                    'audio',
1044                    JingleState.NULL,
1045                    sid=event.sid,
1046                    reason=event.reason)
1047            if event.media == 'video':
1048                ctrl.set_jingle_state(
1049                    'video',
1050                    JingleState.NULL,
1051                    sid=event.sid,
1052                    reason=event.reason)
1053
1054    def handle_event_jingle_error(self, event):
1055        # ('JINGLE_ERROR', account, (peerjid, sid, reason))
1056        account = event.conn.name
1057        ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
1058                or self.msg_win_mgr.get_control(event.jid, account))
1059        if ctrl and event.sid == ctrl.jingle['audio'].sid:
1060            ctrl.set_jingle_state(
1061                'audio',
1062                JingleState.ERROR,
1063                reason=event.reason)
1064
1065    @staticmethod
1066    def handle_event_roster_item_exchange(obj):
1067        # data = (action in [add, delete, modify], exchange_list, jid_from)
1068        RosterItemExchangeWindow(obj.conn.name, obj.action,
1069                                 obj.exchange_items_list, obj.fjid)
1070
1071    def handle_event_plain_connection(self, event):
1072        ConfirmationDialog(
1073            _('Insecure Connection'),
1074            _('Insecure Connection'),
1075            _('You are about to connect to the account %(account)s '
1076              '(%(server)s) using an insecure connection method. This means '
1077              'conversations will not be encrypted. Connecting PLAIN is '
1078              'strongly discouraged.') % {
1079                  'account': event.account,
1080                  'server': app.get_hostname_from_account(event.account)},
1081            [DialogButton.make('Cancel',
1082                               text=_('_Abort'),
1083                               callback=event.abort),
1084             DialogButton.make('Remove',
1085                               text=_('_Connect Anyway'),
1086                               callback=event.connect)]).show()
1087
1088    def create_core_handlers_list(self):
1089        self.handlers = {
1090            'DB_ERROR': [self.handle_event_db_error],
1091            'file-send-error': [self.handle_event_file_send_error],
1092            'client-cert-passphrase': [
1093                self.handle_event_client_cert_passphrase],
1094            'connection-lost': [self.handle_event_connection_lost],
1095            'file-request-error': [self.handle_event_file_request_error],
1096            'file-request-received': [self.handle_event_file_request],
1097            'muc-invitation': [self.handle_event_gc_invitation],
1098            'muc-decline': [self.handle_event_gc_decline],
1099            'http-auth-received': [self.handle_event_http_auth],
1100            'information': [self.handle_event_information],
1101            'iq-error-received': [self.handle_event_iq_error],
1102            'jingle-connected-received': [self.handle_event_jingle_connected],
1103            'jingle-disconnected-received': [
1104                self.handle_event_jingle_disconnected],
1105            'jingle-error-received': [self.handle_event_jingle_error],
1106            'jingle-request-received': [self.handle_event_jingle_incoming],
1107            'jingle-ft-cancelled-received': [self.handle_event_jingleft_cancel],
1108            'message-not-sent': [self.handle_event_msgnotsent],
1109            'message-sent': [self.handle_event_msgsent],
1110            'metacontacts-received': [self.handle_event_metacontacts],
1111            'our-show': [self.handle_event_status],
1112            'password-required': [self.handle_event_password_required],
1113            'plain-connection': [self.handle_event_plain_connection],
1114            'presence-received': [self.handle_event_presence],
1115            'roster-info': [self.handle_event_roster_info],
1116            'roster-item-exchange-received': \
1117                [self.handle_event_roster_item_exchange],
1118            'signed-in': [self.handle_event_signed_in],
1119            'subscribe-presence-received': [
1120                self.handle_event_subscribe_presence],
1121            'subscribed-presence-received': [
1122                self.handle_event_subscribed_presence],
1123            'unsubscribed-presence-received': [
1124                self.handle_event_unsubscribed_presence],
1125            'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
1126            'read-state-sync': [self.handle_event_read_state_sync],
1127        }
1128
1129    def register_core_handlers(self):
1130        """
1131        Register core handlers in Global Events Dispatcher (GED).
1132
1133        This is part of rewriting whole events handling system to use GED.
1134        """
1135        for event_name, event_handlers in self.handlers.items():
1136            for event_handler in event_handlers:
1137                prio = ged.GUI1
1138                if isinstance(event_handler, tuple):
1139                    prio = event_handler[1]
1140                    event_handler = event_handler[0]
1141                app.ged.register_event_handler(event_name, prio,
1142                    event_handler)
1143
1144################################################################################
1145### Methods dealing with app.events
1146################################################################################
1147
1148    def add_event(self, account, jid, event):
1149        """
1150        Add an event to the app.events var
1151        """
1152        # We add it to the app.events queue
1153        # Do we have a queue?
1154        jid = app.get_jid_without_resource(jid)
1155        no_queue = len(app.events.get_events(account, jid)) == 0
1156        # event can be in common.events.*
1157        # event_type can be in advancedNotificationWindow.events_list
1158        event_types = {'file-request': 'ft_request',
1159            'file-completed': 'ft_finished'}
1160        event_type = event_types.get(event.type_)
1161        show_in_roster = get_show_in_roster(event_type, jid)
1162        show_in_systray = get_show_in_systray(event_type, account, jid)
1163        event.show_in_roster = show_in_roster
1164        event.show_in_systray = show_in_systray
1165        app.events.add_event(account, jid, event)
1166
1167        self.roster.show_title()
1168        if no_queue:  # We didn't have a queue: we change icons
1169            if app.contacts.get_contact_with_highest_priority(account, jid):
1170                self.roster.draw_contact(jid, account)
1171            else:
1172                groupchat = event.type_ == 'gc-invitation'
1173                self.roster.add_to_not_in_the_roster(
1174                    account, jid, groupchat=groupchat)
1175
1176        # Select the big brother contact in roster, it's visible because it has
1177        # events.
1178        family = app.contacts.get_metacontacts_family(account, jid)
1179        if family:
1180            _nearby_family, bb_jid, bb_account = \
1181                app.contacts.get_nearby_family_and_big_brother(family,
1182                account)
1183        else:
1184            bb_jid, bb_account = jid, account
1185        self.roster.select_contact(bb_jid, bb_account)
1186
1187    def handle_event(self, account, fjid, type_):
1188        if type_ in ('connection-lost', 'connection-failed'):
1189            app.interface.roster.window.present()
1190            return
1191
1192        w = None
1193        ctrl = None
1194
1195        resource = app.get_resource_from_jid(fjid)
1196        jid = app.get_jid_without_resource(fjid)
1197
1198        if type_ in ('printed_gc_msg', 'printed_marked_gc_msg', 'gc_msg'):
1199            w = self.msg_win_mgr.get_window(jid, account)
1200            if jid in self.minimized_controls[account]:
1201                self.roster.on_groupchat_maximized(None, jid, account)
1202                return
1203            ctrl = self.msg_win_mgr.get_gc_control(jid, account)
1204
1205        elif type_ in ('printed_chat', 'chat', ''):
1206            # '' is for log in/out notifications
1207
1208            ctrl = self.msg_win_mgr.search_control(jid, account, resource)
1209
1210            if not ctrl:
1211                highest_contact = app.contacts.\
1212                    get_contact_with_highest_priority(account, jid)
1213                # jid can have a window if this resource was lower when he sent
1214                # message and is now higher because the other one is offline
1215                if resource and highest_contact.resource == resource and \
1216                not self.msg_win_mgr.has_window(jid, account):
1217                    # remove resource of events too
1218                    app.events.change_jid(account, fjid, jid)
1219                    resource = None
1220                    fjid = jid
1221
1222                contact = None
1223                if resource:
1224                    contact = app.contacts.get_contact(account, jid, resource)
1225                if not contact:
1226                    contact = highest_contact
1227                if not contact:
1228                    # Maybe we deleted the contact from the roster
1229                    return
1230
1231                ctrl = self.new_chat(contact, account, resource=resource)
1232
1233                app.last_message_time[account][jid] = 0 # long time ago
1234
1235            w = ctrl.parent_win
1236        elif type_ in ('printed_pm', 'pm'):
1237
1238            ctrl = self.msg_win_mgr.get_control(fjid, account)
1239
1240            if not ctrl:
1241                room_jid = jid
1242                nick = resource
1243                gc_contact = app.contacts.get_gc_contact(
1244                    account, room_jid, nick)
1245                ctrl = self.new_private_chat(gc_contact, account)
1246
1247            w = ctrl.parent_win
1248        elif type_ in ('file-request', 'file-request-error',
1249        'file-send-error', 'file-error', 'file-stopped', 'file-completed',
1250        'file-hash-error', 'jingle-incoming'):
1251            # Get the first single message event
1252            event = app.events.get_first_event(account, fjid, type_)
1253            if not event:
1254                # default to jid without resource
1255                event = app.events.get_first_event(account, jid, type_)
1256                if not event:
1257                    return
1258                # Open the window
1259                self.roster.open_event(account, jid, event)
1260            else:
1261                # Open the window
1262                self.roster.open_event(account, fjid, event)
1263        elif type_ == 'gc-invitation':
1264            event = app.events.get_first_event(account, jid, type_)
1265            if event is None:
1266                return
1267            open_window('GroupChatInvitation',
1268                        account=account,
1269                        event=event)
1270            app.events.remove_events(account, jid, event)
1271            self.roster.draw_contact(jid, account)
1272        elif type_ == 'subscription_request':
1273            event = app.events.get_first_event(account, jid, type_)
1274            if event is None:
1275                return
1276            open_window('SubscriptionRequest',
1277                        account=account,
1278                        jid=jid,
1279                        text=event.text,
1280                        user_nick=event.nick)
1281            app.events.remove_events(account, jid, event)
1282            self.roster.draw_contact(jid, account)
1283        elif type_ == 'unsubscribed':
1284            event = app.events.get_first_event(account, jid, type_)
1285            if event is None:
1286                return
1287            self.show_unsubscribed_dialog(account, event.contact)
1288            app.events.remove_events(account, jid, event)
1289            self.roster.draw_contact(jid, account)
1290        if w:
1291            w.set_active_tab(ctrl)
1292            w.window.present()
1293            # Using isinstance here because we want to catch all derived types
1294            if isinstance(ctrl, ChatControlBase):
1295                ctrl.scroll_to_end()
1296
1297################################################################################
1298### Methods for opening new messages controls
1299################################################################################
1300
1301    def show_groupchat(self, account, room_jid):
1302        minimized_control = self.minimized_controls[account].get(room_jid)
1303        if minimized_control is not None:
1304            self.roster.on_groupchat_maximized(None, room_jid, account)
1305            return True
1306
1307        if self.msg_win_mgr.has_window(room_jid, account):
1308            gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account)
1309            # FIXME: Access message window directly
1310            gc_ctrl.parent_win.set_active_tab(gc_ctrl)
1311            return True
1312        return False
1313
1314    def create_groupchat_control(self, account, room_jid, muc_data,
1315                                 minimize=False):
1316        avatar_sha = app.storage.cache.get_muc_avatar_sha(room_jid)
1317        contact = app.contacts.create_contact(jid=room_jid,
1318                                              account=account,
1319                                              groups=[_('Group chats')],
1320                                              sub='none',
1321                                              avatar_sha=avatar_sha,
1322                                              groupchat=True)
1323        app.contacts.add_contact(account, contact)
1324
1325        if minimize:
1326            control = GroupchatControl(None, contact, muc_data, account)
1327            app.interface.minimized_controls[account][room_jid] = control
1328            self.roster.add_groupchat(room_jid, account)
1329
1330        else:
1331            mw = self.msg_win_mgr.get_window(room_jid, account)
1332            if not mw:
1333                mw = self.msg_win_mgr.create_window(contact,
1334                                                    account,
1335                                                    ControlType.GROUPCHAT)
1336            control = GroupchatControl(mw, contact, muc_data, account)
1337            mw.new_tab(control)
1338            mw.set_active_tab(control)
1339
1340    @staticmethod
1341    def _create_muc_data(account, room_jid, nick, password, config):
1342        if not nick:
1343            nick = get_group_chat_nick(account, room_jid)
1344
1345        # Fetch data from bookmarks
1346        client = app.get_client(account)
1347        bookmark = client.get_module('Bookmarks').get_bookmark(room_jid)
1348        if bookmark is not None:
1349            if bookmark.password is not None:
1350                password = bookmark.password
1351
1352        return MUCData(room_jid, nick, password, config)
1353
1354    def create_groupchat(self, account, room_jid, config=None):
1355        muc_data = self._create_muc_data(account, room_jid, None, None, config)
1356        self.create_groupchat_control(account, room_jid, muc_data)
1357        app.connections[account].get_module('MUC').create(muc_data)
1358
1359    def show_or_join_groupchat(self, account, room_jid, **kwargs):
1360        if self.show_groupchat(account, room_jid):
1361            return
1362        self.join_groupchat(account, room_jid, **kwargs)
1363
1364    def join_groupchat(self,
1365                       account,
1366                       room_jid,
1367                       password=None,
1368                       nick=None,
1369                       minimized=False):
1370
1371        if not app.account_is_available(account):
1372            return
1373
1374        muc_data = self._create_muc_data(account,
1375                                         room_jid,
1376                                         nick,
1377                                         password,
1378                                         None)
1379        self.create_groupchat_control(
1380            account, room_jid, muc_data, minimize=minimized)
1381
1382        app.connections[account].get_module('MUC').join(muc_data)
1383
1384    def new_private_chat(self, gc_contact, account, session=None):
1385        conn = app.connections[account]
1386        if not session and gc_contact.get_full_jid() in conn.sessions:
1387            sessions = [s for s in conn.sessions[gc_contact.get_full_jid()].\
1388                values() if isinstance(s, ChatControlSession)]
1389
1390            # look for an existing session with a chat control
1391            for s in sessions:
1392                if s.control:
1393                    session = s
1394                    break
1395            if not session and sessions:
1396                # there are no sessions with chat controls, just take the first
1397                # one
1398                session = sessions[0]
1399        if not session:
1400            # couldn't find an existing ChatControlSession, just make a new one
1401            session = conn.make_new_session(gc_contact.get_full_jid(), None,
1402                'pm')
1403
1404        contact = gc_contact.as_contact()
1405        if not session.control:
1406            message_window = self.msg_win_mgr.get_window(
1407                gc_contact.get_full_jid(), account)
1408            if not message_window:
1409                message_window = self.msg_win_mgr.create_window(
1410                    contact, account, ControlType.PRIVATECHAT)
1411
1412            session.control = PrivateChatControl(message_window, gc_contact,
1413                contact, account, session)
1414            message_window.new_tab(session.control)
1415
1416        if app.events.get_events(account, gc_contact.get_full_jid()):
1417            # We call this here to avoid race conditions with widget validation
1418            session.control.read_queue()
1419
1420        return session.control
1421
1422    def new_chat(self, contact, account, resource=None, session=None):
1423        # Get target window, create a control, and associate it with the window
1424        fjid = contact.jid
1425        if resource:
1426            fjid += '/' + resource
1427
1428        mw = self.msg_win_mgr.get_window(fjid, account)
1429        if not mw:
1430            mw = self.msg_win_mgr.create_window(
1431                contact, account, ControlType.CHAT, resource)
1432
1433        chat_control = ChatControl(mw, contact, account, session, resource)
1434
1435        mw.new_tab(chat_control)
1436
1437        if app.events.get_events(account, fjid):
1438            # We call this here to avoid race conditions with widget validation
1439            chat_control.read_queue()
1440
1441        return chat_control
1442
1443    def new_chat_from_jid(self, account, fjid, message=None):
1444        jid, resource = app.get_room_and_nick_from_fjid(fjid)
1445        contact = app.contacts.get_contact(account, jid, resource)
1446        added_to_roster = False
1447        if not contact:
1448            added_to_roster = True
1449            contact = self.roster.add_to_not_in_the_roster(account, jid,
1450                resource=resource)
1451
1452        ctrl = self.msg_win_mgr.get_control(fjid, account)
1453
1454        if not ctrl:
1455            ctrl = self.new_chat(contact, account,
1456                resource=resource)
1457            if app.events.get_events(account, fjid):
1458                ctrl.read_queue()
1459
1460        if message:
1461            buffer_ = ctrl.msg_textview.get_buffer()
1462            buffer_.set_text(message)
1463        mw = ctrl.parent_win
1464        mw.set_active_tab(ctrl)
1465        # For JEP-0172
1466        if added_to_roster:
1467            ctrl.user_nick = app.nicks[account]
1468
1469        return ctrl
1470
1471    def on_open_chat_window(self, widget, contact, account, resource=None,
1472    session=None):
1473        # Get the window containing the chat
1474        fjid = contact.jid
1475
1476        if resource:
1477            fjid += '/' + resource
1478
1479        ctrl = None
1480
1481        if session:
1482            ctrl = session.control
1483        if not ctrl:
1484            win = self.msg_win_mgr.get_window(fjid, account)
1485
1486            if win:
1487                ctrl = win.get_control(fjid, account)
1488
1489        if not ctrl:
1490            ctrl = self.new_chat(contact, account, resource=resource,
1491                session=session)
1492            # last message is long time ago
1493            app.last_message_time[account][ctrl.get_full_jid()] = 0
1494
1495        win = ctrl.parent_win
1496
1497        win.set_active_tab(ctrl)
1498
1499        if app.connections[account].is_zeroconf and \
1500        app.connections[account].status == 'offline':
1501            ctrl = win.get_control(fjid, account)
1502            if ctrl:
1503                ctrl.got_disconnected()
1504
1505################################################################################
1506### Other Methods
1507################################################################################
1508
1509    @staticmethod
1510    def create_account(account,
1511                       username,
1512                       domain,
1513                       password,
1514                       proxy_name,
1515                       custom_host,
1516                       anonymous=False):
1517
1518        account_label = f'{username}@{domain}'
1519        if anonymous:
1520            username = 'anon'
1521            account_label = f'anon@{domain}'
1522
1523        config = {}
1524        config['active'] = False
1525        config['name'] = username
1526        config['resource'] = 'gajim.%s' % helpers.get_random_string(8)
1527        config['account_label'] = account_label
1528        config['account_color'] = get_color_for_account(
1529            '%s@%s' % (username, domain))
1530        config['hostname'] = domain
1531        config['savepass'] = True
1532        config['anonymous_auth'] = anonymous
1533        config['autoconnect'] = True
1534        config['sync_with_global_status'] = True
1535
1536        if proxy_name is not None:
1537            config['proxy'] = proxy_name
1538
1539        use_custom_host = custom_host is not None
1540        config['use_custom_host'] = use_custom_host
1541        if custom_host:
1542            host, _protocol, type_ = custom_host
1543            host, port = host.rsplit(':', maxsplit=1)
1544            config['custom_port'] = int(port)
1545            config['custom_host'] = host
1546            config['custom_type'] = type_.value
1547
1548        app.settings.add_account(account)
1549        for opt in config:
1550            app.settings.set_account_setting(account, opt, config[opt])
1551
1552        # Password module depends on existing config
1553        passwords.save_password(account, password)
1554
1555        app.css_config.refresh()
1556
1557        # Action must be added before account window is updated
1558        app.app.add_account_actions(account)
1559
1560        window = get_app_window('AccountsWindow')
1561        if window is not None:
1562            window.add_account(account)
1563
1564    def enable_account(self, account):
1565        if account == app.ZEROCONF_ACC_NAME:
1566            app.connections[account] = connection_zeroconf.ConnectionZeroconf(
1567                account)
1568        else:
1569            app.connections[account] = Client(account)
1570
1571        app.plugin_manager.register_modules_for_account(
1572            app.connections[account])
1573
1574        # update variables
1575        self.instances[account] = {
1576            'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {},
1577            'sub_request': {}}
1578        self.minimized_controls[account] = {}
1579        app.groups[account] = {}
1580        app.contacts.add_account(account)
1581        app.gc_connected[account] = {}
1582        app.automatic_rooms[account] = {}
1583        app.newly_added[account] = []
1584        app.to_be_removed[account] = []
1585        if account == app.ZEROCONF_ACC_NAME:
1586            app.nicks[account] = app.ZEROCONF_ACC_NAME
1587        else:
1588            app.nicks[account] = app.settings.get_account_setting(account,
1589                                                                  'name')
1590        app.block_signed_in_notifications[account] = True
1591        app.last_message_time[account] = {}
1592        # refresh roster
1593        if len(app.connections) >= 2:
1594            # Do not merge accounts if only one exists
1595            self.roster.regroup = app.settings.get('mergeaccounts')
1596        else:
1597            self.roster.regroup = False
1598        self.roster.setup_and_draw_roster()
1599        gui_menu_builder.build_accounts_menu()
1600        self.roster.send_status(account, 'online', '')
1601        app.settings.set_account_setting(account, 'active', True)
1602        app.app.update_app_actions_state()
1603        window = get_app_window('AccountsWindow')
1604        if window is not None:
1605            GLib.idle_add(window.enable_account, account, True)
1606
1607    def disable_account(self, account):
1608        self.roster.close_all(account, force=True)
1609        for jid in self.minimized_controls[account]:
1610            ctrl = self.minimized_controls[account][jid]
1611            ctrl.shutdown()
1612
1613        for win in get_app_windows(account):
1614            # Close all account specific windows, except the RemoveAccount
1615            # dialog. It shows if the removal was successful.
1616            if type(win).__name__ == 'RemoveAccount':
1617                continue
1618            win.destroy()
1619
1620        if account == app.ZEROCONF_ACC_NAME:
1621            app.connections[account].disable_account()
1622        app.connections[account].cleanup()
1623        del app.connections[account]
1624        del self.instances[account]
1625        del self.minimized_controls[account]
1626        del app.nicks[account]
1627        del app.block_signed_in_notifications[account]
1628        del app.groups[account]
1629        app.contacts.remove_account(account)
1630        del app.gc_connected[account]
1631        del app.automatic_rooms[account]
1632        del app.to_be_removed[account]
1633        del app.newly_added[account]
1634        del app.last_message_time[account]
1635        if len(app.connections) >= 2:
1636            # Do not merge accounts if only one exists
1637            self.roster.regroup = app.settings.get('mergeaccounts')
1638        else:
1639            self.roster.regroup = False
1640        app.settings.set_account_setting(account, 'roster_version', '')
1641        self.roster.setup_and_draw_roster()
1642        self.roster.update_status_selector()
1643        gui_menu_builder.build_accounts_menu()
1644        app.settings.set_account_setting(account, 'active', False)
1645        app.app.update_app_actions_state()
1646
1647    def remove_account(self, account):
1648        if app.settings.get_account_setting(account, 'active'):
1649            self.disable_account(account)
1650
1651        app.storage.cache.remove_roster(account)
1652        # Delete password must be before del_per() because it calls set_per()
1653        # which would recreate the account with defaults values if not found
1654        passwords.delete_password(account)
1655        app.settings.remove_account(account)
1656        app.app.remove_account_actions(account)
1657
1658        window = get_app_window('AccountsWindow')
1659        if window is not None:
1660            window.remove_account(account)
1661
1662    def autoconnect(self):
1663        """
1664        Auto connect at startup
1665        """
1666
1667        for account in app.connections:
1668            if not app.settings.get_account_setting(account, 'autoconnect'):
1669                continue
1670
1671            status = 'online'
1672            status_message = ''
1673
1674            if app.settings.get_account_setting(account, 'restore_last_status'):
1675                status = app.settings.get_account_setting(account, 'last_status')
1676                status_message = app.settings.get_account_setting(
1677                    account, 'last_status_msg')
1678                status_message = helpers.from_one_line(status_message)
1679
1680            self.roster.send_status(account, status, status_message)
1681
1682    def change_status(self, status=None):
1683        # status=None means we want to change the message only
1684
1685        ask = ask_for_status_message(status)
1686
1687        if status is None:
1688            status = helpers.get_global_show()
1689
1690        if ask:
1691            open_window('StatusChange', status=status)
1692            return
1693
1694        for account in app.connections:
1695            if not app.settings.get_account_setting(account,
1696                                                    'sync_with_global_status'):
1697                continue
1698
1699            message = app.get_client(account).status_message
1700            self.roster.send_status(account, status, message)
1701
1702    def change_account_status(self, account, status=None):
1703        # status=None means we want to change the message only
1704
1705        ask = ask_for_status_message(status)
1706
1707        client = app.get_client(account)
1708        if status is None:
1709            status = client.status
1710
1711        if ask:
1712            open_window('StatusChange', status=status, account=account)
1713            return
1714
1715        message = client.status_message
1716        self.roster.send_status(account, status, message)
1717
1718    def show_systray(self):
1719        if not app.is_display(Display.WAYLAND):
1720            self.systray_enabled = True
1721            self.systray.show_icon()
1722
1723    def hide_systray(self):
1724        if not app.is_display(Display.WAYLAND):
1725            self.systray_enabled = False
1726            self.systray.hide_icon()
1727
1728    def process_connections(self):
1729        """
1730        Called each foo (200) milliseconds. Check for idlequeue timeouts
1731        """
1732        try:
1733            app.idlequeue.process()
1734        except Exception:
1735            # Otherwise, an exception will stop our loop
1736
1737            if sys.platform == 'win32':
1738                # On Windows process() calls select.select(), so we need this
1739                # executed as often as possible.
1740                # Adding it directly with GLib.idle_add() causes Gajim to use
1741                # too much CPU time. That's why its added with 1ms timeout.
1742                # On Linux only alarms are checked in process(), so we use
1743                # a bigger timeout
1744                timeout, in_seconds = 1, None
1745            else:
1746                timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT
1747
1748            if in_seconds:
1749                GLib.timeout_add_seconds(timeout, self.process_connections)
1750            else:
1751                GLib.timeout_add(timeout, self.process_connections)
1752            raise
1753        return True # renew timeout (loop for ever)
1754
1755    @staticmethod
1756    def save_config():
1757        app.settings.save()
1758
1759    def update_avatar(self, account=None, jid=None,
1760                      contact=None, room_avatar=False):
1761        self.avatar_storage.invalidate_cache(jid or contact.get_full_jid())
1762        if room_avatar:
1763            app.nec.push_incoming_event(
1764                NetworkEvent('update-room-avatar', account=account, jid=jid))
1765        elif contact is None:
1766            app.nec.push_incoming_event(
1767                NetworkEvent('update-roster-avatar', account=account, jid=jid))
1768        else:
1769            app.nec.push_incoming_event(NetworkEvent('update-gc-avatar',
1770                                                     contact=contact,
1771                                                     room_jid=contact.room_jid))
1772
1773    def save_avatar(self, data):
1774        return self.avatar_storage.save_avatar(data)
1775
1776    def get_avatar(self, contact, size, scale, show=None, pixbuf=False):
1777        if pixbuf:
1778            return self.avatar_storage.get_pixbuf(contact, size, scale, show)
1779        return self.avatar_storage.get_surface(contact, size, scale, show)
1780
1781    def avatar_exists(self, filename):
1782        return self.avatar_storage.get_avatar_path(filename) is not None
1783
1784    # does JID exist only within a groupchat?
1785    def is_pm_contact(self, fjid, account):
1786        bare_jid = app.get_jid_without_resource(fjid)
1787
1788        gc_ctrl = self.msg_win_mgr.get_gc_control(bare_jid, account)
1789
1790        if not gc_ctrl and \
1791        bare_jid in self.minimized_controls[account]:
1792            gc_ctrl = self.minimized_controls[account][bare_jid]
1793
1794        return gc_ctrl and gc_ctrl.is_groupchat
1795
1796    @staticmethod
1797    def create_ipython_window():
1798        # Check if IPython is installed
1799        ipython = find_spec('IPython')
1800        is_installed = ipython is not None
1801        if not is_installed:
1802            # Abort early to avoid tracebacks
1803            print('IPython is not installed')
1804            return
1805        try:
1806            from gajim.dev.ipython_view import IPythonView
1807        except ImportError:
1808            print('ipython_view not found')
1809            return
1810        from gi.repository import Pango
1811
1812        if os.name == 'nt':
1813            font = 'Lucida Console 9'
1814        else:
1815            font = 'Luxi Mono 10'
1816
1817        window = Gtk.Window()
1818        window.set_title(_('Gajim: IPython Console'))
1819        window.set_size_request(750, 550)
1820        window.set_resizable(True)
1821        sw = Gtk.ScrolledWindow()
1822        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1823        view = IPythonView()
1824        view.override_font(Pango.FontDescription(font))
1825        view.set_wrap_mode(Gtk.WrapMode.CHAR)
1826        sw.add(view)
1827        window.add(sw)
1828        window.show_all()
1829        def on_delete(win, event):
1830            win.hide()
1831            return True
1832        window.connect('delete_event', on_delete)
1833        view.updateNamespace({'gajim': app})
1834        app.ipython_window = window
1835
1836    def _network_status_changed(self, monitor, _param):
1837        connected = monitor.get_network_available()
1838        if connected == self.network_state:
1839            return
1840
1841        self.network_state = connected
1842        if connected:
1843            log.info('Network connection available')
1844        else:
1845            log.info('Network connection lost')
1846            for connection in app.connections.values():
1847                if (connection.state.is_connected or
1848                        connection.state.is_available):
1849                    connection.disconnect(gracefully=False, reconnect=True)
1850
1851    def create_zeroconf_default_config(self):
1852        if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'name'):
1853            return
1854        log.info('Creating zeroconf account')
1855        app.settings.add_account(app.ZEROCONF_ACC_NAME)
1856        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1857                                         'autoconnect',
1858                                         True)
1859        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1860                                         'no_log_for',
1861                                         '')
1862        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1863                                         'password',
1864                                         'zeroconf')
1865        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1866                                         'sync_with_global_status',
1867                                         True)
1868        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1869                                         'custom_port',
1870                                         5298)
1871        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1872                                         'is_zeroconf',
1873                                         True)
1874        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1875                                         'use_ft_proxies',
1876                                         False)
1877        app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
1878                                         'active',
1879                                         False)
1880
1881    def check_for_updates(self):
1882        if not app.settings.get('check_for_update'):
1883            return
1884
1885        now = datetime.now()
1886        last_check = app.settings.get('last_update_check')
1887        if not last_check:
1888            def _on_cancel():
1889                app.settings.set('check_for_update', False)
1890
1891            def _on_check():
1892                self._get_latest_release()
1893
1894            ConfirmationDialog(
1895                _('Update Check'),
1896                _('Gajim Update Check'),
1897                _('Search for Gajim updates periodically?'),
1898                [DialogButton.make('Cancel',
1899                                   text=_('_No'),
1900                                   callback=_on_cancel),
1901                 DialogButton.make('Accept',
1902                                   text=_('_Search Periodically'),
1903                                   callback=_on_check)]).show()
1904            return
1905
1906        last_check_time = datetime.strptime(last_check, '%Y-%m-%d %H:%M')
1907        if (now - last_check_time).days < 7:
1908            return
1909
1910        self._get_latest_release()
1911
1912    def _get_latest_release(self):
1913        log.info('Checking for Gajim updates')
1914        session = Soup.Session()
1915        session.props.user_agent = 'Gajim %s' % app.version
1916        message = Soup.Message.new('GET', 'https://gajim.org/current-version.json')
1917        session.queue_message(message, self._on_update_checked)
1918
1919    def _on_update_checked(self, _session, message):
1920        now = datetime.now()
1921        app.settings.set('last_update_check', now.strftime('%Y-%m-%d %H:%M'))
1922
1923        body = message.props.response_body.data
1924        if not body:
1925            log.warning('Could not reach gajim.org for update check')
1926            return
1927
1928        data = json.loads(body)
1929        latest_version = data['current_version']
1930
1931        if V(latest_version) > V(app.version):
1932            def _on_cancel(is_checked):
1933                if is_checked:
1934                    app.settings.set('check_for_update', False)
1935
1936            def _on_update(is_checked):
1937                if is_checked:
1938                    app.settings.set('check_for_update', False)
1939                helpers.open_uri('https://gajim.org/download')
1940
1941            ConfirmationCheckDialog(
1942                _('Update Available'),
1943                _('Gajim Update Available'),
1944                _('There is an update available for Gajim '
1945                  '(latest version: %s)') % str(latest_version),
1946                _('_Do not show again'),
1947                [DialogButton.make('Cancel',
1948                                    text=_('_Later'),
1949                                    callback=_on_cancel),
1950                 DialogButton.make('Accept',
1951                                    text=_('_Update Now'),
1952                                    callback=_on_update)]).show()
1953        else:
1954            log.info('Gajim is up to date')
1955
1956    def run(self, application):
1957        if app.settings.get('trayicon') != 'never':
1958            self.show_systray()
1959
1960        self.roster = roster_window.RosterWindow(application)
1961        if self.msg_win_mgr.mode == \
1962        MessageWindowMgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER:
1963            self.msg_win_mgr.create_window(None, None, None)
1964
1965        # Creating plugin manager
1966        from gajim import plugins
1967        app.plugin_manager = plugins.PluginManager()
1968        app.plugin_manager.init_plugins()
1969
1970        self.roster._before_fill()
1971        for account in app.connections:
1972            app.connections[account].get_module('Roster').load_roster()
1973        self.roster._after_fill()
1974
1975        # get instances for windows/dialogs that will show_all()/hide()
1976        self.instances['file_transfers'] = FileTransfersWindow()
1977
1978        GLib.timeout_add(100, self.autoconnect)
1979        if sys.platform == 'win32':
1980            timeout, in_seconds = 20, None
1981        else:
1982            timeout, in_seconds = app.idlequeue.PROCESS_TIMEOUT
1983
1984        if in_seconds:
1985            GLib.timeout_add_seconds(timeout, self.process_connections)
1986        else:
1987            GLib.timeout_add(timeout, self.process_connections)
1988
1989        def remote_init():
1990            if app.settings.get('remote_control'):
1991                try:
1992                    from gajim import remote_control
1993                    remote_control.GajimRemote()
1994                except Exception:
1995                    pass
1996        GLib.timeout_add_seconds(5, remote_init)
1997
1998    def __init__(self):
1999        app.interface = self
2000        app.thread_interface = ThreadInterface
2001        # This is the manager and factory of message windows set by the module
2002        self.msg_win_mgr = None
2003        self.minimized_controls = {}
2004        self.pass_dialog = {}
2005        self.db_error_dialog = None
2006
2007        self.handlers = {}
2008        self.roster = None
2009
2010        self.avatar_storage = AvatarStorage()
2011
2012        # Load CSS files
2013        app.load_css_config()
2014
2015        app.storage.archive.reset_shown_unread_messages()
2016
2017        for account in app.settings.get_accounts():
2018            if app.settings.get_account_setting(account, 'is_zeroconf'):
2019                app.ZEROCONF_ACC_NAME = account
2020                break
2021
2022        app.idlequeue = idlequeue.get_idlequeue()
2023        # resolve and keep current record of resolved hosts
2024        app.socks5queue = socks5.SocksQueue(app.idlequeue,
2025            self.handle_event_file_rcv_completed,
2026            self.handle_event_file_progress,
2027            self.handle_event_file_error)
2028        app.proxy65_manager = proxy65_manager.Proxy65Manager(app.idlequeue)
2029        app.default_session_type = ChatControlSession
2030
2031        # Creating Network Events Controller
2032        from gajim.common import nec
2033        app.nec = nec.NetworkEventsController()
2034        app.notification = Notification()
2035
2036        self.create_core_handlers_list()
2037        self.register_core_handlers()
2038
2039        # self.create_zeroconf_default_config()
2040        # if app.settings.get_account_setting(app.ZEROCONF_ACC_NAME, 'active') \
2041        # and app.is_installed('ZEROCONF'):
2042        #     app.connections[app.ZEROCONF_ACC_NAME] = \
2043        #         connection_zeroconf.ConnectionZeroconf(app.ZEROCONF_ACC_NAME)
2044
2045        for account in app.settings.get_accounts():
2046            if (not app.settings.get_account_setting(account, 'is_zeroconf') and
2047                    app.settings.get_account_setting(account, 'active')):
2048                app.connections[account] = Client(account)
2049
2050        self.instances = {}
2051
2052        for a in app.connections:
2053            self.instances[a] = {'infos': {}, 'disco': {}, 'gc_config': {},
2054                'search': {}, 'sub_request': {}}
2055            self.minimized_controls[a] = {}
2056            app.contacts.add_account(a)
2057            app.groups[a] = {}
2058            app.gc_connected[a] = {}
2059            app.automatic_rooms[a] = {}
2060            app.newly_added[a] = []
2061            app.to_be_removed[a] = []
2062            app.nicks[a] = app.settings.get_account_setting(a, 'name')
2063            app.block_signed_in_notifications[a] = True
2064            app.last_message_time[a] = {}
2065
2066        if sys.platform not in ('win32', 'darwin'):
2067            logind.enable()
2068            music_track.enable()
2069        else:
2070            GLib.timeout_add_seconds(20, self.check_for_updates)
2071
2072        idle.Monitor.set_interval(app.settings.get('autoawaytime') * 60,
2073                                  app.settings.get('autoxatime') * 60)
2074
2075        self.systray_enabled = False
2076
2077        if not app.is_display(Display.WAYLAND):
2078            from gajim.gui import statusicon
2079            self.systray = statusicon.StatusIcon()
2080
2081        if sys.platform in ('win32', 'darwin'):
2082            from gajim.gui.emoji_chooser import emoji_chooser
2083            emoji_chooser.load()
2084
2085        self.last_ftwindow_update = 0
2086
2087        self._network_monitor = Gio.NetworkMonitor.get_default()
2088        self._network_monitor.connect('notify::network-available',
2089                                     self._network_status_changed)
2090        self.network_state = self._network_monitor.get_network_available()
2091
2092
2093class ThreadInterface:
2094    def __init__(self, func, func_args=(), callback=None, callback_args=()):
2095        """
2096        Call a function in a thread
2097        """
2098        def thread_function(func, func_args, callback, callback_args):
2099            output = func(*func_args)
2100            if callback:
2101                GLib.idle_add(callback, output, *callback_args)
2102
2103        Thread(target=thread_function, args=(func, func_args, callback,
2104                callback_args)).start()
2105