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