config.py 22.7 KB
Newer Older
1
2
"""
Defines the global config instance, used to get or set (and save) values
3
4
5
6
7
from/to the config file.

This module has the particularity that some imports and global variables
are delayed because it would mean doing an incomplete setup of the python
loggers.
8
9
10

TODO: get http://bugs.python.org/issue1410680 fixed, one day, in order
to remove our ugly custom I/O methods.
11
12
"""

13
import logging.config
mathieui's avatar
mathieui committed
14
import os
15
import stat
mathieui's avatar
mathieui committed
16
import sys
mathieui's avatar
mathieui committed
17
import pkg_resources
mathieui's avatar
mathieui committed
18

19
from configparser import RawConfigParser, NoOptionError, NoSectionError
20
from pathlib import Path
mathieui's avatar
mathieui committed
21
22
23
from shutil import copy2
from typing import Callable, Dict, List, Optional, Union, Tuple

mathieui's avatar
mathieui committed
24
from poezio.args import parse_args
25
from poezio import xdg
26

mathieui's avatar
mathieui committed
27
28
29
30
ConfigValue = Union[str, int, float, bool]

DEFSECTION = "Poezio"

31
32
33
34
35
36
DEFAULT_CONFIG = {
    'Poezio': {
        'ack_message_receipts': True,
        'add_space_after_completion': True,
        'after_completion': ',',
        'alternative_nickname': '',
37
        'auto_reconnect': True,
38
39
        'autorejoin_delay': '5',
        'autorejoin': False,
40
        'beep_on': 'highlight private invite disconnect',
41
        'bookmark_on_join': False,
42
43
        'ca_cert_path': '',
        'certificate': '',
mathieui's avatar
mathieui committed
44
        'certfile': '',
45
        'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL',
46
47
        'connection_check_interval': 300,
        'connection_timeout_delay': 30,
48
49
50
51
        'create_gaps': False,
        'custom_host': '',
        'custom_port': '',
        'default_nick': '',
52
        'default_muc_service': '',
53
        'deterministic_nick_colors': True,
54
        'device_id': '',
55
        'nick_color_aliases': True,
56
57
58
59
60
        'display_activity_notifications': False,
        'display_gaming_notifications': False,
        'display_mood_notifications': False,
        'display_tune_notifications': False,
        'display_user_color_in_join_part': True,
61
        'enable_avatars': True,
mathieui's avatar
mathieui committed
62
        'enable_carbons': True,
63
        'enable_css_parsing': True,
64
65
66
67
68
        'enable_user_activity': True,
        'enable_user_gaming': True,
        'enable_user_mood': True,
        'enable_user_nick': True,
        'enable_user_tune': True,
69
        'enable_vertical_tab_list': True,
70
        'enable_xhtml_im': True,
mathieui's avatar
mathieui committed
71
        'enable_smacks': False,
mathieui's avatar
mathieui committed
72
        'eval_password': '',
73
74
75
76
        'exec_remote': False,
        'extract_inline_images': True,
        'filter_info_messages': '',
        'force_encryption': True,
mathieui's avatar
mathieui committed
77
        'force_remote_bookmarks': False,
78
        'go_to_previous_tab_on_alt_number': False,
79
80
81
82
83
84
85
        'group_corrections': True,
        'hide_exit_join': -1,
        'hide_status_change': 120,
        'hide_user_list': False,
        'highlight_on': '',
        'ignore_certificate': False,
        'ignore_private': False,
86
        'image_use_half_blocks': False,
87
        'information_buffer_popup_on': 'error roster warning help info',
88
        'information_buffer_type_filter': '',
89
        'jid': '',
mathieui's avatar
mathieui committed
90
        'keyfile': '',
91
92
93
94
95
96
97
98
        'lang': 'en',
        'lazy_resize': True,
        'log_dir': '',
        'log_errors': True,
        'max_lines_in_memory': 2048,
        'max_messages_in_memory': 2048,
        'max_nick_length': 25,
        'muc_history_length': 50,
99
        'notify_messages': True,
100
101
102
103
104
105
106
107
108
109
110
111
112
113
        'open_all_bookmarks': False,
        'password': '',
        'plugins_autoload': '',
        'plugins_conf_dir': '',
        'plugins_dir': '',
        'popup_time': 4,
        'private_auto_response': '',
        'remote_fifo_path': './',
        'request_message_receipts': True,
        'rooms': '',
        'roster_group_sort': 'name',
        'roster_show_offline': False,
        'roster_sort': 'jid:show',
        'save_status': True,
114
        'self_ping_interval': 0,
115
        'self_ping_timeout': 60,
116
117
118
119
120
121
122
123
124
        'send_chat_states': True,
        'send_initial_presence': True,
        'send_os_info': True,
        'send_poezio_info': True,
        'send_time': True,
        'separate_history': False,
        'server': 'anon.jeproteste.info',
        'show_composing_tabs': 'direct',
        'show_inactive_tabs': True,
125
        'show_jid_in_conversations': True,
126
        'show_muc_jid': False,
127
128
129
130
131
132
        'show_roster_jids': True,
        'show_roster_subscriptions': '',
        'show_s2s_errors': True,
        'show_tab_names': False,
        'show_tab_numbers': True,
        'show_timestamps': True,
133
        'show_useless_separator': True,
134
135
136
137
138
        'status': '',
        'status_message': '',
        'theme': 'default',
        'themes_dir': '',
        'tmp_image_dir': '',
139
        'unique_prefix_tab_names': False,
140
        'use_bookmarks_method': '',
141
        'use_log': True,
142
143
144
145
146
147
148
149
150
151
152
153
154
155
        'use_remote_bookmarks': True,
        'user_list_sort': 'desc',
        'use_tab_nicks': True,
        'vertical_tab_list_size': 20,
        'vertical_tab_list_sort': 'desc',
        'whitespace_interval': 300,
        'words': ''
    },
    'bindings': {
        'M-i': '^I'
    },
    'var': {
        'folded_roster_groups': '',
        'info_win_height': 2
156
    },
mathieui's avatar
mathieui committed
157
    'muc_colors': {}
158
159
}

