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