1# This file is part of Gajim. 2# 3# Gajim is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published 5# by the Free Software Foundation; version 3 only. 6# 7# Gajim is distributed in the hope that it will be useful, 8# but WITHOUT ANY WARRANTY; without even the implied warranty of 9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10# GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License 13# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 14 15from typing import Any 16from typing import Dict 17from typing import List 18from typing import Union 19 20import sys 21import json 22import logging 23import sqlite3 24import inspect 25import weakref 26from pathlib import Path 27from collections import namedtuple 28from collections import defaultdict 29 30from gi.repository import GLib 31 32from gajim import IS_PORTABLE 33from gajim.common import app 34from gajim.common import configpaths 35from gajim.common import optparser 36from gajim.common.helpers import get_muc_context 37from gajim.common.setting_values import APP_SETTINGS 38from gajim.common.setting_values import ACCOUNT_SETTINGS 39from gajim.common.setting_values import PROXY_SETTINGS 40from gajim.common.setting_values import PROXY_EXAMPLES 41from gajim.common.setting_values import PLUGIN_SETTINGS 42from gajim.common.setting_values import DEFAULT_SOUNDEVENT_SETTINGS 43from gajim.common.setting_values import STATUS_PRESET_SETTINGS 44from gajim.common.setting_values import STATUS_PRESET_EXAMPLES 45from gajim.common.setting_values import HAS_APP_DEFAULT 46from gajim.common.setting_values import HAS_ACCOUNT_DEFAULT 47 48SETTING_TYPE = Union[bool, int, str, object] 49 50log = logging.getLogger('gajim.c.settings') 51 52CREATE_SQL = ''' 53 CREATE TABLE settings ( 54 name TEXT UNIQUE, 55 settings TEXT 56 ); 57 58 CREATE TABLE account_settings ( 59 account TEXT UNIQUE, 60 settings TEXT 61 ); 62 63 INSERT INTO settings(name, settings) VALUES ('app', '{}'); 64 INSERT INTO settings(name, settings) VALUES ('soundevents', '{}'); 65 INSERT INTO settings(name, settings) VALUES ('status_presets', '%s'); 66 INSERT INTO settings(name, settings) VALUES ('proxies', '%s'); 67 INSERT INTO settings(name, settings) VALUES ('plugins', '{}'); 68 69 PRAGMA user_version=0; 70 ''' % (json.dumps(STATUS_PRESET_EXAMPLES), 71 json.dumps(PROXY_EXAMPLES)) 72 73 74class Settings: 75 def __init__(self): 76 self._con = None 77 self._commit_scheduled = None 78 79 self._settings = {} 80 self._account_settings = {} 81 82 self._callbacks = defaultdict(list) 83 84 def connect_signal(self, setting, func, account=None, jid=None): 85 if not inspect.ismethod(func): 86 # static methods are not bound to an object so we can’t easily 87 # remove the func once it should not be called anymore 88 raise ValueError('Only bound methods can be connected') 89 90 91 func = weakref.WeakMethod(func) 92 self._callbacks[(setting, account, jid)].append(func) 93 94 def disconnect_signals(self, object_): 95 for _, handlers in self._callbacks.items(): 96 for handler in list(handlers): 97 if isinstance(handler, tuple): 98 continue 99 func = handler() 100 if func is None or func.__self__ is object_: 101 handlers.remove(handler) 102 103 def bind_signal(self, 104 setting, 105 widget, 106 func_name, 107 account=None, 108 jid=None, 109 inverted=False, 110 default_text=None): 111 112 callbacks = self._callbacks[(setting, account, jid)] 113 func = getattr(widget, func_name) 114 callbacks.append((func, inverted, default_text)) 115 116 def _on_destroy(*args): 117 callbacks.remove((func, inverted, default_text)) 118 119 widget.connect('destroy', _on_destroy) 120 121 def _notify(self, value, setting, account=None, jid=None): 122 log.info('Signal: %s changed', setting) 123 124 callbacks = self._callbacks[(setting, account, jid)] 125 for func in list(callbacks): 126 if isinstance(func, tuple): 127 func, inverted, default_text = func 128 if isinstance(value, bool) and inverted: 129 value = not value 130 131 if value == '' and default_text is not None: 132 value = default_text 133 134 try: 135 func(value) 136 except Exception: 137 log.exception('Error while executing signal callback') 138 continue 139 140 if func() is None: 141 callbacks.remove(func) 142 continue 143 144 func = func() 145 if func is None: 146 continue 147 148 try: 149 func(value, setting, account, jid) 150 except Exception: 151 log.exception('Error while executing signal callback') 152 153 def init(self) -> None: 154 self._setup_installation_defaults() 155 self._connect_database() 156 self._load_settings() 157 self._load_account_settings() 158 if not self._settings['app']: 159 self._migrate_old_config() 160 self._commit() 161 self._migrate_database() 162 163 @staticmethod 164 def _setup_installation_defaults() -> None: 165 if IS_PORTABLE: 166 APP_SETTINGS['use_keyring'] = False 167 168 @staticmethod 169 def _namedtuple_factory(cursor: Any, row: Any) -> Any: 170 fields = [col[0] for col in cursor.description] 171 return namedtuple("Row", fields)(*row) 172 173 def _connect_database(self) -> None: 174 path = configpaths.get('SETTINGS') 175 if path.is_dir(): 176 log.error('%s is a directory but should be a file', path) 177 sys.exit() 178 179 if not path.exists(): 180 self._create_database(CREATE_SQL, path) 181 182 self._con = sqlite3.connect(path) 183 self._con.row_factory = self._namedtuple_factory 184 185 @staticmethod 186 def _create_database(statement: str, path: Path) -> None: 187 log.info('Creating %s', path) 188 con = sqlite3.connect(path) 189 190 try: 191 con.executescript(statement) 192 except Exception: 193 log.exception('Error') 194 con.close() 195 path.unlink() 196 sys.exit() 197 198 con.commit() 199 con.close() 200 path.chmod(0o600) 201 202 def _get_user_version(self) -> int: 203 return self._con.execute('PRAGMA user_version').fetchone()[0] 204 205 def _set_user_version(self, version: int) -> None: 206 self._con.execute(f'PRAGMA user_version = {version}') 207 self._commit() 208 209 def _commit(self, schedule: bool = False) -> None: 210 if not schedule: 211 if self._commit_scheduled is not None: 212 GLib.source_remove(self._commit_scheduled) 213 self._commit_scheduled = None 214 log.info('Commit') 215 self._con.commit() 216 217 elif self._commit_scheduled is None: 218 self._commit_scheduled = GLib.timeout_add( 219 200, self._scheduled_commit) 220 221 def save(self) -> None: 222 self._commit() 223 224 def _scheduled_commit(self) -> None: 225 self._commit_scheduled = None 226 log.info('Commit') 227 self._con.commit() 228 229 def _migrate_database(self) -> None: 230 try: 231 self._migrate() 232 except Exception: 233 self._con.close() 234 log.exception('Error') 235 sys.exit() 236 237 def _migrate(self) -> None: 238 pass 239 240 def _migrate_old_config(self) -> None: 241 config_file = configpaths.get('CONFIG_FILE') 242 if not config_file.exists(): 243 return 244 245 # Read legacy config 246 optparser.OptionsParser(str(configpaths.get('CONFIG_FILE'))).read() 247 248 account_settings = app.config.get_all_per('accounts') 249 self._cleanup_account_default_values('account', account_settings) 250 251 contact_settings = app.config.get_all_per('contacts') 252 self._cleanup_account_default_values('contact', contact_settings) 253 254 group_chat_settings = app.config.get_all_per('rooms') 255 self._cleanup_account_default_values('group_chat', 256 group_chat_settings) 257 258 for account, settings in account_settings.items(): 259 self.add_account(account) 260 self._account_settings[account]['account'] = settings 261 self._account_settings[account]['contact'] = contact_settings 262 self._account_settings[account]['group_chat'] = group_chat_settings 263 self._commit_account_settings(account) 264 265 self._migrate_encryption_settings() 266 267 # Migrate plugin settings 268 self._settings['plugins'] = app.config.get_all_per('plugins') 269 self._commit_settings('plugins') 270 271 self._migrate_app_settings() 272 self._migrate_soundevent_settings() 273 self._migrate_status_preset_settings() 274 self._migrate_proxy_settings() 275 276 new_path = config_file.with_name(f'{config_file.name}.old') 277 config_file.rename(new_path) 278 log.info('Successfully migrated config') 279 280 def _migrate_app_settings(self) -> None: 281 app_settings = app.config.get_all() 282 283 # Migrate deprecated settings 284 value = app_settings.pop('send_chatstate_muc_default', None) 285 if value is not None: 286 for account in self._account_settings: 287 self._account_settings[account]['account']['gc_send_chatstate_default'] = value 288 289 value = app_settings.pop('send_chatstate_default', None) 290 if value is not None: 291 for account in self._account_settings: 292 self._account_settings[account]['account']['send_chatstate_default'] = value 293 294 value = app_settings.pop('print_join_left_default', None) 295 if value is not None: 296 app_settings['gc_print_join_left_default'] = value 297 298 value = app_settings.pop('print_status_muc_default', None) 299 if value is not None: 300 app_settings['gc_print_status_default'] = value 301 302 # Cleanup values which are equal to current defaults 303 for setting, value in list(app_settings.items()): 304 if (setting not in APP_SETTINGS or 305 value == APP_SETTINGS[setting]): 306 del app_settings[setting] 307 308 self._settings['app'] = app_settings 309 self._commit_settings('app') 310 311 for account in self._account_settings: 312 self._commit_account_settings(account) 313 314 def _migrate_encryption_settings(self) -> None: 315 # Migrate encryption settings into contact/group chat settings 316 encryption_settings = app.config.get_all_per('encryption') 317 for key, settings in encryption_settings.items(): 318 account, jid = self._split_encryption_config_key(key) 319 if account is None: 320 continue 321 322 encryption = settings.get('encryption') 323 if not encryption: 324 continue 325 326 if '@' not in jid: 327 continue 328 329 # Sad try to determine if the jid is a group chat 330 # At this point there is no better way 331 domain = jid.split('@')[1] 332 subdomain = domain.split('.')[0] 333 if subdomain in ('muc', 'conference', 'conf', 334 'rooms', 'room', 'chat'): 335 category = 'group_chat' 336 else: 337 category = 'contact' 338 339 if not jid in self._account_settings[account][category]: 340 self._account_settings[account][category][jid] = { 341 'encryption': encryption} 342 else: 343 self._account_settings[account][category][ 344 jid]['encryption'] = encryption 345 self._commit_account_settings(account) 346 347 def _split_encryption_config_key(self, key: str) -> Any: 348 for account in self._account_settings: 349 if not key.startswith(account): 350 continue 351 jid = key.replace(f'{account}-', '', 1) 352 return account, jid 353 return None, None 354 355 def _migrate_soundevent_settings(self) -> None: 356 soundevent_settings = app.config.get_all_per('soundevents') 357 for soundevent, settings in list(soundevent_settings.items()): 358 if soundevent not in DEFAULT_SOUNDEVENT_SETTINGS: 359 del soundevent_settings[soundevent] 360 continue 361 362 for setting, value in list(settings.items()): 363 if DEFAULT_SOUNDEVENT_SETTINGS[soundevent][setting] == value: 364 del soundevent_settings[soundevent][setting] 365 if not soundevent_settings[soundevent]: 366 del soundevent_settings[soundevent] 367 368 self._settings['soundevents'] = soundevent_settings 369 self._commit_settings('soundevents') 370 371 def _migrate_status_preset_settings(self) -> None: 372 status_preset_settings = app.config.get_all_per('statusmsg') 373 for preset, settings in list(status_preset_settings.items()): 374 if '_last_' in preset: 375 del status_preset_settings[preset] 376 continue 377 378 for setting, value in list(settings.items()): 379 if setting not in STATUS_PRESET_SETTINGS: 380 continue 381 if STATUS_PRESET_SETTINGS[setting] == value: 382 del status_preset_settings[preset][setting] 383 if not status_preset_settings[preset]: 384 del status_preset_settings[preset] 385 386 self._settings['status_presets'] = status_preset_settings 387 self._commit_settings('status_presets') 388 389 def _migrate_proxy_settings(self) -> None: 390 proxy_settings = app.config.get_all_per('proxies') 391 for proxy_name, settings in proxy_settings.items(): 392 for setting, value in list(settings.items()): 393 if (setting not in PROXY_SETTINGS or 394 PROXY_SETTINGS[setting] == value): 395 del proxy_settings[proxy_name][setting] 396 397 self._settings['proxies'] = proxy_settings 398 self._commit_settings('proxies') 399 400 @staticmethod 401 def _cleanup_account_default_values(category: str, settings: Any) -> None: 402 for contact, settings_ in list(settings.items()): 403 for setting, value in list(settings_.items()): 404 if setting not in ACCOUNT_SETTINGS[category]: 405 del settings[contact][setting] 406 if not settings[contact]: 407 del settings[contact] 408 continue 409 410 default = ACCOUNT_SETTINGS[category][setting] 411 if default == value: 412 del settings[contact][setting] 413 if not settings[contact]: 414 del settings[contact] 415 continue 416 417 def close(self) -> None: 418 log.info('Close settings') 419 self._con.commit() 420 self._con.close() 421 self._con = None 422 423 def _load_settings(self) -> None: 424 settings = self._con.execute('SELECT * FROM settings').fetchall() 425 for row in settings: 426 log.info('Load %s settings', row.name) 427 self._settings[row.name] = json.loads(row.settings) 428 429 def _load_account_settings(self) -> None: 430 account_settings = self._con.execute( 431 'SELECT * FROM account_settings').fetchall() 432 for row in account_settings: 433 log.info('Load account settings: %s', row.account) 434 self._account_settings[row.account] = json.loads(row.settings) 435 436 def _commit_account_settings(self, 437 account: str, 438 schedule: bool = True) -> None: 439 log.info('Set account settings: %s', account) 440 self._con.execute( 441 'UPDATE account_settings SET settings = ? WHERE account = ?', 442 (json.dumps(self._account_settings[account]), account)) 443 444 self._commit(schedule=schedule) 445 446 def _commit_settings(self, name: str, schedule: bool = True) -> None: 447 log.info('Set settings: %s', name) 448 self._con.execute( 449 'UPDATE settings SET settings = ? WHERE name = ?', 450 (json.dumps(self._settings[name]), name)) 451 452 self._commit(schedule=schedule) 453 454 def get_app_setting(self, setting: str) -> SETTING_TYPE: 455 if setting not in APP_SETTINGS: 456 raise ValueError(f'Invalid app setting: {setting}') 457 458 try: 459 return self._settings['app'][setting] 460 except KeyError: 461 return APP_SETTINGS[setting] 462 463 get = get_app_setting 464 465 def set_app_setting(self, setting: str, value: SETTING_TYPE) -> None: 466 if setting not in APP_SETTINGS: 467 raise ValueError(f'Invalid app setting: {setting}') 468 469 default = APP_SETTINGS[setting] 470 if not isinstance(value, type(default)) and value is not None: 471 raise TypeError(f'Invalid type for {setting}: ' 472 f'{value} {type(value)}') 473 474 if value is None: 475 try: 476 del self._settings['app'][setting] 477 except KeyError: 478 pass 479 480 self._commit_settings('app') 481 self._notify(default, setting) 482 return 483 484 self._settings['app'][setting] = value 485 486 self._commit_settings('app') 487 self._notify(value, setting) 488 489 set = set_app_setting 490 491 def get_plugin_setting(self, plugin: str, setting: str) -> SETTING_TYPE: 492 if setting not in PLUGIN_SETTINGS: 493 raise ValueError(f'Invalid plugin setting: {setting}') 494 495 if plugin not in self._settings['plugins']: 496 raise ValueError(f'Unknown plugin {plugin}') 497 498 try: 499 return self._settings['plugins'][plugin][setting] 500 except KeyError: 501 return PLUGIN_SETTINGS[setting] 502 503 def get_plugins(self) -> List[str]: 504 return list(self._settings['plugins'].keys()) 505 506 def set_plugin_setting(self, 507 plugin: str, 508 setting: str, 509 value: bool) -> None: 510 511 if setting not in PLUGIN_SETTINGS: 512 raise ValueError(f'Invalid plugin setting: {setting}') 513 514 default = PLUGIN_SETTINGS[setting] 515 if not isinstance(value, type(default)): 516 raise TypeError(f'Invalid type for {setting}: ' 517 f'{value} {type(value)}') 518 519 if plugin in self._settings['plugins']: 520 self._settings['plugins'][plugin][setting] = value 521 else: 522 self._settings['plugins'][plugin] = {setting: value} 523 524 self._commit_settings('plugins') 525 526 def remove_plugin(self, plugin: str) -> None: 527 try: 528 del self._settings['plugins'][plugin] 529 except KeyError: 530 pass 531 532 def add_account(self, account: str) -> None: 533 log.info('Add account: %s', account) 534 self._account_settings[account] = {'account': {}, 535 'contact': {}, 536 'group_chat': {}} 537 self._con.execute( 538 'INSERT INTO account_settings(account, settings) VALUES(?, ?)', 539 (account, json.dumps(self._account_settings[account]))) 540 self._commit() 541 542 def remove_account(self, account: str) -> None: 543 if account not in self._account_settings: 544 raise ValueError(f'Unknown account: {account}') 545 546 del self._account_settings[account] 547 self._con.execute( 548 'DELETE FROM account_settings WHERE account = ?', 549 (account,)) 550 self._commit() 551 552 def get_accounts(self) -> List[str]: 553 return list(self._account_settings.keys()) 554 555 def get_account_setting(self, 556 account: str, 557 setting: str) -> SETTING_TYPE: 558 559 if account not in self._account_settings: 560 raise ValueError(f'Account missing: {account}') 561 562 if setting not in ACCOUNT_SETTINGS['account']: 563 raise ValueError(f'Invalid account setting: {setting}') 564 565 try: 566 return self._account_settings[account]['account'][setting] 567 except KeyError: 568 return ACCOUNT_SETTINGS['account'][setting] 569 570 def set_account_setting(self, 571 account: str, 572 setting: str, 573 value: SETTING_TYPE) -> None: 574 575 if account not in self._account_settings: 576 raise ValueError(f'Account missing: {account}') 577 578 if setting not in ACCOUNT_SETTINGS['account']: 579 raise ValueError(f'Invalid account setting: {setting}') 580 581 default = ACCOUNT_SETTINGS['account'][setting] 582 if not isinstance(value, type(default)) and value is not None: 583 raise TypeError(f'Invalid type for {setting}: ' 584 f'{value} {type(value)}') 585 586 if value is None: 587 try: 588 del self._account_settings[account]['account'][setting] 589 except KeyError: 590 pass 591 592 self._commit_account_settings(account) 593 self._notify(default, setting, account) 594 return 595 596 self._account_settings[account]['account'][setting] = value 597 598 self._commit_account_settings(account) 599 self._notify(value, setting, account) 600 601 def get_group_chat_setting(self, 602 account: str, 603 jid: str, 604 setting: str) -> SETTING_TYPE: 605 606 if account not in self._account_settings: 607 raise ValueError(f'Account missing: {account}') 608 609 if setting not in ACCOUNT_SETTINGS['group_chat']: 610 raise ValueError(f'Invalid group chat setting: {setting}') 611 612 try: 613 return self._account_settings[account]['group_chat'][jid][setting] 614 except KeyError: 615 616 context = get_muc_context(jid) 617 if context is None: 618 # If there is no disco info available 619 # to determine the context assume public 620 log.warning('Unable to determine context for: %s', jid) 621 context = 'public' 622 623 default = ACCOUNT_SETTINGS['group_chat'][setting] 624 if default is HAS_APP_DEFAULT: 625 context_default_setting = f'gc_{setting}_{context}_default' 626 if context_default_setting in APP_SETTINGS: 627 return self.get_app_setting(context_default_setting) 628 return self.get_app_setting(f'gc_{setting}_default') 629 630 if default is HAS_ACCOUNT_DEFAULT: 631 context_default_setting = f'gc_{setting}_{context}_default' 632 if context_default_setting in ACCOUNT_SETTINGS['account']: 633 return self.get_account_setting(account, 634 context_default_setting) 635 return self.get_account_setting(account, 636 f'gc_{setting}_default') 637 638 return default 639 640 def set_group_chat_setting(self, 641 account: str, 642 jid: str, 643 setting: str, 644 value: SETTING_TYPE) -> None: 645 646 if account not in self._account_settings: 647 raise ValueError(f'Account missing: {account}') 648 649 if setting not in ACCOUNT_SETTINGS['group_chat']: 650 raise ValueError(f'Invalid group chat setting: {setting}') 651 652 default = ACCOUNT_SETTINGS['group_chat'][setting] 653 if default in (HAS_APP_DEFAULT, HAS_ACCOUNT_DEFAULT): 654 655 context = get_muc_context(jid) 656 if context is None: 657 # If there is no disco info available 658 # to determine the context assume public 659 log.warning('Unable to determine context for: %s', jid) 660 context = 'public' 661 662 default_store = APP_SETTINGS 663 if default is HAS_ACCOUNT_DEFAULT: 664 default_store = ACCOUNT_SETTINGS['account'] 665 666 context_default_setting = f'gc_{setting}_{context}_default' 667 if context_default_setting in default_store: 668 default = default_store[context_default_setting] 669 else: 670 default = default_store[f'gc_{setting}_default'] 671 672 if not isinstance(value, type(default)) and value is not None: 673 raise TypeError(f'Invalid type for {setting}: ' 674 f'{value} {type(value)}') 675 676 if value is None: 677 try: 678 del self._account_settings[account]['group_chat'][jid][setting] 679 except KeyError: 680 pass 681 682 self._commit_account_settings(account) 683 self._notify(default, setting, account, jid) 684 return 685 686 group_chat_settings = self._account_settings[account]['group_chat'] 687 if jid not in group_chat_settings: 688 group_chat_settings[jid] = {setting: value} 689 else: 690 group_chat_settings[jid][setting] = value 691 692 self._commit_account_settings(account) 693 self._notify(value, setting, account, jid) 694 695 def set_group_chat_settings(self, 696 setting: str, 697 value: SETTING_TYPE, 698 context: str = None) -> None: 699 700 for account in self._account_settings: 701 for jid in self._account_settings[account]['group_chat']: 702 if context is not None: 703 if get_muc_context(jid) != context: 704 continue 705 self.set_group_chat_setting(account, jid, setting, value) 706 707 def get_contact_setting(self, 708 account: str, 709 jid: str, 710 setting: str) -> SETTING_TYPE: 711 712 if account not in self._account_settings: 713 raise ValueError(f'Account missing: {account}') 714 715 if setting not in ACCOUNT_SETTINGS['contact']: 716 raise ValueError(f'Invalid contact setting: {setting}') 717 718 try: 719 return self._account_settings[account]['contact'][jid][setting] 720 except KeyError: 721 default = ACCOUNT_SETTINGS['contact'][setting] 722 if default is HAS_APP_DEFAULT: 723 return self.get_app_setting(f'{setting}_default') 724 725 if default is HAS_ACCOUNT_DEFAULT: 726 return self.get_account_setting(account, f'{setting}_default') 727 728 return default 729 730 def set_contact_setting(self, 731 account: str, 732 jid: str, 733 setting: str, 734 value: SETTING_TYPE) -> None: 735 736 if account not in self._account_settings: 737 raise ValueError(f'Account missing: {account}') 738 739 if setting not in ACCOUNT_SETTINGS['contact']: 740 raise ValueError(f'Invalid contact setting: {setting}') 741 742 default = ACCOUNT_SETTINGS['contact'][setting] 743 if default in (HAS_APP_DEFAULT, HAS_ACCOUNT_DEFAULT): 744 745 default_store = APP_SETTINGS 746 if default is HAS_ACCOUNT_DEFAULT: 747 default_store = ACCOUNT_SETTINGS['account'] 748 749 default = default_store[f'{setting}_default'] 750 751 if not isinstance(value, type(default)) and value is not None: 752 raise TypeError(f'Invalid type for {setting}: ' 753 f'{value} {type(value)}') 754 755 if value is None: 756 try: 757 del self._account_settings[account]['contact'][jid][setting] 758 except KeyError: 759 pass 760 761 self._commit_account_settings(account) 762 self._notify(default, setting, account, jid) 763 return 764 765 contact_settings = self._account_settings[account]['contact'] 766 if jid not in contact_settings: 767 contact_settings[jid] = {setting: value} 768 else: 769 contact_settings[jid][setting] = value 770 771 self._commit_account_settings(account) 772 self._notify(value, setting, account, jid) 773 774 def set_contact_settings(self, 775 setting: str, 776 value: SETTING_TYPE) -> None: 777 778 for account in self._account_settings: 779 for jid in self._account_settings[account]['contact']: 780 self.set_contact_setting(account, jid, setting, value) 781 782 def set_soundevent_setting(self, 783 event_name: str, 784 setting: str, 785 value: SETTING_TYPE) -> None: 786 787 if event_name not in DEFAULT_SOUNDEVENT_SETTINGS: 788 raise ValueError(f'Invalid soundevent: {event_name}') 789 790 if setting not in DEFAULT_SOUNDEVENT_SETTINGS[event_name]: 791 raise ValueError(f'Invalid soundevent setting: {setting}') 792 793 default = DEFAULT_SOUNDEVENT_SETTINGS[event_name][setting] 794 if not isinstance(value, type(default)): 795 raise TypeError(f'Invalid type for {setting}: ' 796 f'{value} {type(value)}') 797 798 if event_name not in self._settings['soundevents']: 799 self._settings['soundevents'][event_name] = {setting: value} 800 else: 801 self._settings['soundevents'][event_name][setting] = value 802 803 self._commit_settings('soundevents') 804 805 def get_soundevent_settings(self, 806 event_name: str) -> Dict[str, SETTING_TYPE]: 807 if event_name not in DEFAULT_SOUNDEVENT_SETTINGS: 808 raise ValueError(f'Invalid soundevent: {event_name}') 809 810 settings = DEFAULT_SOUNDEVENT_SETTINGS[event_name].copy() 811 user_settings = self._settings['soundevents'].get(event_name, {}) 812 settings.update(user_settings) 813 return settings 814 815 def set_status_preset_setting(self, 816 status_preset: str, 817 setting: str, 818 value: str) -> None: 819 820 if setting not in STATUS_PRESET_SETTINGS: 821 raise ValueError(f'Invalid status preset setting: {setting}') 822 823 if not isinstance(value, str): 824 raise TypeError(f'Invalid type for {setting}: ' 825 f'{value} {type(value)}') 826 827 presets = self._settings['status_presets'] 828 if status_preset not in presets: 829 presets[status_preset] = {setting: value} 830 else: 831 presets[status_preset][setting] = value 832 833 self._commit_settings('status_presets') 834 835 def get_status_preset_settings(self, status_preset: str) -> Dict[str, str]: 836 if status_preset not in self._settings['status_presets']: 837 raise ValueError(f'Invalid status preset name: {status_preset}') 838 839 settings = STATUS_PRESET_SETTINGS.copy() 840 user_settings = self._settings['status_presets'][status_preset] 841 settings.update(user_settings) 842 return settings 843 844 def get_status_presets(self) -> List[str]: 845 return list(self._settings['status_presets'].keys()) 846 847 def remove_status_preset(self, status_preset: str) -> None: 848 if status_preset not in self._settings['status_presets']: 849 raise ValueError(f'Unknown status preset: {status_preset}') 850 851 del self._settings['status_presets'][status_preset] 852 self._commit_settings('status_presets') 853 854 def set_proxy_setting(self, 855 proxy_name: str, 856 setting: str, 857 value: SETTING_TYPE) -> None: 858 859 if setting not in PROXY_SETTINGS: 860 raise ValueError(f'Invalid proxy setting: {setting}') 861 862 default = PROXY_SETTINGS[setting] 863 if not isinstance(value, type(default)): 864 raise TypeError(f'Invalid type for {setting}: ' 865 f'{value} {type(value)}') 866 867 if proxy_name in self._settings['proxies']: 868 self._settings['proxies'][proxy_name][setting] = value 869 else: 870 self._settings['proxies'][proxy_name] = {setting: value} 871 872 self._commit_settings('proxies') 873 874 def get_proxy_settings(self, proxy_name: str) -> Dict[str, SETTING_TYPE]: 875 if proxy_name not in self._settings['proxies']: 876 raise ValueError(f'Unknown proxy: {proxy_name}') 877 878 settings = PROXY_SETTINGS.copy() 879 user_settings = self._settings['proxies'][proxy_name] 880 settings.update(user_settings) 881 return settings 882 883 def get_proxies(self) -> List[str]: 884 return list(self._settings['proxies'].keys()) 885 886 def add_proxy(self, proxy_name: str) -> None: 887 if proxy_name in self._settings['proxies']: 888 raise ValueError(f'Proxy already exists: {proxy_name}') 889 890 self._settings['proxies'][proxy_name] = {} 891 892 def rename_proxy(self, old_proxy_name: str, new_proxy_name: str) -> None: 893 settings = self._settings['proxies'].pop(old_proxy_name) 894 self._settings['proxies'][new_proxy_name] = settings 895 896 def remove_proxy(self, proxy_name: str) -> None: 897 if proxy_name not in self._settings['proxies']: 898 raise ValueError(f'Unknown proxy: {proxy_name}') 899 900 del self._settings['proxies'][proxy_name] 901 self._commit_settings('proxies') 902 903 if self.get_app_setting('global_proxy') == proxy_name: 904 self.set_app_setting('global_proxy', None) 905 906 for account in self._account_settings: 907 if self.get_account_setting(account, 'proxy') == proxy_name: 908 self.set_account_setting(account, 'proxy', None) 909 910 911class LegacyConfig: 912 913 @staticmethod 914 def get(setting: str) -> SETTING_TYPE: 915 return app.settings.get_app_setting(setting) 916 917 @staticmethod 918 def set(setting: str, value: SETTING_TYPE) -> None: 919 app.settings.set_app_setting(setting, value) 920 921 @staticmethod 922 def get_per(kind: str, key: str, setting: str) -> SETTING_TYPE: 923 if kind == 'accounts': 924 return app.settings.get_account_setting(key, setting) 925 926 if kind == 'plugins': 927 return app.settings.get_plugin_setting(key, setting) 928 raise ValueError 929 930 @staticmethod 931 def set_per(kind: str, key: str, setting: str, value: SETTING_TYPE) -> None: 932 if kind == 'accounts': 933 app.settings.set_account_setting(key, setting, value) 934 raise ValueError 935