mathieui's avatar
mathieui committed
160

161
162
163
164
class Config(RawConfigParser):
    """
    load/save the config to a file
    """
mathieui's avatar
mathieui committed
165

mathieui's avatar
mathieui committed
166
    def __init__(self, file_name: Path, default=None) -> None:
167
        RawConfigParser.__init__(self, None)
168
        # make the options case sensitive
169
        self.optionxform = lambda param: str(param)
170
        self.file_name = file_name
171
        self.read_file()
172
        self.default = default
173
174

    def read_file(self):
175
        RawConfigParser.read(self, str(self.file_name), encoding='utf-8')
176
        # Check config integrity and fix it if it’s wrong
177
178
179
180
181
        # only when the object is the main config
        if self.__class__ is Config:
            for section in ('bindings', 'var'):
                if not self.has_section(section):
                    self.add_section(section)
182

mathieui's avatar
mathieui committed
183
184
185
186
    def get(self,
            option: str,
            default: Optional[ConfigValue] = None,
            section=DEFSECTION) -> ConfigValue:
187
188
189
190
191
192
        """
        get a value from the config but return
        a default value if it is not found
        The type of default defines the type
        returned
        """
193
194
195
196
197
198
        if default is None:
            if self.default:
                default = self.default.get(section, {}).get(option)
            else:
                default = ''

199
        try:
mathieui's avatar
mathieui committed
200
201
202
            if isinstance(default, bool):
                res = self.getboolean(option, section)
            elif isinstance(default, int):
203
                res = self.getint(option, section)
mathieui's avatar
mathieui committed
204
            elif isinstance(default, float):
205
                res = self.getfloat(option, section)
206
            else:
207
                res = self.getstr(option, section)
208
        except (NoOptionError, NoSectionError, ValueError, AttributeError):
209
            return default if default is not None else ''
210

211
        if res is None:
212
213
214
            return default
        return res

mathieui's avatar
mathieui committed
215
216
    def get_by_tabname(self,
                       option,
217
                       tabname: str,
mathieui's avatar
mathieui committed
218
219
220
                       fallback=True,
                       fallback_server=True,
                       default=''):
221
222
223
224
225
226
        """
        Try to get the value for the option. First we look in
        a section named `tabname`, if the option is not present
        in the section, we search for the global option if fallback is
        True. And we return `default` as a fallback as a last resort.
        """
mathieui's avatar
mathieui committed
227
        from slixmpp import JID
