# Copyright (C) 2003-2014 Yann Leboulanger # Copyright (C) 2005-2006 Dimitur Kirov # Travis Shirk # Nikos Kouremenos # Copyright (C) 2006 Junglecow J # Stefan Bethge # Copyright (C) 2006-2008 Jean-Marie Traissard # Copyright (C) 2007-2008 Brendan Taylor # Stephan Erb # Copyright (C) 2008 Jonathan Schleifer # Copyright (C) 2018 Philipp Hörist # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . from typing import Any # pylint: disable=unused-import from typing import Dict # pylint: disable=unused-import from typing import List # pylint: disable=unused-import from typing import Optional # pylint: disable=unused-import from typing import cast import os import sys import logging import uuid from pathlib import Path from distutils.version import LooseVersion as V from collections import namedtuple import nbxmpp from gi.repository import Gdk import gajim from gajim.common import config as c_config from gajim.common import configpaths from gajim.common import ged as ged_module from gajim.common.i18n import LANG from gajim.common.const import Display from gajim.common.contacts import LegacyContactsAPI from gajim.common.events import Events from gajim.common.types import NetworkEventsControllerT # pylint: disable=unused-import from gajim.common.types import InterfaceT # pylint: disable=unused-import from gajim.common.types import LoggerT # pylint: disable=unused-import from gajim.common.types import ConnectionT # pylint: disable=unused-import interface = cast(InterfaceT, None) thread_interface = lambda *args: None # Interface to run a thread and then a callback config = c_config.Config() version = gajim.__version__ connections = {} # type: Dict[str, ConnectionT] avatar_cache = {} # type: Dict[str, Dict[str, Any]] bob_cache = {} # type: Dict[str, bytes] ipython_window = None app = None # Gtk.Application ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher nec = cast(NetworkEventsControllerT, None) plugin_manager = None # Plugins Manager logger = cast(LoggerT, None) css_config = None os_info = None # used to cache os information transport_type = {} # type: Dict[str, str] # dict of time of the latest incoming message per jid # {acct1: {jid1: time1, jid2: time2}, } last_message_time = {} # type: Dict[str, Dict[str, float]] contacts = LegacyContactsAPI() # tell if we are connected to the room or not # {acct: {room_jid: True}} gc_connected = {} # type: Dict[str, Dict[str, bool]] # dict of the pass required to enter a room # {room_jid: password} gc_passwords = {} # type: Dict[str, str] # dict of rooms that must be automaticaly configured # and for which we have a list of invities # {account: {room_jid: {'invities': []}}} automatic_rooms = {} # type: Dict[str, Dict[str, Dict[str, List[str]]]] # dict of groups, holds if they are expanded or not groups = {} # type: Dict[str, Dict[str, Dict[str, bool]]] # list of contacts that has just signed in newly_added = {} # type: Dict[str, List[str]] # list of contacts that has just signed out to_be_removed = {} # type: Dict[str, List[str]] events = Events() notification = None # list of our nick names in each account nicks = {} # type: Dict[str, str] # should we block 'contact signed in' notifications for this account? # this is only for the first 30 seconds after we change our show # to something else than offline # can also contain account/transport_jid to block notifications for contacts # from this transport block_signed_in_notifications = {} # type: Dict[str, bool] # type of each connection (ssl, tls, tcp, ...) con_types = {} # type: Dict[str, Optional[str]] # whether we pass auto away / xa or not #'off': don't use sleeper for this account #'online': online and use sleeper #'autoaway': autoaway and use sleeper #'autoxa': autoxa and use sleeper sleeper_state = {} # type: Dict[str, str] status_before_autoaway = {} # type: Dict[str, str] # Is Gnome configured to activate on single click ? single_click = False SHOW_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] # zeroconf account name ZEROCONF_ACC_NAME = 'Local' # These will be set in app.gui_interface. idlequeue = None # type: nbxmpp.idlequeue.IdleQueue socks5queue = None gupnp_igd = None gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_common_features = [ nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE, nbxmpp.NS_MUC, nbxmpp.NS_COMMANDS, nbxmpp.NS_DISCO_INFO, nbxmpp.NS_LAST, nbxmpp.NS_DATA, nbxmpp.NS_ENCRYPTED, nbxmpp.NS_PING, nbxmpp.NS_TIME_REVISED, nbxmpp.NS_VERSION, nbxmpp.NS_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_CONFERENCE, nbxmpp.NS_CORRECT, nbxmpp.NS_EME, nbxmpp.NS_HASHES_2, nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256, nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_HASHES_SHA3_256, nbxmpp.NS_HASHES_SHA3_512, nbxmpp.NS_HASHES_BLAKE2B_256, nbxmpp.NS_HASHES_BLAKE2B_512, nbxmpp.NS_JINGLE, nbxmpp.NS_JINGLE_FILE_TRANSFER_5, nbxmpp.NS_JINGLE_XTLS, nbxmpp.NS_JINGLE_BYTESTREAM, nbxmpp.NS_JINGLE_IBB, nbxmpp.NS_AVATAR_METADATA + '+notify', nbxmpp.NS_BOOKMARKS + '+notify', ] # Optional features gajim supports per account gajim_optional_features = {} # type: Dict[str, List[str]] # Capabilities hash per account caps_hash = {} # type: Dict[str, List[str]] _dependencies = { 'PYTHON-DBUS': False, 'PYBONJOUR': False, 'PYGPG': False, 'GPG_BINARY': False, 'FARSTREAM': False, 'GEOCLUE': False, 'UPNP': False, 'PYCURL': False, 'GSPELL': False, 'IDLE': False, } def is_installed(dependency): if dependency == 'GPG': # Alias for checking python-gnupg and the GPG binary return _dependencies['PYGPG'] and _dependencies['GPG_BINARY'] if dependency == 'ZEROCONF': # Alias for checking zeroconf libs return _dependencies['PYTHON-DBUS'] or _dependencies['PYBONJOUR'] return _dependencies[dependency] def is_flatpak(): return gajim.IS_FLATPAK def is_display(display): # XWayland reports as Display X11, so try with env var is_wayland = os.environ.get('XDG_SESSION_TYPE') == 'wayland' if is_wayland and display == Display.WAYLAND: return True default = Gdk.Display.get_default() if default is None: log('gajim').warning('Could not determine window manager') return False return default.__class__.__name__ == display.value def disable_dependency(dependency): _dependencies[dependency] = False def detect_dependencies(): import gi # ZEROCONF try: import pybonjour # pylint: disable=unused-variable _dependencies['PYBONJOUR'] = True except Exception: pass try: import dbus # pylint: disable=unused-variable _dependencies['PYTHON-DBUS'] = True except Exception: pass # python-gnupg try: import gnupg # We need https://pypi.python.org/pypi/python-gnupg # but https://pypi.python.org/pypi/gnupg shares the same package name. # It cannot be used as a drop-in replacement. # We test with a version check if python-gnupg is installed as it is # on a much lower version number than gnupg # Also we need at least python-gnupg 0.3.8 v_gnupg = gnupg.__version__ if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'): log('gajim').info('Gajim needs python-gnupg >= 0.3.8') raise ImportError _dependencies['PYGPG'] = True except ImportError: pass # GPG BINARY import subprocess def test_gpg(binary='gpg'): if os.name == 'nt': gpg_cmd = binary + ' -h >nul 2>&1' else: gpg_cmd = binary + ' -h >/dev/null 2>&1' if subprocess.call(gpg_cmd, shell=True): return False return True if test_gpg(binary='gpg2'): _dependencies['GPG_BINARY'] = 'gpg2' elif test_gpg(binary='gpg'): _dependencies['GPG_BINARY'] = 'gpg' # FARSTREAM try: if os.name == 'nt': root_path = Path(sys.executable).parents[1] fs_plugin_path = str(root_path / 'lib' / 'farstream-0.2') gst_plugin_path = str(root_path / 'lib' / 'gstreamer-1.0') os.environ['FS_PLUGIN_PATH'] = fs_plugin_path os.environ['GST_PLUGIN_PATH'] = gst_plugin_path gi.require_version('Farstream', '0.2') from gi.repository import Farstream gi.require_version('Gst', '1.0') from gi.repository import Gst try: Gst.init(None) conference = Gst.ElementFactory.make('fsrtpconference', None) conference.new_session(Farstream.MediaType.AUDIO) except Exception as error: log('gajim').info(error) _dependencies['FARSTREAM'] = True except (ImportError, ValueError): pass # GEOCLUE try: gi.require_version('Geoclue', '2.0') from gi.repository import Geoclue # pylint: disable=unused-variable _dependencies['GEOCLUE'] = True except (ImportError, ValueError): pass # UPNP try: gi.require_version('GUPnPIgd', '1.0') from gi.repository import GUPnPIgd global gupnp_igd gupnp_igd = GUPnPIgd.SimpleIgd() _dependencies['UPNP'] = True except ValueError: pass # PYCURL try: import pycurl # pylint: disable=unused-variable _dependencies['PYCURL'] = True except ImportError: pass # IDLE try: from gajim.common import idle if idle.Monitor.is_available(): _dependencies['IDLE'] = True except Exception: pass # GSPELL try: gi.require_version('Gspell', '1') from gi.repository import Gspell langs = Gspell.language_get_available() for lang in langs: log('gajim').info('%s (%s) dict available', lang.get_name(), lang.get_code()) if langs: _dependencies['GSPELL'] = True except (ImportError, ValueError): pass # Print results for dep, val in _dependencies.items(): log('gajim').info('%-13s %s', dep, val) log('gajim').info('Used language: %s', LANG) def get_gpg_binary(): return _dependencies['GPG_BINARY'] def get_an_id(): return str(uuid.uuid4()) def get_nick_from_jid(jid): pos = jid.find('@') return jid[:pos] def get_server_from_jid(jid): pos = jid.find('@') + 1 # after @ return jid[pos:] def get_name_and_server_from_jid(jid): name = get_nick_from_jid(jid) server = get_server_from_jid(jid) return name, server def get_room_and_nick_from_fjid(jid): # fake jid is the jid for a contact in a room # gaim@conference.jabber.no/nick/nick-continued # return ('gaim@conference.jabber.no', 'nick/nick-continued') l = jid.split('/', 1) if len(l) == 1: # No nick l.append('') return l def get_real_jid_from_fjid(account, fjid): """ Return real jid or returns None, if we don't know the real jid """ room_jid, nick = get_room_and_nick_from_fjid(fjid) if not nick: # It's not a fake_jid, it is a real jid return fjid # we return the real jid real_jid = fjid if interface.msg_win_mgr.get_gc_control(room_jid, account): # It's a pm, so if we have real jid it's in contact.jid gc_contact = contacts.get_gc_contact(account, room_jid, nick) if not gc_contact: return # gc_contact.jid is None when it's not a real jid (we don't know real jid) real_jid = gc_contact.jid return real_jid def get_room_from_fjid(jid): return get_room_and_nick_from_fjid(jid)[0] def get_contact_name_from_jid(account, jid): c = contacts.get_first_contact_from_jid(account, jid) return c.name def get_jid_without_resource(jid): return jid.split('/')[0] def construct_fjid(room_jid, nick): # fake jid is the jid for a contact in a room # gaim@conference.jabber.org/nick return room_jid + '/' + nick def get_resource_from_jid(jid): jids = jid.split('/', 1) if len(jids) > 1: return jids[1] # abc@doremi.org/res/res-continued return '' def get_number_of_accounts(): """ Return the number of ALL accounts """ return len(connections.keys()) def get_number_of_connected_accounts(accounts_list=None): """ Returns the number of CONNECTED accounts. Uou can optionally pass an accounts_list and if you do those will be checked, else all will be checked """ connected_accounts = 0 if accounts_list is None: accounts = connections.keys() else: accounts = accounts_list for account in accounts: if account_is_connected(account): connected_accounts = connected_accounts + 1 return connected_accounts def get_connected_accounts(): """ Returns a list of CONNECTED accounts """ account_list = [] for account in connections: if account_is_connected(account): account_list.append(account) return account_list def get_enabled_accounts_with_labels(exclude_local=True, connected_only=False, private_storage_only=False): """ Returns a list with [account, account_label] entries. Order by account_label """ accounts = [] for acc in connections: if exclude_local and account_is_zeroconf(acc): continue if connected_only and not account_is_connected(acc): continue if private_storage_only and not account_supports_private_storage(acc): continue accounts.append([acc, get_account_label(acc)]) accounts.sort(key=lambda xs: str.lower(xs[1])) return accounts def get_account_label(account): return config.get_per('accounts', account, 'account_label') or account def account_is_zeroconf(account): return connections[account].is_zeroconf def account_supports_private_storage(account): # If Delimiter module is not available we can assume # Private Storage is not available return connections[account].get_module('Delimiter').available def account_is_connected(account): if account not in connections: return False # 0 is offline, 1 is connecting return connections[account].connected > 1 def is_invisible(account): return SHOW_LIST[connections[account].connected] == 'invisible' def account_is_disconnected(account): return not account_is_connected(account) def zeroconf_is_connected(): return account_is_connected(ZEROCONF_ACC_NAME) and \ config.get_per('accounts', ZEROCONF_ACC_NAME, 'is_zeroconf') def in_groupchat(account, room_jid): room_jid = str(room_jid) if room_jid not in gc_connected[account]: return False return gc_connected[account][room_jid] def get_number_of_securely_connected_accounts(): """ Return the number of the accounts that are SSL/TLS connected """ num_of_secured = 0 for account in connections: if account_is_securely_connected(account): num_of_secured += 1 return num_of_secured def account_is_securely_connected(account): if not account_is_connected(account): return False return con_types.get(account) in ('tls', 'ssl') def get_transport_name_from_jid(jid, use_config_setting=True): """ Returns 'gg', 'irc' etc If JID is not from transport returns None. """ #FIXME: jid can be None! one TB I saw had this problem: # in the code block # it is a groupchat presence in handle_event_notify # jid was None. Yann why? if not jid or (use_config_setting and not config.get('use_transports_iconsets')): return host = get_server_from_jid(jid) if host in transport_type: return transport_type[host] # host is now f.e. icq.foo.org or just icq (sometimes on hacky transports) host_splitted = host.split('.') if host_splitted: # now we support both 'icq.' and 'icq' but not icqsucks.org host = host_splitted[0] if host in ('irc', 'icq', 'sms', 'weather', 'mrim', 'facebook'): return host if host == 'gg': return 'gadu-gadu' if host == 'jit': return 'icq' if host == 'facebook': return 'facebook' return None def jid_is_transport(jid): # if not '@' or '@' starts the jid then it is transport if jid.find('@') <= 0: return True return False def get_jid_from_account(account_name): """ Return the jid we use in the given account """ name = config.get_per('accounts', account_name, 'name') hostname = config.get_per('accounts', account_name, 'hostname') jid = name + '@' + hostname return jid def get_account_from_jid(jid): for account in config.get_per('accounts'): if jid == get_jid_from_account(account): return account def get_our_jids(): """ Returns a list of the jids we use in our accounts """ our_jids = list() for account in contacts.get_accounts(): our_jids.append(get_jid_from_account(account)) return our_jids def get_hostname_from_account(account_name, use_srv=False): """ Returns hostname (if custom hostname is used, that is returned) """ if use_srv and connections[account_name].connected_hostname: return connections[account_name].connected_hostname if config.get_per('accounts', account_name, 'use_custom_host'): return config.get_per('accounts', account_name, 'custom_host') return config.get_per('accounts', account_name, 'hostname') def get_notification_image_prefix(jid): """ Returns the prefix for the notification images """ transport_name = get_transport_name_from_jid(jid) if transport_name in ('icq', 'facebook'): prefix = transport_name else: prefix = 'jabber' return prefix def get_name_from_jid(account, jid): """ Return from JID's shown name and if no contact returns jids """ contact = contacts.get_first_contact_from_jid(account, jid) if contact: actor = contact.get_shown_name() else: actor = jid return actor def get_muc_domain(account): return connections[account].muc_jid.get('jabber', None) def get_recent_groupchats(account): recent_groupchats = config.get_per( 'accounts', account, 'recent_groupchats').split() RecentGroupchat = namedtuple('RecentGroupchat', ['room', 'server', 'nickname']) recent_list = [] for groupchat in recent_groupchats: jid = nbxmpp.JID(groupchat) recent = RecentGroupchat( jid.getNode(), jid.getDomain(), jid.getResource()) recent_list.append(recent) return recent_list def add_recent_groupchat(account, room_jid, nickname): recent = config.get_per( 'accounts', account, 'recent_groupchats').split() full_jid = room_jid + '/' + nickname if full_jid in recent: recent.remove(full_jid) recent.insert(0, full_jid) if len(recent) > 10: recent = recent[0:9] config_value = ' '.join(recent) config.set_per( 'accounts', account, 'recent_groupchats', config_value) def get_priority(account, show): """ Return the priority an account must have """ if not show: show = 'online' if show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible') and \ config.get_per('accounts', account, 'adjust_priority_with_status'): prio = config.get_per('accounts', account, 'autopriority_' + show) else: prio = config.get_per('accounts', account, 'priority') if prio < -128: prio = -128 elif prio > 127: prio = 127 return prio def log(domain): if domain != 'gajim': domain = 'gajim.%s' % domain return logging.getLogger(domain) def prefers_app_menu(): if sys.platform == 'darwin': return True if sys.platform == 'win32': return False return app.prefers_app_menu() def get_app_window(cls, account=None, jid=None): for win in app.get_windows(): if isinstance(cls, str): if type(win).__name__ != cls: continue elif not isinstance(win, cls): continue if account is not None: if account != win.account: continue if jid is not None: if jid != win.jid: continue return win return None def load_css_config(): global css_config from gajim.gtk.css_config import CSSConfig css_config = CSSConfig() def set_win_debug_mode(enable: bool) -> None: debug_folder = Path(configpaths.get('DEBUG')) debug_enabled = debug_folder / 'debug-enabled' if enable: debug_enabled.touch() else: if debug_enabled.exists(): debug_enabled.unlink() def get_win_debug_mode() -> bool: if sys.platform != 'win32': return False debug_folder = Path(configpaths.get('DEBUG')) debug_enabled = debug_folder / 'debug-enabled' return debug_enabled.exists() def get_stored_bob_data(algo_hash: str) -> Optional[bytes]: try: return bob_cache[algo_hash] except KeyError: filepath = Path(configpaths.get('BOB')) / algo_hash if filepath.exists(): with open(str(filepath), 'r+b') as file: data = file.read() return data return None