1# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
2# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
3#                         Travis Shirk <travis AT pobox.com>
4#                         Nikos Kouremenos <kourem AT gmail.com>
5# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
6#                    Stefan Bethge <stefan AT lanpartei.de>
7# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
8# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
9#                         Stephan Erb <steve-e AT h3c.de>
10# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
11# Copyright (C) 2018 Philipp Hörist <philipp @ hoerist.com>
12#
13# This file is part of Gajim.
14#
15# Gajim is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published
17# by the Free Software Foundation; version 3 only.
18#
19# Gajim is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26
27from typing import Any  # pylint: disable=unused-import
28from typing import Dict  # pylint: disable=unused-import
29from typing import List  # pylint: disable=unused-import
30from typing import Optional  # pylint: disable=unused-import
31from typing import cast
32
33import os
34import sys
35import logging
36import uuid
37from collections import namedtuple
38from collections import defaultdict
39
40import nbxmpp
41from gi.repository import Gdk
42
43import gajim
44from gajim.common import config as c_config
45from gajim.common import configpaths
46from gajim.common import ged as ged_module
47from gajim.common.i18n import LANG
48from gajim.common.const import Display
49from gajim.common.events import Events
50from gajim.common.types import NetworkEventsControllerT  # pylint: disable=unused-import
51from gajim.common.types import InterfaceT  # pylint: disable=unused-import
52from gajim.common.types import ConnectionT  # pylint: disable=unused-import
53from gajim.common.types import LegacyContactsAPIT  # pylint: disable=unused-import
54from gajim.common.types import SettingsT  # pylint: disable=unused-import
55
56interface = cast(InterfaceT, None)
57thread_interface = lambda *args: None # Interface to run a thread and then a callback
58config = c_config.Config()
59settings = cast(SettingsT, None)
60version = gajim.__version__
61connections = {}  # type: Dict[str, ConnectionT]
62avatar_cache = {}  # type: Dict[str, Dict[str, Any]]
63bob_cache = {} # type: Dict[str, bytes]
64ipython_window = None
65app = None  # Gtk.Application
66
67ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher
68nec = cast(NetworkEventsControllerT, None)
69plugin_manager = None # Plugins Manager
70
71class Storage:
72    def __init__(self):
73        self.cache = None
74        self.archive = None
75
76storage = Storage()
77
78css_config = None
79
80transport_type = {}  # type: Dict[str, str]
81
82# dict of time of the latest incoming message per jid
83# {acct1: {jid1: time1, jid2: time2}, }
84last_message_time = {}  # type: Dict[str, Dict[str, float]]
85
86contacts = cast(LegacyContactsAPIT, None)
87
88# tell if we are connected to the room or not
89# {acct: {room_jid: True}}
90gc_connected = {}  # type: Dict[str, Dict[str, bool]]
91
92# dict of the pass required to enter a room
93# {room_jid: password}
94gc_passwords = {}  # type: Dict[str, str]
95
96# dict of rooms that must be automatically configured
97# and for which we have a list of invities
98# {account: {room_jid: {'invities': []}}}
99automatic_rooms = {}  # type: Dict[str, Dict[str, Dict[str, List[str]]]]
100
101 # dict of groups, holds if they are expanded or not
102groups = {}  # type: Dict[str, Dict[str, Dict[str, bool]]]
103
104# list of contacts that has just signed in
105newly_added = {}  # type: Dict[str, List[str]]
106
107# list of contacts that has just signed out
108to_be_removed = {}  # type: Dict[str, List[str]]
109
110events = Events()
111
112notification = None
113
114# list of our nick names in each account
115nicks = {}  # type: Dict[str, str]
116
117# should we block 'contact signed in' notifications for this account?
118# this is only for the first 30 seconds after we change our show
119# to something else than offline
120# can also contain account/transport_jid to block notifications for contacts
121# from this transport
122block_signed_in_notifications = {}  # type: Dict[str, bool]
123
124proxy65_manager = None
125
126cert_store = None
127
128task_manager = None
129
130# zeroconf account name
131ZEROCONF_ACC_NAME = 'Local'
132
133# These will be set in app.gui_interface.
134idlequeue = None  # type: nbxmpp.idlequeue.IdleQueue
135socks5queue = None
136
137gupnp_igd = None
138
139gsound_ctx = None
140
141_dependencies = {
142    'AVAHI': False,
143    'PYBONJOUR': False,
144    'FARSTREAM': False,
145    'GST': False,
146    'AV': False,
147    'GEOCLUE': False,
148    'UPNP': False,
149    'GSOUND': False,
150    'GSPELL': False,
151    'IDLE': False,
152}
153
154_tasks = defaultdict(list)  # type: Dict[int, List[Any]]
155
156def print_version():
157    log('gajim').info('Gajim Version: %s', gajim.__version__)
158
159
160def get_client(account):
161    return connections[account]
162
163
164def is_installed(dependency):
165    if dependency == 'ZEROCONF':
166        # Alias for checking zeroconf libs
167        return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
168    return _dependencies[dependency]
169
170
171def is_flatpak():
172    return gajim.IS_FLATPAK
173
174
175def is_portable():
176    return gajim.IS_PORTABLE
177
178
179def is_display(display):
180    # XWayland reports as Display X11, so try with env var
181    is_wayland = os.environ.get('XDG_SESSION_TYPE') == 'wayland'
182    if is_wayland and display == Display.WAYLAND:
183        return True
184
185    default = Gdk.Display.get_default()
186    if default is None:
187        log('gajim').warning('Could not determine window manager')
188        return False
189    return default.__class__.__name__ == display.value
190
191def disable_dependency(dependency):
192    _dependencies[dependency] = False
193
194def detect_dependencies():
195    import gi
196
197    # ZEROCONF
198    try:
199        import pybonjour  # pylint: disable=unused-import
200        _dependencies['PYBONJOUR'] = True
201    except Exception:
202        pass
203
204    try:
205        gi.require_version('Avahi', '0.6')
206        from gi.repository import Avahi  # pylint: disable=unused-import
207        _dependencies['AVAHI'] = True
208    except Exception:
209        pass
210
211    try:
212        gi.require_version('Gst', '1.0')
213        from gi.repository import Gst
214        _dependencies['GST'] = True
215    except Exception:
216        pass
217
218    try:
219        gi.require_version('Farstream', '0.2')
220        from gi.repository import Farstream
221        _dependencies['FARSTREAM'] = True
222    except Exception:
223        pass
224
225    try:
226        if _dependencies['GST'] and _dependencies['FARSTREAM']:
227            Gst.init(None)
228            conference = Gst.ElementFactory.make('fsrtpconference', None)
229            conference.new_session(Farstream.MediaType.AUDIO)
230            from gajim.gui.gstreamer import create_gtk_widget
231            sink, _, _ = create_gtk_widget()
232            if sink is not None:
233                _dependencies['AV'] = True
234    except Exception as error:
235        log('gajim').warning('AV dependency test failed: %s', error)
236
237    # GEOCLUE
238    try:
239        gi.require_version('Geoclue', '2.0')
240        from gi.repository import Geoclue  # pylint: disable=unused-import
241        _dependencies['GEOCLUE'] = True
242    except (ImportError, ValueError):
243        pass
244
245    # UPNP
246    try:
247        gi.require_version('GUPnPIgd', '1.0')
248        from gi.repository import GUPnPIgd
249        global gupnp_igd
250        gupnp_igd = GUPnPIgd.SimpleIgd()
251        _dependencies['UPNP'] = True
252    except ValueError:
253        pass
254
255    # IDLE
256    try:
257        from gajim.common import idle
258        if idle.Monitor.is_available():
259            _dependencies['IDLE'] = True
260    except Exception:
261        pass
262
263    # GSOUND
264    try:
265        gi.require_version('GSound', '1.0')
266        from gi.repository import GLib
267        from gi.repository import GSound
268        global gsound_ctx
269        gsound_ctx = GSound.Context()
270        try:
271            gsound_ctx.init()
272            _dependencies['GSOUND'] = True
273        except GLib.Error as error:
274            log('gajim').warning('GSound init failed: %s', error)
275    except (ImportError, ValueError):
276        pass
277
278    # GSPELL
279    try:
280        gi.require_version('Gspell', '1')
281        from gi.repository import Gspell
282        langs = Gspell.language_get_available()
283        for lang in langs:
284            log('gajim').info('%s (%s) dict available',
285                              lang.get_name(), lang.get_code())
286        if langs:
287            _dependencies['GSPELL'] = True
288    except (ImportError, ValueError):
289        pass
290
291    # Print results
292    for dep, val in _dependencies.items():
293        log('gajim').info('%-13s %s', dep, val)
294
295    log('gajim').info('Used language: %s', LANG)
296
297def detect_desktop_env():
298    if sys.platform in ('win32', 'darwin'):
299        return sys.platform
300
301    desktop = os.environ.get('XDG_CURRENT_DESKTOP')
302    if desktop is None:
303        return None
304
305    if 'gnome' in desktop.lower():
306        return 'gnome'
307    return desktop
308
309desktop_env = detect_desktop_env()
310
311def get_an_id():
312    return str(uuid.uuid4())
313
314def get_nick_from_jid(jid):
315    pos = jid.find('@')
316    return jid[:pos]
317
318def get_server_from_jid(jid):
319    pos = jid.find('@') + 1 # after @
320    return jid[pos:]
321
322def get_name_and_server_from_jid(jid):
323    name = get_nick_from_jid(jid)
324    server = get_server_from_jid(jid)
325    return name, server
326
327def get_room_and_nick_from_fjid(jid):
328    # fake jid is the jid for a contact in a room
329    # gaim@conference.jabber.no/nick/nick-continued
330    # return ('gaim@conference.jabber.no', 'nick/nick-continued')
331    l = jid.split('/', 1)
332    if len(l) == 1: # No nick
333        l.append('')
334    return l
335
336def get_real_jid_from_fjid(account, fjid):
337    """
338    Return real jid or returns None, if we don't know the real jid
339    """
340    room_jid, nick = get_room_and_nick_from_fjid(fjid)
341    if not nick: # It's not a fake_jid, it is a real jid
342        return fjid # we return the real jid
343    real_jid = fjid
344    if interface.msg_win_mgr.get_gc_control(room_jid, account):
345        # It's a pm, so if we have real jid it's in contact.jid
346        gc_contact = contacts.get_gc_contact(account, room_jid, nick)
347        if not gc_contact:
348            return
349        # gc_contact.jid is None when it's not a real jid (we don't know real jid)
350        real_jid = gc_contact.jid
351    return real_jid
352
353def get_room_from_fjid(jid):
354    return get_room_and_nick_from_fjid(jid)[0]
355
356def get_contact_name_from_jid(account, jid):
357    c = contacts.get_first_contact_from_jid(account, jid)
358    return c.name
359
360def get_jid_without_resource(jid):
361    return jid.split('/')[0]
362
363def construct_fjid(room_jid, nick):
364    # fake jid is the jid for a contact in a room
365    # gaim@conference.jabber.org/nick
366    return room_jid + '/' + nick
367
368def get_resource_from_jid(jid):
369    jids = jid.split('/', 1)
370    if len(jids) > 1:
371        return jids[1] # abc@doremi.org/res/res-continued
372    return ''
373
374def get_number_of_accounts():
375    """
376    Return the number of ALL accounts
377    """
378    return len(connections.keys())
379
380def get_number_of_connected_accounts(accounts_list=None):
381    """
382    Returns the number of CONNECTED accounts. Uou can optionally pass an
383    accounts_list and if you do those will be checked, else all will be checked
384    """
385    connected_accounts = 0
386    if accounts_list is None:
387        accounts = connections.keys()
388    else:
389        accounts = accounts_list
390    for account in accounts:
391        if account_is_connected(account):
392            connected_accounts = connected_accounts + 1
393    return connected_accounts
394
395def get_available_clients():
396    clients = []
397    for client in connections.values():
398        if client.state.is_available:
399            clients.append(client)
400    return clients
401
402def get_connected_accounts(exclude_local=False):
403    """
404    Returns a list of CONNECTED accounts
405    """
406    account_list = []
407    for account in connections:
408        if account == 'Local' and exclude_local:
409            continue
410        if account_is_connected(account):
411            account_list.append(account)
412    return account_list
413
414def get_accounts_sorted():
415    '''
416    Get all accounts alphabetically sorted with Local first
417    '''
418    account_list = settings.get_accounts()
419    account_list.sort(key=str.lower)
420    if 'Local' in account_list:
421        account_list.remove('Local')
422        account_list.insert(0, 'Local')
423    return account_list
424
425def get_enabled_accounts_with_labels(exclude_local=True, connected_only=False,
426                                     private_storage_only=False):
427    """
428    Returns a list with [account, account_label] entries.
429    Order by account_label
430    """
431    accounts = []
432    for acc in connections:
433        if exclude_local and account_is_zeroconf(acc):
434            continue
435        if connected_only and not account_is_connected(acc):
436            continue
437        if private_storage_only and not account_supports_private_storage(acc):
438            continue
439
440        accounts.append([acc, get_account_label(acc)])
441
442    accounts.sort(key=lambda xs: str.lower(xs[1]))
443    return accounts
444
445def get_account_label(account):
446    return settings.get_account_setting(account, 'account_label') or account
447
448def account_is_zeroconf(account):
449    return connections[account].is_zeroconf
450
451def account_supports_private_storage(account):
452    # If Delimiter module is not available we can assume
453    # Private Storage is not available
454    return connections[account].get_module('Delimiter').available
455
456def account_is_connected(account):
457    if account not in connections:
458        return False
459    return (connections[account].state.is_connected or
460            connections[account].state.is_available)
461
462def account_is_available(account):
463    if account not in connections:
464        return False
465    return connections[account].state.is_available
466
467def account_is_disconnected(account):
468    return not account_is_connected(account)
469
470def zeroconf_is_connected():
471    return account_is_connected(ZEROCONF_ACC_NAME) and \
472            settings.get_account_setting(ZEROCONF_ACC_NAME, 'is_zeroconf')
473
474def in_groupchat(account, room_jid):
475    room_jid = str(room_jid)
476    if room_jid not in gc_connected[account]:
477        return False
478    return gc_connected[account][room_jid]
479
480def get_transport_name_from_jid(jid, use_config_setting=True):
481    """
482    Returns 'gg', 'irc' etc
483
484    If JID is not from transport returns None.
485    """
486    #FIXME: jid can be None! one TB I saw had this problem:
487    # in the code block # it is a groupchat presence in handle_event_notify
488    # jid was None. Yann why?
489    if not jid or (use_config_setting and not config.get('use_transports_iconsets')):
490        return
491
492    host = get_server_from_jid(jid)
493    if host in transport_type:
494        return transport_type[host]
495
496    # host is now f.e. icq.foo.org or just icq (sometimes on hacky transports)
497    host_splitted = host.split('.')
498    if host_splitted:
499        # now we support both 'icq.' and 'icq' but not icqsucks.org
500        host = host_splitted[0]
501
502    if host in ('irc', 'icq', 'sms', 'weather', 'mrim', 'facebook'):
503        return host
504    if host == 'gg':
505        return 'gadu-gadu'
506    if host == 'jit':
507        return 'icq'
508    if host == 'facebook':
509        return 'facebook'
510    return None
511
512def jid_is_transport(jid):
513    # if not '@' or '@' starts the jid then it is transport
514    if jid.find('@') <= 0:
515        return True
516    return False
517
518def get_jid_from_account(account_name):
519    """
520    Return the jid we use in the given account
521    """
522    name = settings.get_account_setting(account_name, 'name')
523    hostname = settings.get_account_setting(account_name, 'hostname')
524    jid = name + '@' + hostname
525    return jid
526
527def get_account_from_jid(jid):
528    for account in settings.get_accounts():
529        if jid == get_jid_from_account(account):
530            return account
531
532def get_our_jids():
533    """
534    Returns a list of the jids we use in our accounts
535    """
536    our_jids = list()
537    for account in contacts.get_accounts():
538        our_jids.append(get_jid_from_account(account))
539    return our_jids
540
541def get_hostname_from_account(account_name, use_srv=False):
542    """
543    Returns hostname (if custom hostname is used, that is returned)
544    """
545    if use_srv and connections[account_name].connected_hostname:
546        return connections[account_name].connected_hostname
547    if settings.get_account_setting(account_name, 'use_custom_host'):
548        return settings.get_account_setting(account_name, 'custom_host')
549    return settings.get_account_setting(account_name, 'hostname')
550
551def get_notification_image_prefix(jid):
552    """
553    Returns the prefix for the notification images
554    """
555    transport_name = get_transport_name_from_jid(jid)
556    if transport_name in ('icq', 'facebook'):
557        prefix = transport_name
558    else:
559        prefix = 'jabber'
560    return prefix
561
562def get_name_from_jid(account, jid):
563    """
564    Return from JID's shown name and if no contact returns jids
565    """
566    contact = contacts.get_first_contact_from_jid(account, jid)
567    if contact:
568        actor = contact.get_shown_name()
569    else:
570        actor = jid
571    return actor
572
573
574def get_recent_groupchats(account):
575    recent_groupchats = settings.get_account_setting(
576        account, 'recent_groupchats').split()
577
578    RecentGroupchat = namedtuple('RecentGroupchat',
579                                 ['room', 'server', 'nickname'])
580
581    recent_list = []
582    for groupchat in recent_groupchats:
583        jid = nbxmpp.JID.from_string(groupchat)
584        recent = RecentGroupchat(jid.localpart, jid.domain, jid.resource)
585        recent_list.append(recent)
586    return recent_list
587
588def add_recent_groupchat(account, room_jid, nickname):
589    recent = settings.get_account_setting(
590        account, 'recent_groupchats').split()
591    full_jid = room_jid + '/' + nickname
592    if full_jid in recent:
593        recent.remove(full_jid)
594    recent.insert(0, full_jid)
595    if len(recent) > 10:
596        recent = recent[0:9]
597    config_value = ' '.join(recent)
598    settings.set_account_setting(account, 'recent_groupchats', config_value)
599
600def get_priority(account, show):
601    """
602    Return the priority an account must have
603    """
604    if not show:
605        show = 'online'
606
607    if show in ('online', 'chat', 'away', 'xa', 'dnd') and \
608    settings.get_account_setting(account, 'adjust_priority_with_status'):
609        prio = settings.get_account_setting(account, 'autopriority_' + show)
610    else:
611        prio = settings.get_account_setting(account, 'priority')
612    if prio < -128:
613        prio = -128
614    elif prio > 127:
615        prio = 127
616    return prio
617
618def log(domain):
619    if domain != 'gajim':
620        domain = 'gajim.%s' % domain
621    return logging.getLogger(domain)
622
623def prefers_app_menu():
624    if sys.platform == 'darwin':
625        return True
626    if sys.platform == 'win32':
627        return False
628    return app.prefers_app_menu()
629
630def load_css_config():
631    global css_config
632    from gajim.gui.css_config import CSSConfig
633    css_config = CSSConfig()
634
635def set_debug_mode(enable: bool) -> None:
636    debug_folder = configpaths.get('DEBUG')
637    debug_enabled = debug_folder / 'debug-enabled'
638    if enable:
639        debug_enabled.touch()
640    else:
641        if debug_enabled.exists():
642            debug_enabled.unlink()
643
644def get_debug_mode() -> bool:
645    debug_folder = configpaths.get('DEBUG')
646    debug_enabled = debug_folder / 'debug-enabled'
647    return debug_enabled.exists()
648
649def get_stored_bob_data(algo_hash: str) -> Optional[bytes]:
650    try:
651        return bob_cache[algo_hash]
652    except KeyError:
653        filepath = configpaths.get('BOB') / algo_hash
654        if filepath.exists():
655            with open(str(filepath), 'r+b') as file:
656                data = file.read()
657            return data
658    return None
659
660def get_groupchat_control(account, jid):
661    control = app.interface.msg_win_mgr.get_gc_control(jid, account)
662    if control is not None:
663        return control
664    try:
665        return app.interface.minimized_controls[account][jid]
666    except Exception:
667        return None
668
669
670def register_task(self, task):
671    _tasks[id(self)].append(task)
672
673
674def remove_task(task, id_):
675    try:
676        _tasks[id_].remove(task)
677    except Exception:
678        pass
679    else:
680        if not _tasks[id_]:
681            del _tasks[id_]
682
683
684def cancel_tasks(obj):
685    id_ = id(obj)
686    if id_ not in _tasks:
687        return
688
689    task_list = _tasks[id_]
690    for task in task_list:
691        task.cancel()
692