228
229
        if isinstance(tabname, JID):
            tabname = tabname.full
230
231
        if self.default and (not default) and fallback:
            default = self.default.get(DEFSECTION, {}).get(option, '')
232
233
234
235
        if tabname in self.sections():
            if option in self.options(tabname):
                # We go the tab-specific option
                return self.get(option, default, tabname)
236
237
        if fallback_server:
            return self.get_by_servname(tabname, option, default, fallback)
238
239
240
241
242
        if fallback:
            # We fallback to the global option
            return self.get(option, default)
        return default

243
244
245
246
247
248
249
250
251
252
253
254
255
    def get_by_servname(self, jid, option, default, fallback=True):
        """
        Try to get the value of an option for a server
        """
        server = safeJID(jid).server
        if server:
            server = '@' + server
            if server in self.sections() and option in self.options(server):
                return self.get(option, default, server)
        if fallback:
            return self.get(option, default)
        return default

256
    def __get(self, option, section=DEFSECTION, **kwargs):
257
258
259
        """
        facility for RawConfigParser.get
        """
260
261
262
263
264
265
266
        return RawConfigParser.get(self, section, option, **kwargs)

    def _get(self, section, conv, option, **kwargs):
        """
        Redirects RawConfigParser._get
        """
        return conv(self.__get(option, section, **kwargs))
267

268
    def getstr(self, option, section=DEFSECTION):
269
270
271
        """
        get a value and returns it as a string
        """
272
        return self.__get(option, section)
273

274
    def getint(self, option, section=DEFSECTION):
275
276
277
        """
        get a value and returns it as an int
        """
278
        return RawConfigParser.getint(self, section, option)
279

280
    def getfloat(self, option, section=DEFSECTION):
281
282
283
        """
        get a value and returns it as a float
        """
284
        return RawConfigParser.getfloat(self, section, option)
285

286
    def getboolean(self, option, section=DEFSECTION):
287
288
289
        """
        get a value and returns it as a boolean
        """
290
        return RawConfigParser.getboolean(self, section, option)
291

mathieui's avatar
mathieui committed
292
293
    def write_in_file(self, section: str, option: str,
                      value: ConfigValue) -> bool:
294
295
296
297
298
        """
        Our own way to save write the value in the file
        Just find the right section, and then find the
        right option, and edit it.
        """
299
300
301
        result = self._parse_file()
        if not result:
            return False
302
        else:
303
            sections, result_lines = result
304

305
        if section not in sections:
306
            result_lines.append('[%s]' % section)
mathieui's avatar
mathieui committed
307
            result_lines.append('%s = %s' % (option, value))
308
309
        else:
            begin, end = sections[section]
310
311
            pos = find_line(result_lines, begin, end, option)

312
            if pos == -1:
313
                result_lines.insert(end, '%s = %s' % (option, value))
314
315
316
317
318
            else:
                result_lines[pos] = '%s = %s' % (option, value)

        return self._write_file(result_lines)

mathieui's avatar
mathieui committed
319
    def remove_in_file(self, section: str, option: str) -> bool:
320
321
322
323
324
325
326
327
328
        """
        Our own way to remove an option from the file.
        """
        result = self._parse_file()
        if not result:
            return False
        else:
            sections, result_lines = result

329
        if section not in sections:
mathieui's avatar
mathieui committed
330
331
332
            log.error(
                'Tried to remove the option %s from a non-'
                'existing section (%s)', option, section)
333
334
335
336
337
            return True
        else:
            begin, end = sections[section]
            pos = find_line(result_lines, begin, end, option)

338
            if pos == -1:
mathieui's avatar
mathieui committed
339
340
341
                log.error(
                    'Tried to remove a non-existing option %s'
                    ' from section %s', option, section)
342
343
344
                return True
            else:
                del result_lines[pos]
345
346
347

        return self._write_file(result_lines)

mathieui's avatar
mathieui committed
348
    def _write_file(self, lines: List[str]) -> bool:
349
350
351
352
        """
        Write the config file, write to a temporary file
        before copying it to the final destination
        """
mathieui's avatar
mathieui committed
353
        try:
mathieui's avatar
mathieui committed
354
355
            filename = self.file_name.parent / (
                '.%s.tmp' % self.file_name.name)
356
357
            with os.fdopen(
                    os.open(
Link Mauve's avatar
Link Mauve committed
358
                        str(filename),
359
360
361
362
363
364
365
                        os.O_WRONLY | os.O_CREAT,
                        0o600,
                    ),
                    'w',
                    encoding='utf-8') as fd:
                for line in lines:
                    fd.write('%s\n' % line)
366
            filename.replace(self.file_name)
mathieui's avatar
mathieui committed
367
368
        except:
            success = False
369
            log.error('Unable to save the config file.', exc_info=True)
mathieui's avatar
mathieui committed
370
371
372
        else:
            success = True
        return success
373

mathieui's avatar
mathieui committed
374
    def _parse_file(self) -> Optional[Tuple[Dict[str, List[int]], List[str]]]:
375
376
377
378
379
380
381
382
383
384
        """
        Parse the config file and return the list of sections with
        their start and end positions, and the lines in the file.

        Duplicate sections are preserved but ignored for the parsing.

        Returns an empty tuple if reading fails
        """
        if file_ok(self.file_name):
            try:
385
                with self.file_name.open('r', encoding='utf-8') as df:
mathieui's avatar
mathieui committed
386
387
                    lines_before = [line.strip()
                                    for line in df]  # type: List[str]
388
            except OSError:
mathieui's avatar
mathieui committed
389
390
391
392
                log.error(
                    'Unable to read the config file %s',
                    self.file_name,
                    exc_info=True)
mathieui's avatar
mathieui committed
393
                return None
394
395
396
        else:
            lines_before = []

mathieui's avatar
mathieui committed
397
        sections = {}  # type: Dict[str, List[int]]
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
        duplicate_section = False
        current_section = ''
        current_line = 0

        for line in lines_before:
            if line.startswith('['):
                if not duplicate_section and current_section:
                    sections[current_section][1] = current_line

                duplicate_section = False
                current_section = line[1:-1]

                if current_section in sections:
                    log.error('Error while reading the configuration file,'
                              ' skipping until next section')
                    duplicate_section = True
                else:
                    sections[current_section] = [current_line, current_line]

            current_line += 1
418
        if not duplicate_section and current_section:
419
420
            sections[current_section][1] = current_line

421
        return (sections, lines_before)
422

mathieui's avatar
mathieui committed
423
424
    def set_and_save(self, option: str, value: ConfigValue,
                     section=DEFSECTION) -> Tuple[str, str]:
425
426
427
428
        """
        set the value in the configuration then save it
        to the file
        """
429
430
431
432
433
        # Special case for a 'toggle' value. We take the current value
        # and set the opposite. Warning if the no current value exists
        # or it is not a bool.
        if value == "toggle":
            current = self.get(option, "", section)
434
435
            if isinstance(current, bool):
                value = str(not current)
436
            else:
437
438
439
440
441
                if current.lower() == "false":
                    value = "true"
                elif current.lower() == "true":
                    value = "false"
                else:
mathieui's avatar
mathieui committed
442
443
444
445
                    return (
                        'Could not toggle option: %s.'
                        ' Current value is %s.' % (option, current or "empty"),
                        'Warning')
446
447
448
449
        if self.has_section(section):
            RawConfigParser.set(self, section, option, value)
        else:
            self.add_section(section)
450
            RawConfigParser.set(self, section, option, value)
mathieui's avatar
mathieui committed
451
        if not self.write_in_file(section, option, value):
452
            return ('Unable to write in the config file', 'Error')
453
        if isinstance(option, str) and 'password' in option and 'eval_password' not in option:
454
            value = '********'
mathieui's avatar
mathieui committed
455
        return ("%s=%s" % (option, value), 'Info')
456

mathieui's avatar
mathieui committed
457
458
    def remove_and_save(self, option: str,
                        section=DEFSECTION) -> Tuple[str, str]:
459
460
461
462
463
464
        """
        Remove an option and then save it the config file
        """
        if self.has_section(section):
            RawConfigParser.remove_option(self, section, option)
        if not self.remove_in_file(section, option):
465
466
            return ('Unable to save the config file', 'Error')
        return ('Option %s deleted' % option, 'Info')
467

mathieui's avatar
mathieui committed
468
    def silent_set(self, option: str, value: ConfigValue, section=DEFSECTION):
mathieui's avatar
mathieui committed
469
470
471
472
473
474
475
476
477
        """
        Set a value, save, and return True on success and False on failure
        """
        if self.has_section(section):
            RawConfigParser.set(self, section, option, value)
        else:
            self.add_section(section)
            RawConfigParser.set(self, section, option, value)
        return self.write_in_file(section, option, value)
478

mathieui's avatar
mathieui committed
479
    def set(self, option: str, value: ConfigValue, section=DEFSECTION):
480
481
482
483
484
485
486
487
        """
        Set the value of an option temporarily
        """
        try:
            RawConfigParser.set(self, section, option, value)
        except NoSectionError:
            pass

mathieui's avatar
mathieui committed
488
    def to_dict(self) -> Dict[str, Dict[str, ConfigValue]]:
489
490
491
        """
        Returns a dict of the form {section: {option: value, option: value}, …}
        """
mathieui's avatar
mathieui committed
492
        res = {}  # Dict[str, Dict[str, ConfigValue]]
493
494
495
496
497
        for section in self.sections():
            res[section] = {}
            for option in self.options(section):
                res[section][option] = self.get(option, "", section)
        return res
498
499


mathieui's avatar
mathieui committed
500
def find_line(lines: List[str], start: int, end: int, option: str) -> int:
501
502
503
504
505
506
507
508
    """
    Get the number of the line containing the option in the
    relevant part of the config file.

    Returns -1 if the option isn’t found
    """
    current = start
    for line in lines[start:end]:
mathieui's avatar
mathieui committed
509
510
        if (line.startswith('%s ' % option)
                or line.startswith('%s=' % option)):
511
512
513
514
            return current
        current += 1
    return -1

mathieui's avatar
mathieui committed
515

mathieui's avatar
mathieui committed
516
def file_ok(filepath: Path) -> bool:
517
518
519
520
    """
    Returns True if the file exists and is readable and writeable,
    False otherwise.
    """
521
    val = filepath.exists()
Link Mauve's avatar
Link Mauve committed
522
    val &= os.access(str(filepath), os.R_OK | os.W_OK)
523
524
    return bool(val)

mathieui's avatar
mathieui committed
525

mathieui's avatar
mathieui committed
526
def get_image_cache() -> Path:
527
528
529
530
531
532
533
534
    if not config.get('extract_inline_images'):
        return None
    tmp_dir = config.get('tmp_image_dir')
    if tmp_dir:
        return Path(tmp_dir)
    return xdg.CACHE_HOME / 'images'


535
536
537
538
539
540
541
542
def check_config():
    """
    Check the config file and print results
    """
    result = {'missing': [], 'changed': []}
    for option in DEFAULT_CONFIG['Poezio']:
        value = config.get(option)
        if value != DEFAULT_CONFIG['Poezio'][option]:
mathieui's avatar
mathieui committed
543
544
            result['changed'].append((option, value,
                                      DEFAULT_CONFIG['Poezio'][option]))
545
546
547
548
549
550
551
552
553
554
        else:
            value = config.get(option, default='')
            upper = value.upper()
            default = str(DEFAULT_CONFIG['Poezio'][option]).upper()
            if upper != default:
                result['missing'].append(option)

    result['changed'].sort(key=lambda x: x[0])
    result['missing'].sort()
    if result['changed']:
mathieui's avatar
mathieui committed
555
556
        print(
            '\033[1mOptions changed from the default configuration:\033[0m\n')
557
        for option, new_value, default in result['changed']:
mathieui's avatar
mathieui committed
558
559
560
            print(
                '    \033[1m%s\033[0m = \033[33m%s\033[0m (default: \033[32m%s\033[0m)'
                % (option, new_value, default))
561
562
563
564
565
566

    if result['missing']:
        print('\n\033[1mMissing options:\033[0m (the defaults are used)\n')
        for option in result['missing']:
            print('    \033[31m%s\033[0m' % option)

mathieui's avatar
mathieui committed
567

568
def run_cmdline_args():
569
570
    "Parse the command line arguments"
    global options
571
    options = parse_args(xdg.CONFIG_HOME)
572
573

    # Copy a default file if none exists
574
575
576
577
    if not options.filename.is_file():
        try:
            options.filename.parent.mkdir(parents=True, exist_ok=True)
        except OSError as e:
mathieui's avatar
mathieui committed
578
579
            sys.stderr.write(
                'Poezio was unable to create the config directory: %s\n' % e)
580
581
            sys.exit(1)
        default = Path(__file__).parent / '..' / 'data' / 'default_config.cfg'
mathieui's avatar
mathieui committed
582
583
        other = Path(
            pkg_resources.resource_filename('poezio', 'default_config.cfg'))
584
        if default.is_file():
585
            copy2(str(default), str(options.filename))
586
        elif other.is_file():
587
            copy2(str(other), str(options.filename))
588
589
590
591

        # Inside the nixstore and possibly other distributions, the reference
        # file is readonly, so is the copy.
        # Make it writable by the user who just created it.
592
        if options.filename.exists():
mathieui's avatar
mathieui committed
593
594
            options.filename.chmod(options.filename.stat().st_mode
                                   | stat.S_IWUSR)
595

596
597
598
        global firstrun
        firstrun = True

mathieui's avatar
mathieui committed
599

600
601
602
603
def create_global_config():
    "Create the global config object, or crash"
    try:
        global config
604
        config = Config(options.filename, DEFAULT_CONFIG)
605
606
607
608
609
610
611
    except:
        import traceback
        sys.stderr.write('Poezio was unable to read or'
                         ' parse the config file.\n')
        traceback.print_exc(limit=0)
        sys.exit(1)

mathieui's avatar
mathieui committed
612

613
614
def setup_logging():
    "Change the logging config according to the cmdline options and config"
615
    global LOG_DIR
616
617
    LOG_DIR = config.get('log_dir')
    LOG_DIR = Path(LOG_DIR).expanduser() if LOG_DIR else xdg.DATA_HOME / 'logs'
618
    if config.get('log_errors'):
619
620
621
622
623
624
625
626
627
628
629
630
631
632
        try:
            LOG_DIR.mkdir(parents=True, exist_ok=True)
        except OSError:
            # We can’t really log any error here, because logging isn’t setup yet.
            pass
        else:
            LOGGING_CONFIG['root']['handlers'].append('error')
            LOGGING_CONFIG['handlers']['error'] = {
                'level': 'ERROR',
                'class': 'logging.FileHandler',
                'filename': str(LOG_DIR / 'errors.log'),
                'formatter': 'simple',
            }
            logging.disable(logging.WARNING)
633
634
635
636

    if options.debug:
        LOGGING_CONFIG['root']['handlers'].append('debug')
        LOGGING_CONFIG['handlers']['debug'] = {
mathieui's avatar
mathieui committed
637
638
639
640
641
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': options.debug,
            'formatter': 'simple',
        }
642
        logging.disable(logging.NOTSET)
643
644
645
646

    if LOGGING_CONFIG['root']['handlers']:
        logging.config.dictConfig(LOGGING_CONFIG)
    else:
647
        logging.disable(logging.ERROR)
648
649
650
651
652
        logging.basicConfig(level=logging.CRITICAL)

    global log
    log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
653

654
def post_logging_setup():
louiz’'s avatar
louiz’ committed
655
    # common imports slixmpp, which creates then its loggers, so
656
    # it needs to be after logger configuration
mathieui's avatar
mathieui committed
657
    from poezio.common import safeJID as JID
658
659
    global safeJID
    safeJID = JID
660

mathieui's avatar
mathieui committed
661

662
663
664
665
666
LOGGING_CONFIG = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'simple': {
667
            'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s'
668
669
        }
    },
mathieui's avatar
mathieui committed
670
    'handlers': {},
671
    'root': {
mathieui's avatar
mathieui committed
672
673
674
        'handlers': [],
        'propagate': True,
        'level': 'DEBUG',
675
676
677
    }
}

678
679
680
# True if this is the first run, in this case we will display
# some help in the info buffer
firstrun = False
681

682
# Global config object. Is setup in poezio.py
mathieui's avatar
mathieui committed
683
config = None  # type: Config
684

685
# The logger object for this module
mathieui's avatar
mathieui committed
686
log = None  # type: Optional[logging.Logger]
687

688
689
# The command-line options
options = None
690

691
# delayed import from common.py
mathieui's avatar
mathieui committed
692
safeJID = None  # type: Optional[Callable]
693

694
# the global log dir
695
LOG_DIR = Path()