diff options
44 files changed, 952 insertions, 392 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..28bf3dd3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.4" +install: + - pip install -r requirements.txt + - python setup.py build_ext --inplace +script: make test @@ -32,6 +32,10 @@ uninstall: doc: make -C doc/ html + +test: + py.test -v test/ + pot: xgettext src/*.py --from-code=utf-8 --keyword=_ -o locale/poezio.pot @@ -45,4 +49,4 @@ release: tar cJf poezio-$(version).tar.xz poezio-$(version) && \ tar czf poezio-$(version).tar.gz poezio-$(version) -.PHONY : doc +.PHONY : doc test diff --git a/data/default_config.cfg b/data/default_config.cfg index c1f766b0..35bc498b 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -379,6 +379,14 @@ ack_message_receipts = true # Ask for message delivery receipts (XEP-0184) request_message_receipts = true +# Extract base64 images received in XHTML-IM messages +# if true. +extract_inline_images = true + +# The directory where the images will be saved; if unset, +# defaults to $XDG_CACHE_HOME/poezio/images. +tmp_image_dir = + # Receive the tune notifications or not (in order to display informations # in the roster). # If this is set to false, then the display_tune_notifications diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 32d82f7a..d0ff5bbe 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -316,6 +316,14 @@ to understand what is :ref:`carbons <carbons-details>` or If this is set to ``false``, you will no longer be subscribed to tune events, and the :term:`display_tune_notifications` option will be ignored. + group_corrections + + **Default value:** ``true`` + + Enable a message to “correct” (replace) another message in the display if the + sender intended it as such. See :ref:`Message Correction <correct-feature>` for + more information. + use_bookmark_method **Default value:** ``[empty]`` @@ -851,6 +859,25 @@ Other The lang some automated entities will use when replying to you. + extract_inline_images + + **Default value:** ``true`` + + Some clients send inline images in base64 inside some messages, which results in + an useless wall of text. If this option is ``true``, then that base64 text will + be replaced with a :file:`file://` link to the image file extracted in + :term:`tmp_image_dir` or :file:`$XDG_CACHE_HOME/poezio/images` by default, which + is usually :file:`~/.cache/poezio/images` + + tmp_image_dir + + **Default value:** ``[empty]`` + + The directory where poezio will save the images received, if + :term:`extract_inline_images` is set to true. If unset, poezio + will default to :file:`$XDG_CACHE_HOME/poezio/images` which is + usually :file:`~/.cache/poezio/images`. + muc_history_length **Default value:** ``50`` diff --git a/doc/source/install.rst b/doc/source/install.rst index b655894b..dbe5ddfa 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -80,6 +80,8 @@ Poezio depends on two libraries: - DNSPython_ (the python3 version, often called dnspython3) - SleekXMPP_ +Additionally, it needs *python3-setuptools* to install an executable file. + If you do not want to install those libraries, you can skip directly to the :ref:`installation part <poezio-install-label>` @@ -139,7 +141,8 @@ If you have git installed, it will download and update locally the libraries for you. (and if you don’t have git installed, install it) -If you really want to install it, run as root (or sudo in ubuntu or whatever): +If you really want to install it, first install the *python3-setuptools* package +in your distribution, then run as root (or sudo in ubuntu or whatever): .. code-block:: bash diff --git a/doc/source/misc/correct.rst b/doc/source/misc/correct.rst index 61100634..fda4abcb 100644 --- a/doc/source/misc/correct.rst +++ b/doc/source/misc/correct.rst @@ -1,3 +1,5 @@ +.. _correct-feature: + Message Correction ================== diff --git a/plugins/otr.py b/plugins/otr.py index c2e5a663..44fdb323 100644 --- a/plugins/otr.py +++ b/plugins/otr.py @@ -210,7 +210,7 @@ def hl(tab): conv_jid = safeJID(tab.name) if 'private' in config.get('beep_on', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, conv_jid.bare, False): + if not config.get_by_tabname('disable_beep', conv_jid.bare, default=False): curses.beep() class PoezioContext(Context): @@ -430,11 +430,11 @@ class Plugin(BasePlugin): jid = safeJID(jid).full if not jid in self.contexts: flags = POLICY_FLAGS.copy() - policy = self.config.get_by_tabname('encryption_policy', 'ondemand', jid).lower() - logging_policy = self.config.get_by_tabname('log', 'false', jid).lower() - allow_v2 = self.config.get_by_tabname('allow_v2', 'true', jid).lower() + policy = self.config.get_by_tabname('encryption_policy', jid, default='ondemand').lower() + logging_policy = self.config.get_by_tabname('log', jid, default='false').lower() + allow_v2 = self.config.get_by_tabname('allow_v2', jid, default='true').lower() flags['ALLOW_V2'] = (allow_v2 != 'false') - allow_v1 = self.config.get_by_tabname('allow_v1', 'false', jid).lower() + allow_v1 = self.config.get_by_tabname('allow_v1', jid, default='false').lower() flags['ALLOW_V1'] = (allow_v1 == 'true') self.contexts[jid] = PoezioContext(self.account, jid, self.core.xmpp, self.core) self.contexts[jid].log = 1 if logging_policy != 'false' else 0 @@ -544,7 +544,7 @@ class Plugin(BasePlugin): nick_color = get_theme().COLOR_REMOTE_USER body = txt.decode() - if self.config.get_by_tabname('decode_xhtml', True, msg['from'].bare): + if self.config.get_by_tabname('decode_xhtml', msg['from'].bare, default=True): try: body = xhtml.xhtml_to_poezio_colors(body, force=True) except: diff --git a/plugins/screen_detach.py b/plugins/screen_detach.py index 3552a179..53827c11 100644 --- a/plugins/screen_detach.py +++ b/plugins/screen_detach.py @@ -7,15 +7,14 @@ import os import stat import pyinotify -SCREEN_DIR = '/var/run/screen/S-%s' % (os.getlogin(),) - class Plugin(BasePlugin): def init(self): + screen_dir = '/var/run/screen/S-%s' % (os.getlogin(),) self.timed_event = None sock_path = None self.thread = None - for f in os.listdir(SCREEN_DIR): - path = os.path.join(SCREEN_DIR, f) + for f in os.listdir(screen_dir): + path = os.path.join(screen_dir, f) if screen_attached(path): sock_path = path self.attached = True diff --git a/requirements.txt b/requirements.txt index 462dc735..79d2a470 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ --e git://github.com/afflux/pure-python-otr.git#egg=potr +git+git://github.com/afflux/pure-python-otr.git#egg=potr sleekxmpp==1.2 dnspython3==1.11.1 sphinx==1.2.1 +setuptools argparse pyinotify python-mpd2 diff --git a/scripts/poezio b/scripts/poezio deleted file mode 100755 index 665edcaa..00000000 --- a/scripts/poezio +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/python3 - -from poezio import main -main() @@ -1,7 +1,13 @@ #!/usr/bin/env python3 + try: from setuptools import setup, Extension except ImportError: + print('Setuptools was not found.\n' + 'This script will use distutils instead, which will NOT' + ' be able to install a `poezio` executable.\nIf you are ' + 'using it to build a package or install poezio, please ' + 'install setuptools.\n\nYou will also see a few warnings.\n') from distutils.core import setup, Extension import os @@ -53,11 +59,12 @@ setup(name="poezio", 'poezio_plugins', 'poezio_plugins.gpg', 'poezio_themes'], package_dir = {'poezio': 'src', 'poezio_plugins': 'plugins', 'poezio_themes': 'data/themes'}, package_data = {'poezio': ['default_config.cfg']}, - scripts = ['scripts/poezio', 'scripts/poezio_gpg_export'], + scripts = ['scripts/poezio_gpg_export'], + entry_points={ 'console_scripts': [ 'poezio = poezio:main' ] }, data_files = [('share/man/man1/', ['data/poezio.1'])], - install_requires = ['sleekxmpp==1.2.4', - 'dnspython3>=1.11.1'], + install_requires = ['sleekxmpp>=1.2.4', + 'dnspython3>=1.10.0'], extras_require = {'OTR plugin': 'python-potr>=1.0', 'Screen autoaway plugin': 'pyinotify==0.9.4'} ) diff --git a/src/bookmark.py b/src/bookmark.py index 6d271652..15a28c9d 100644 --- a/src/bookmark.py +++ b/src/bookmark.py @@ -25,7 +25,7 @@ def xml_iter(xml, tag=''): else: return xml.getiterator(tag) -preferred = config.get('use_bookmarks_method', 'pep').lower() +preferred = config.get('use_bookmarks_method').lower() if preferred not in ('pep', 'privatexml'): preferred = 'privatexml' not_preferred = 'privatexml' if preferred == 'pep' else 'pep' @@ -159,8 +159,8 @@ def save(xmpp, core=None): core.information('Could not save bookmarks.', 'Error') elif core: core.information('Bookmarks saved', 'Info') - if config.get('use_remote_bookmarks', True): - preferred = config.get('use_bookmarks_method', 'privatexml') + if config.get('use_remote_bookmarks'): + preferred = config.get('use_bookmarks_method') cb = functools.partial(_cb, core) save_remote(xmpp, cb, method=preferred) @@ -201,7 +201,7 @@ def get_remote(xmpp, callback): """Add the remotely stored bookmarks to the list.""" if xmpp.anon: return - method = config.get('use_bookmarks_method', '') + method = config.get('use_bookmarks_method') if not method: available_methods = {} def _save_and_call_callback(): @@ -232,7 +232,7 @@ def save_bookmarks_method(available_methods): def get_local(): """Add the locally stored bookmarks to the list.""" - rooms = config.get('rooms', '') + rooms = config.get('rooms') if not rooms: return rooms = rooms.split(':') @@ -244,7 +244,7 @@ def get_local(): nick = jid.resource else: nick = None - passwd = config.get_by_tabname('password', '', jid.bare, fallback=False) or None + passwd = config.get_by_tabname('password', jid.bare, fallback=False) or None b = Bookmark(jid.bare, autojoin=True, nick=nick, password=passwd, method='local') if not get_by_jid(b.jid): bookmarks.append(b) diff --git a/src/common.py b/src/common.py index d50e7027..a62c83f1 100644 --- a/src/common.py +++ b/src/common.py @@ -188,17 +188,6 @@ def datetime_tuple(timestamp): :param str timestamp: The string containing the formatted date. :return: The date. :rtype: :py:class:`datetime.datetime` - - >>> time.timezone = 0; time.altzone = 0 - >>> datetime_tuple('20130226T06:23:12') - datetime.datetime(2013, 2, 26, 6, 23, 12) - >>> datetime_tuple('2013-02-26T06:23:12+02:00') - datetime.datetime(2013, 2, 26, 4, 23, 12) - >>> time.timezone = -3600; time.altzone = -3600 - >>> datetime_tuple('20130226T07:23:12') - datetime.datetime(2013, 2, 26, 8, 23, 12) - >>> datetime_tuple('2013-02-26T07:23:12+02:00') - datetime.datetime(2013, 2, 26, 6, 23, 12) """ timestamp = timestamp.replace('-', '', 2).replace(':', '') date = timestamp[:15] @@ -227,15 +216,10 @@ def datetime_tuple(timestamp): def get_utc_time(local_time=None): """ - Get the current time in UTC + Get the current UTC time :param datetime local_time: The current local time :return: The current UTC time - >>> delta = timedelta(seconds=-3600) - >>> d = datetime.now() - >>> time.timezone = -3600; time.altzone = -3600 - >>> get_utc_time(local_time=d) == d + delta - True """ if local_time is None: local_time = datetime.now() @@ -258,12 +242,6 @@ def get_utc_time(local_time=None): def get_local_time(utc_time): """ Get the local time from an UTC time - - >>> delta = timedelta(seconds=-3600) - >>> d = datetime.now() - >>> time.timezone = -3600; time.altzone = -3600 - >>> get_local_time(d) == d - delta - True """ if OLD_PYTHON: isdst = time.localtime(int(utc_time.strftime("%s"))).tm_isdst @@ -315,16 +293,6 @@ def shell_split(st): >>> shell_split('"sdf 1" "toto 2"') ['sdf 1', 'toto 2'] - >>> shell_split('toto "titi"') - ['toto', 'titi'] - >>> shell_split('toto ""') - ['toto', ''] - >>> shell_split('to"to titi "a" b') - ['to"to', 'titi', 'a', 'b'] - >>> shell_split('"toto titi" toto ""') - ['toto titi', 'toto', ''] - >>> shell_split('toto "titi') - ['toto', 'titi'] """ sh = shlex.shlex(st) ret = [] @@ -358,18 +326,8 @@ def find_argument(pos, text, quoted=True): def find_argument_quoted(pos, text): """ - >>> find_argument_quoted(4, 'toto titi tata') - 3 - >>> find_argument_quoted(4, '"toto titi" tata') - 0 - >>> find_argument_quoted(8, '"toto" "titi tata"') - 1 - >>> find_argument_quoted(8, '"toto" "titi tata') - 1 - >>> find_argument_quoted(3, '"toto" "titi tata') - 0 - >>> find_argument_quoted(18, '"toto" "titi tata" ') - 2 + Get the number of the argument at position pos in + a string with possibly quoted text. """ sh = shlex.shlex(text) count = -1 @@ -384,16 +342,8 @@ def find_argument_quoted(pos, text): def find_argument_unquoted(pos, text): """ - >>> find_argument_unquoted(2, 'toto titi tata') - 0 - >>> find_argument_unquoted(3, 'toto titi tata') - 0 - >>> find_argument_unquoted(6, 'toto titi tata') - 1 - >>> find_argument_unquoted(4, 'toto titi tata') - 3 - >>> find_argument_unquoted(25, 'toto titi tata') - 3 + Get the number of the argument at position pos in + a string without interpreting quotes. """ ret = text.split() search = 0 @@ -531,7 +481,3 @@ def safeJID(*args, **kwargs): except InvalidJID: return JID('') - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/src/config.py b/src/config.py index f40f8742..1f0771ca 100644 --- a/src/config.py +++ b/src/config.py @@ -22,16 +22,129 @@ from os import environ, makedirs, path, remove from shutil import copy2 from args import parse_args +DEFAULT_CONFIG = { + 'Poezio': { + 'ack_message_receipts': True, + 'add_space_after_completion': True, + 'after_completion': ',', + 'alternative_nickname': '', + 'auto_reconnect': False, + 'autorejoin_delay': '5', + 'autorejoin': False, + 'beep_on': 'highlight private invite', + 'ca_cert_path': '', + 'certificate': '', + 'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL', + 'connection_check_interval': 60, + 'connection_timeout_delay': 10, + 'create_gaps': False, + 'custom_host': '', + 'custom_port': '', + 'default_nick': '', + 'display_activity_notifications': False, + 'display_gaming_notifications': False, + 'display_mood_notifications': False, + 'display_tune_notifications': False, + 'display_user_color_in_join_part': True, + 'enable_carbons': False, + 'enable_user_activity': True, + 'enable_user_gaming': True, + 'enable_user_mood': True, + 'enable_user_nick': True, + 'enable_user_tune': True, + 'enable_vertical_tab_list': False, + 'enable_xhtml_im': True, + 'exec_remote': False, + 'extract_inline_images': True, + 'filter_info_messages': '', + 'force_encryption': True, + 'group_corrections': True, + 'hide_exit_join': -1, + 'hide_status_change': 120, + 'hide_user_list': False, + 'highlight_on': '', + 'ignore_certificate': False, + 'ignore_private': False, + 'information_buffer_popup_on': 'error roster warning help info', + 'jid': '', + 'lang': 'en', + 'lazy_resize': True, + 'load_log': 10, + 'log_dir': '', + 'logfile': 'logs', + 'log_errors': True, + 'max_lines_in_memory': 2048, + 'max_messages_in_memory': 2048, + 'max_nick_length': 25, + 'muc_history_length': 50, + 'notify_messages': True, + '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, + 'resource': '', + 'rooms': '', + 'roster_group_sort': 'name', + 'roster_show_offline': False, + 'roster_sort': 'jid:show', + 'save_status': True, + '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, + 'show_muc_jid': True, + 'show_roster_jids': True, + 'show_roster_subscriptions': '', + 'show_s2s_errors': True, + 'show_tab_names': False, + 'show_tab_numbers': True, + 'show_timestamps': True, + 'show_useless_separator': False, + 'status': '', + 'status_message': '', + 'theme': 'default', + 'themes_dir': '', + 'tmp_image_dir': '', + 'use_bookmarks_method': '', + 'use_log': False, + '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 + } +} + class Config(RawConfigParser): """ load/save the config to a file """ - def __init__(self, file_name): + def __init__(self, file_name, default=None): RawConfigParser.__init__(self, None) # make the options case sensitive self.optionxform = str self.file_name = file_name self.read_file() + self.default = default def read_file(self): try: @@ -43,13 +156,19 @@ class Config(RawConfigParser): if not self.has_section(section): self.add_section(section) - def get(self, option, default, section=DEFSECTION): + def get(self, option, default=None, section=DEFSECTION): """ get a value from the config but return a default value if it is not found The type of default defines the type returned """ + if default is None: + if self.default: + default = self.default.get(section, {}).get(option) + else: + default = '' + try: if type(default) == int: res = self.getint(option, section) @@ -61,18 +180,21 @@ class Config(RawConfigParser): res = self.getstr(option, section) except (NoOptionError, NoSectionError, ValueError, AttributeError): return default + if res is None: return default return res - def get_by_tabname(self, option, default, tabname, fallback=True, - fallback_server=True): + def get_by_tabname(self, option, tabname, + fallback=True, fallback_server=True, default=''): """ 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. """ + if self.default and (not default) and fallback: + default = self.default.get(DEFSECTION, {}).get(option, '') if tabname in self.sections(): if option in self.options(tabname): # We go the tab-specific option @@ -360,7 +482,6 @@ def file_ok(filepath): def check_create_config_dir(): """ create the configuration directory if it doesn't exist - and copy the default config in it """ CONFIG_HOME = environ.get("XDG_CONFIG_HOME") if not CONFIG_HOME: @@ -373,6 +494,23 @@ def check_create_config_dir(): pass return CONFIG_PATH +def check_create_cache_dir(): + """ + create the cache directory if it doesn't exist + also create the subdirectories + """ + global CACHE_DIR + CACHE_HOME = environ.get("XDG_CACHE_HOME") + if not CACHE_HOME: + CACHE_HOME = path.join(environ.get('HOME'), '.cache') + CACHE_DIR = path.join(CACHE_HOME, 'poezio') + + try: + makedirs(CACHE_DIR) + makedirs(path.join(CACHE_DIR, 'images')) + except OSError: + pass + def run_cmdline_args(CONFIG_PATH): "Parse the command line arguments" global options @@ -394,7 +532,7 @@ def create_global_config(): "Create the global config object, or crash" try: global config - config = Config(options.filename) + config = Config(options.filename, DEFAULT_CONFIG) except: import traceback sys.stderr.write('Poezio was unable to read or' @@ -405,7 +543,7 @@ def create_global_config(): def check_create_log_dir(): "Create the poezio logging directory if it doesn’t exist" global LOG_DIR - LOG_DIR = config.get('log_dir', '') + LOG_DIR = config.get('log_dir') if not LOG_DIR: @@ -425,7 +563,7 @@ def check_create_log_dir(): def setup_logging(): "Change the logging config according to the cmdline options and config" - if config.get('log_errors', True): + if config.get('log_errors'): LOGGING_CONFIG['root']['handlers'].append('error') LOGGING_CONFIG['handlers']['error'] = { 'level': 'ERROR', @@ -494,3 +632,6 @@ safeJID = None # the global log dir LOG_DIR = '' + +# the global cache dir +CACHE_DIR = '' diff --git a/src/connection.py b/src/connection.py index 214194f9..1bbe632d 100644 --- a/src/connection.py +++ b/src/connection.py @@ -29,28 +29,28 @@ class Connection(slixmpp.ClientXMPP): """ __init = False def __init__(self): - resource = config.get('resource', '') - if config.get('jid', ''): + resource = config.get('resource') + if config.get('jid'): # Field used to know if we are anonymous or not. # many features will be handled differently # depending on this setting self.anon = False - jid = '%s' % config.get('jid', '') + jid = '%s' % config.get('jid') if resource: jid = '%s/%s'% (jid, resource) - password = config.get('password', '') or getpass.getpass() + password = config.get('password') or getpass.getpass() else: # anonymous auth self.anon = True - jid = config.get('server', 'anon.jeproteste.info') + jid = config.get('server') if resource: jid = '%s/%s' % (jid, resource) password = None jid = safeJID(jid) # TODO: use the system language slixmpp.ClientXMPP.__init__(self, jid, password, - lang=config.get('lang', 'en')) + lang=config.get('lang')) - force_encryption = config.get('force_encryption', True) + force_encryption = config.get('force_encryption') if force_encryption: self['feature_mechanisms'].unencrypted_plain = False self['feature_mechanisms'].unencrypted_digest = False @@ -58,6 +58,7 @@ class Connection(slixmpp.ClientXMPP): self['feature_mechanisms'].unencrypted_scram = False self.core = None + self.auto_reconnect = config.get('auto_reconnect') self.reconnect_max_attempts = 0 self.auto_authorize = None # prosody defaults, lowest is AES128-SHA, it should be a minimum @@ -65,12 +66,12 @@ class Connection(slixmpp.ClientXMPP): self.ciphers = config.get('ciphers', 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK' ':!SRP:!3DES:!aNULL') - self.ca_certs = config.get('ca_cert_path', '') or None - interval = config.get('whitespace_interval', '300') - if interval.isdecimal() and int(interval) > 0: + self.ca_certs = config.get('ca_cert_path') or None + interval = config.get('whitespace_interval') + if int(interval) > 0: self.whitespace_keepalive_interval = int(interval) else: - self.whitespace_keepalive_interval = 300 + self.whitespace_keepalive = False self.register_plugin('xep_0004') self.register_plugin('xep_0012') self.register_plugin('xep_0030') @@ -89,33 +90,31 @@ class Connection(slixmpp.ClientXMPP): # without a body XEP_0184._filter_add_receipt_request = fixes._filter_add_receipt_request self.register_plugin('xep_0184') - self.plugin['xep_0184'].auto_ack = config.get('ack_message_receipts', - True) - self.plugin['xep_0184'].auto_request = config.get( - 'request_message_receipts', True) + self.plugin['xep_0184'].auto_ack = config.get('ack_message_receipts') + self.plugin['xep_0184'].auto_request = config.get('request_message_receipts') self.register_plugin('xep_0191') self.register_plugin('xep_0199') - if config.get('enable_user_tune', True): + if config.get('enable_user_tune'): self.register_plugin('xep_0118') - if config.get('enable_user_nick', True): + if config.get('enable_user_nick'): self.register_plugin('xep_0172') - if config.get('enable_user_mood', True): + if config.get('enable_user_mood'): self.register_plugin('xep_0107') - if config.get('enable_user_activity', True): + if config.get('enable_user_activity'): self.register_plugin('xep_0108') - if config.get('enable_user_gaming', True): + if config.get('enable_user_gaming'): self.register_plugin('xep_0196') - if config.get('send_poezio_info', True): + if config.get('send_poezio_info'): info = {'name':'poezio', 'version': options.version} - if config.get('send_os_info', True): + if config.get('send_os_info'): info['os'] = common.get_os_info() self.plugin['xep_0030'].set_identities( identities=set([('client', 'pc', None, 'Poezio')])) @@ -124,7 +123,7 @@ class Connection(slixmpp.ClientXMPP): self.plugin['xep_0030'].set_identities( identities=set([('client', 'pc', None, '')])) self.register_plugin('xep_0092', pconfig=info) - if config.get('send_time', True): + if config.get('send_time'): self.register_plugin('xep_0202') self.register_plugin('xep_0224') self.register_plugin('xep_0249') @@ -143,8 +142,8 @@ class Connection(slixmpp.ClientXMPP): # Happens when we change the value with /set while we are not # connected. Do nothing in that case return - ping_interval = config.get('connection_check_interval', 60) - timeout_delay = config.get('connection_timeout_delay', 10) + ping_interval = config.get('connection_check_interval') + timeout_delay = config.get('connection_timeout_delay') if timeout_delay <= 0: # We help the stupid user (with a delay of 0, poezio will try to # reconnect immediately because the timeout is immediately @@ -161,7 +160,7 @@ class Connection(slixmpp.ClientXMPP): """ Connect and process events. """ - custom_host = config.get('custom_host', '') + custom_host = config.get('custom_host') custom_port = config.get('custom_port', 5222) if custom_port == -1: custom_port = 5222 diff --git a/src/core/commands.py b/src/core/commands.py index c27263e2..4a8f7f19 100644 --- a/src/core/commands.py +++ b/src/core/commands.py @@ -7,6 +7,7 @@ import logging log = logging.getLogger(__name__) import functools +import os import sys from datetime import datetime from gettext import gettext as _ @@ -24,6 +25,7 @@ import tabs from common import safeJID from config import config, options as config_opts import multiuserchat as muc +from plugin import PluginConfig from roster import roster from theming import dump_tuple, get_theme @@ -369,16 +371,13 @@ def command_join(self, arg, histo_length=None): room = room[1:] current_status = self.get_status() if not histo_length: - histo_length = config.get('muc_history_length', 20) + histo_length = config.get('muc_history_length') if histo_length == -1: histo_length = None if histo_length is not None: histo_length = str(histo_length) if password is None: # try to use a saved password - password = config.get_by_tabname('password', - None, - room, - fallback=False) + password = config.get_by_tabname('password', room, fallback=False) if tab and not tab.joined: if tab.last_connection: if tab.last_connection is not None: @@ -476,7 +475,7 @@ def command_bookmark(self, arg=''): /bookmark [room][/nick] [autojoin] [password] """ - if not config.get('use_remote_bookmarks', True): + if not config.get('use_remote_bookmarks'): self.command_bookmark_local(arg) return args = common.shell_split(arg) @@ -537,7 +536,7 @@ def command_bookmark(self, arg=''): if not bm: bm = bookmark.Bookmark(roomname) bookmark.bookmarks.append(bm) - bm.method = config.get('use_bookmarks_method', 'pep') + bm.method = config.get('use_bookmarks_method') if nick: bm.nick = nick if password: @@ -592,17 +591,39 @@ def command_remove_bookmark(self, arg=''): def command_set(self, arg): """ - /set [module|][section] <option> <value> + /set [module|][section] <option> [value] """ args = common.shell_split(arg) - if len(args) != 2 and len(args) != 3: - self.command_help('set') - return - if len(args) == 2: + if len(args) == 1: option = args[0] - value = args[1] - info = config.set_and_save(option, value) - self.trigger_configuration_change(option, value) + value = config.get(option) + info = ('%s=%s' % (option, value), 'Info') + elif len(args) == 2: + if '|' in args[0]: + plugin_name, section = args[0].split('|')[:2] + if not section: + section = plugin_name + option = args[1] + if not plugin_name in self.plugin_manager.plugins: + file_name = self.plugin_manager.plugins_conf_dir + file_name = os.path.join(file_name, plugin_name + '.cfg') + plugin_config = PluginConfig(file_name, plugin_name) + else: + plugin_config = self.plugin_manager.plugins[plugin_name].config + value = plugin_config.get(option, default='', section=section) + info = ('%s=%s' % (option, value), 'Info') + else: + possible_section = args[0] + if config.has_section(possible_section): + section = possible_section + option = args[1] + value = config.get(option, section=section) + info = ('%s=%s' % (option, value), 'Info') + else: + option = args[0] + value = args[1] + info = config.set_and_save(option, value) + self.trigger_configuration_change(option, value) elif len(args) == 3: if '|' in args[0]: plugin_name, section = args[0].split('|')[:2] @@ -611,15 +632,21 @@ def command_set(self, arg): option = args[1] value = args[2] if not plugin_name in self.plugin_manager.plugins: - return - plugin = self.plugin_manager.plugins[plugin_name] - info = plugin.config.set_and_save(option, value, section) + file_name = self.plugin_manager.plugins_conf_dir + file_name = os.path.join(file_name, plugin_name + '.cfg') + plugin_config = PluginConfig(file_name, plugin_name) + else: + plugin_config = self.plugin_manager.plugins[plugin_name].config + info = plugin_config.set_and_save(option, value, section) else: section = args[0] option = args[1] value = args[2] info = config.set_and_save(option, value, section) self.trigger_configuration_change(option, value) + else: + self.command_help('set') + return self.call_for_resize() self.information(*info) @@ -813,11 +840,11 @@ def command_quit(self, arg=''): msg = arg else: msg = None - if config.get('enable_user_mood', True): + if config.get('enable_user_mood'): self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity', True): + if config.get('enable_user_activity'): self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming', True): + if config.get('enable_user_gaming'): self.xmpp.plugin['xep_0196'].stop() self.save_config() self.plugin_manager.disable_plugins() diff --git a/src/core/completions.py b/src/core/completions.py index 7acddef9..7d95321b 100644 --- a/src/core/completions.py +++ b/src/core/completions.py @@ -46,7 +46,7 @@ def completion_presence(self, the_input): def completion_theme(self, the_input): """ Completion for /theme""" - themes_dir = config.get('themes_dir', '') + themes_dir = config.get('themes_dir') themes_dir = themes_dir or\ os.path.join(os.environ.get('XDG_DATA_HOME') or\ os.path.join(os.environ.get('HOME'), '.local', 'share'), @@ -175,7 +175,7 @@ def completion_bookmark(self, the_input): tab = self.get_tab_by_name(jid.bare, tabs.MucTab) nicks = [tab.own_nick] if tab else [] default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick', '') + nick = config.get('default_nick') if not nick: if not default in nicks: nicks.append(default) @@ -309,7 +309,7 @@ def completion_set(self, the_input): if '|' in args[1]: plugin_name, section = args[1].split('|')[:2] if not plugin_name in self.plugin_manager.plugins: - return the_input.auto_completion([''], n, quotify=True) + return the_input.new_completion([''], n, quotify=True) plugin = self.plugin_manager.plugins[plugin_name] end_list = plugin.config.options(section or plugin_name) elif not config.has_option('Poezio', args[1]): @@ -319,19 +319,19 @@ def completion_set(self, the_input): else: end_list = [] else: - end_list = [config.get(args[1], ''), ''] + end_list = [str(config.get(args[1], '')), ''] elif n == 3: if '|' in args[1]: plugin_name, section = args[1].split('|')[:2] if not plugin_name in self.plugin_manager.plugins: - return the_input.auto_completion([''], n, quotify=True) + return the_input.new_completion([''], n, quotify=True) plugin = self.plugin_manager.plugins[plugin_name] - end_list = [plugin.config.get(args[2], '', section or plugin_name), ''] + end_list = [str(plugin.config.get(args[2], '', section or plugin_name)), ''] else: if not config.has_section(args[1]): end_list = [''] else: - end_list = [config.get(args[2], '', args[1]), ''] + end_list = [str(config.get(args[2], '', args[1])), ''] else: return return the_input.new_completion(end_list, n, quotify=True) @@ -356,7 +356,7 @@ def completion_bookmark_local(self, the_input): tab = self.get_tab_by_name(jid.bare, tabs.MucTab) nicks = [tab.own_nick] if tab else [] default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick', '') + nick = config.get('default_nick') if not nick: if not default in nicks: nicks.append(default) diff --git a/src/core/core.py b/src/core/core.py index eeb25c83..70136250 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -65,10 +65,10 @@ class Core(object): sys.excepthook = self.on_exception self.connection_time = time.time() self.stdscr = None - status = config.get('status', None) + status = config.get('status') status = possible_show.get(status, None) self.status = Status(show=status, - message=config.get('status_message', '')) + message=config.get('status_message')) self.running = True self.xmpp = singleton.Singleton(connection.Connection) self.xmpp.core = self @@ -83,7 +83,7 @@ class Core(object): # that are displayed in almost all tabs, in an # information window. self.information_buffer = TextBuffer() - self.information_win_size = config.get('info_win_height', 2, 'var') + self.information_win_size = config.get('info_win_height', section='var') self.information_win = windows.TextWin(300) self.information_buffer.add_window(self.information_win) self.left_tab_win = None @@ -97,7 +97,7 @@ class Core(object): self._current_tab_nb = 0 self.previous_tab_nb = 0 - own_nick = config.get('default_nick', '') + own_nick = config.get('default_nick') own_nick = own_nick or self.xmpp.boundjid.user own_nick = own_nick or os.environ.get('USER') own_nick = own_nick or 'poezio' @@ -127,7 +127,7 @@ class Core(object): self.register_initial_commands() # We are invisible - if not config.get('send_initial_presence', True): + if not config.get('send_initial_presence'): del self.commands['status'] del self.commands['show'] @@ -256,19 +256,19 @@ class Core(object): connection.MatchAll(None), self.incoming_stanza) self.xmpp.register_handler(self.all_stanzas) - if config.get('enable_user_tune', True): + if config.get('enable_user_tune'): self.xmpp.add_event_handler("user_tune_publish", self.on_tune_event) - if config.get('enable_user_nick', True): + if config.get('enable_user_nick'): self.xmpp.add_event_handler("user_nick_publish", self.on_nick_received) - if config.get('enable_user_mood', True): + if config.get('enable_user_mood'): self.xmpp.add_event_handler("user_mood_publish", self.on_mood_event) - if config.get('enable_user_activity', True): + if config.get('enable_user_activity'): self.xmpp.add_event_handler("user_activity_publish", self.on_activity_event) - if config.get('enable_user_gaming', True): + if config.get('enable_user_gaming'): self.xmpp.add_event_handler("user_gaming_publish", self.on_gaming_event) @@ -357,13 +357,14 @@ class Core(object): """ Called when the request_message_receipts option changes """ - self.xmpp.plugin['xep_0184'].auto_request = config.get(option, True) + self.xmpp.plugin['xep_0184'].auto_request = config.get(option, + default=True) def on_ack_receipts_config_change(self, option, value): """ Called when the ack_message_receipts option changes """ - self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, True) + self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, default=True) def on_plugins_dir_config_change(self, option, value): """ @@ -419,7 +420,7 @@ class Core(object): old_section = old_config.get(section, {}) for option in config.options(section): old_value = old_section.get(option) - new_value = config.get(option, "", section) + new_value = config.get(option, default="", section=section) if new_value != old_value: self.trigger_configuration_change(option, new_value) log.debug("Config reloaded.") @@ -441,11 +442,11 @@ class Core(object): } log.error("%s received. Exiting…", signals[sig]) - if config.get('enable_user_mood', True): + if config.get('enable_user_mood'): self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity', True): + if config.get('enable_user_activity'): self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming', True): + if config.get('enable_user_gaming'): self.xmpp.plugin['xep_0196'].stop() self.plugin_manager.disable_plugins() self.disconnect('%s received' % signals.get(sig)) @@ -455,7 +456,7 @@ class Core(object): """ Load the plugins on startup. """ - plugins = config.get('plugins_autoload', '') + plugins = config.get('plugins_autoload') if ':' in plugins: for plugin in plugins.split(':'): self.plugin_manager.load(plugin) @@ -704,9 +705,9 @@ class Core(object): work. If you try to do anything else, your |, [, <<, etc will be interpreted as normal command arguments, not shell special tokens. """ - if config.get('exec_remote', False): + if config.get('exec_remote'): # We just write the command in the fifo - fifo_path = config.get('remote_fifo_path', './') + fifo_path = config.get('remote_fifo_path') if not self.remote_fifo: try: self.remote_fifo = Fifo(os.path.join(fifo_path, @@ -802,7 +803,7 @@ class Core(object): or to use it when joining a new muc) """ self.status = Status(show=pres, message=msg) - if config.get('save_status', True): + if config.get('save_status'): ok = config.silent_set('status', pres if pres else '') msg = msg.replace('\n', '|') if msg else '' ok = ok and config.silent_set('status_message', msg) @@ -1043,7 +1044,7 @@ class Core(object): return False elif not self.tabs[old_pos]: return False - if config.get('create_gaps', False): + if config.get('create_gaps'): return self.insert_tab_gaps(old_pos, new_pos) return self.insert_tab_nogaps(old_pos, new_pos) @@ -1294,7 +1295,7 @@ class Core(object): if self.previous_tab_nb != nb: self.current_tab_nb = self.previous_tab_nb self.previous_tab_nb = 0 - if config.get('create_gaps', False): + if config.get('create_gaps'): if nb >= len(self.tabs) - 1: self.tabs.remove(tab) nb -= 1 @@ -1345,7 +1346,7 @@ class Core(object): """ Displays an informational message in the "Info" buffer """ - filter_messages = config.get('filter_info_messages', '').split(':') + filter_messages = config.get('filter_info_messages').split(':') for words in filter_messages: if words and words in msg: log.debug('Did not show the message:\n\t%s> %s', typ, msg) @@ -1355,12 +1356,11 @@ class Core(object): nb_lines = self.information_buffer.add_message(msg, nickname=typ, nick_color=color) - popup_on = config.get('information_buffer_popup_on', - 'error roster warning help info').split() + popup_on = config.get('information_buffer_popup_on').split() if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() elif typ != '' and typ.lower() in popup_on: - popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2 + popup_time = config.get('popup_time') + (nb_lines - 1) * 2 self.pop_information_win_up(nb_lines, popup_time) else: if self.information_win_size != 0: @@ -1553,7 +1553,7 @@ class Core(object): """ Enable/disable the left panel. """ - enabled = config.get('enable_vertical_tab_list', False) + enabled = config.get('enable_vertical_tab_list') if not config.silent_set('enable_vertical_tab_list', str(not enabled)): self.information(_('Unable to write in the config file'), 'Error') self.call_for_resize() @@ -1578,14 +1578,14 @@ class Core(object): Resize the GlobalInfoBar only once at each resize """ height, width = self.stdscr.getmaxyx() - if config.get('enable_vertical_tab_list', False): + if config.get('enable_vertical_tab_list'): if self.size.core_degrade_x: return try: height, _ = self.stdscr.getmaxyx() truncated_win = self.stdscr.subwin(height, - config.get('vertical_tab_list_size', 20), + config.get('vertical_tab_list_size'), 0, 0) except: log.error('Curses error on infobar resize', exc_info=True) @@ -1623,11 +1623,11 @@ class Core(object): # the screen that they can occupy, and we draw the tab list on the # remaining space, on the left height, width = self.stdscr.getmaxyx() - if (config.get('enable_vertical_tab_list', False) and + if (config.get('enable_vertical_tab_list') and not self.size.core_degrade_x): try: scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size', 20)) + config.get('vertical_tab_list_size')) except: log.error('Curses error on resize', exc_info=True) return @@ -1637,7 +1637,7 @@ class Core(object): self.resize_global_info_bar() self.resize_global_information_win() for tab in self.tabs: - if config.get('lazy_resize', True): + if config.get('lazy_resize'): tab.need_resize = True else: tab.resize() @@ -1865,7 +1865,7 @@ class Core(object): usage='<jid>', shortdesc=_('List available ad-hoc commands on the given jid')) - if config.get('enable_user_activity', True): + if config.get('enable_user_activity'): self.register_command('activity', self.command_activity, usage='[<general> [specific] [text]]', desc=_('Send your current activity to your contacts ' @@ -1873,7 +1873,7 @@ class Core(object): '"stop broadcasting an activity".'), shortdesc=_('Send your activity.'), completion=self.completion_activity) - if config.get('enable_user_mood', True): + if config.get('enable_user_mood'): self.register_command('mood', self.command_mood, usage='[<mood> [text]]', desc=_('Send your current mood to your contacts ' @@ -1881,7 +1881,7 @@ class Core(object): '"stop broadcasting a mood".'), shortdesc=_('Send your mood.'), completion=self.completion_mood) - if config.get('enable_user_gaming', True): + if config.get('enable_user_gaming'): self.register_command('gaming', self.command_gaming, usage='[<game name> [server address]]', desc=_('Send your current gaming activity to ' @@ -2025,7 +2025,7 @@ def replace_key_with_bound(key): Replace an inputted key with the one defined as its replacement in the config """ - bind = config.get(key, key, 'bindings') + bind = config.get(key, default=key, section='bindings') if not bind: bind = key return bind diff --git a/src/core/handlers.py b/src/core/handlers.py index 4e2fcfd3..648c3e4d 100644 --- a/src/core/handlers.py +++ b/src/core/handlers.py @@ -13,6 +13,7 @@ import sys import time from hashlib import sha1, sha512 from gettext import gettext as _ +from os import path from slixmpp import InvalidJID from slixmpp.stanza import Message @@ -27,7 +28,7 @@ import windows import xhtml import multiuserchat as muc from common import safeJID -from config import config +from config import config, CACHE_DIR from contact import Resource from logger import logger from roster import roster @@ -46,7 +47,7 @@ def on_session_start_features(self, _): features = iq['disco_info']['features'] rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab) rostertab.check_blocking(features) - if (config.get('enable_carbons', True) and + if (config.get('enable_carbons') and 'urn:xmpp:carbons:2' in features): self.xmpp.plugin['xep_0280'].enable() self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) @@ -120,7 +121,7 @@ def on_groupchat_invitation(self, message): if password: msg += ". The password is \"%s\"." % password self.information(msg, 'Info') - if 'invite' in config.get('beep_on', 'invite').split(): + if 'invite' in config.get('beep_on').split(): curses.beep() logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full) self.pending_invites[jid.bare] = inviter.full @@ -151,7 +152,7 @@ def on_groupchat_direct_invitation(self, message): msg += "\nreason: %s" % reason self.information(msg, 'Info') - if 'invite' in config.get('beep_on', 'invite').split(): + if 'invite' in config.get('beep_on').split(): curses.beep() self.pending_invites[room.bare] = inviter.full @@ -188,8 +189,12 @@ def on_normal_message(self, message): elif message['type'] == 'headline' and message['body']: return self.information('%s says: %s' % (message['from'], message['body']), 'Headline') - use_xhtml = config.get('enable_xhtml_im', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + use_xhtml = config.get('enable_xhtml_im') + tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') + extract_images = config.get('extract_inline_images') + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body: return @@ -203,7 +208,7 @@ def on_normal_message(self, message): if conv_jid.bare in roster: remote_nick = roster[conv_jid.bare].name # check for a received nick - if not remote_nick and config.get('enable_user_nick', True): + if not remote_nick and config.get('enable_user_nick'): if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None: remote_nick = message['nick']['nick'] if not remote_nick: @@ -234,13 +239,15 @@ def on_normal_message(self, message): self.events.trigger('conversation_msg', message, conversation) if not message['body']: return - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) delayed, date = common.find_delayed_tag(message) def try_modify(): replaced_id = message['replace']['id'] - if replaced_id and (config.get_by_tabname('group_corrections', - True, conv_jid.bare)): + if replaced_id and config.get_by_tabname('group_corrections', + conv_jid.bare): try: conversation.modify_message(body, replaced_id, message['id'], jid=jid, nickname=remote_nick) @@ -263,8 +270,8 @@ def on_normal_message(self, message): conversation.remote_wants_chatstates = True else: conversation.remote_wants_chatstates = False - if 'private' in config.get('beep_on', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, conv_jid.bare, False): + if 'private' in config.get('beep_on').split(): + if not config.get_by_tabname('disable_beep', conv_jid.bare): curses.beep() if self.current_tab() is not conversation: conversation.state = 'private' @@ -314,7 +321,7 @@ def on_gaming_event(self, message): if contact.gaming: logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming))) - if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', False, contact.bare_jid): + if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid): if contact.gaming: self.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming') else: @@ -347,7 +354,7 @@ def on_mood_event(self, message): if contact.mood: logger.log_roster_change(contact.bare_jid, 'has now the mood: %s' % contact.mood) - if old_mood != contact.mood and config.get_by_tabname('display_mood_notifications', False, contact.bare_jid): + if old_mood != contact.mood and config.get_by_tabname('display_mood_notifications', contact.bare_jid): if contact.mood: self.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood') else: @@ -386,7 +393,7 @@ def on_activity_event(self, message): if contact.activity: logger.log_roster_change(contact.bare_jid, 'has now the activity %s' % contact.activity) - if old_activity != contact.activity and config.get_by_tabname('display_activity_notifications', False, contact.bare_jid): + if old_activity != contact.activity and config.get_by_tabname('display_activity_notifications', contact.bare_jid): if contact.activity: self.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity') else: @@ -420,7 +427,7 @@ def on_tune_event(self, message): if contact.tune: logger.log_roster_change(message['from'].bare, 'is now listening to %s' % common.format_tune_string(contact.tune)) - if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', False, contact.bare_jid): + if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid): if contact.tune: self.information( 'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune), @@ -451,8 +458,12 @@ def on_groupchat_message(self, message): return self.events.trigger('muc_msg', message, tab) - use_xhtml = config.get('enable_xhtml_im', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + use_xhtml = config.get('enable_xhtml_im') + tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') + extract_images = config.get('extract_inline_images') + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body: return @@ -460,8 +471,8 @@ def on_groupchat_message(self, message): delayed, date = common.find_delayed_tag(message) replaced_id = message['replace']['id'] replaced = False - if replaced_id is not '' and (config.get_by_tabname( - 'group_corrections', True, message['from'].bare)): + if replaced_id is not '' and config.get_by_tabname('group_corrections', + message['from'].bare): try: if tab.modify_message(body, replaced_id, message['id'], time=date, nickname=nick_from, user=user): @@ -487,8 +498,8 @@ def on_groupchat_message(self, message): current.input.refresh() self.doupdate() - if 'message' in config.get('beep_on', 'highlight private').split(): - if (not config.get_by_tabname('disable_beep', False, room_from, False) + if 'message' in config.get('beep_on').split(): + if (not config.get_by_tabname('disable_beep', room_from) and self.own_nick != message['from'].resource): curses.beep() @@ -508,28 +519,34 @@ def on_groupchat_private_message(self, message): return self.on_groupchat_message(message) room_from = jid.bare - use_xhtml = config.get('enable_xhtml_im', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + use_xhtml = config.get('enable_xhtml_im') + tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') + extract_images = config.get('extract_inline_images') + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation - ignore = config.get_by_tabname('ignore_private', False, room_from) + ignore = config.get_by_tabname('ignore_private', room_from) if not tab: # It's the first message we receive: create the tab if body and not ignore: tab = self.open_private_window(room_from, nick_from, False) if ignore: self.events.trigger('ignored_private', message, tab) - msg = config.get_by_tabname('private_auto_response', None, room_from) + msg = config.get_by_tabname('private_auto_response', room_from) if msg and body: self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') return self.events.trigger('private_msg', message, tab) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, + tmp_dir=tmp_dir, + extract_images=extract_images) if not body or not tab: return replaced_id = message['replace']['id'] replaced = False user = tab.parent_muc.get_user_by_name(nick_from) - if replaced_id is not '' and (config.get_by_tabname( - 'group_corrections', True, room_from)): + if replaced_id is not '' and config.get_by_tabname('group_corrections', + room_from): try: tab.modify_message(body, replaced_id, message['id'], user=user, jid=message['from'], nickname=nick_from) @@ -548,8 +565,8 @@ def on_groupchat_private_message(self, message): tab.remote_wants_chatstates = True else: tab.remote_wants_chatstates = False - if 'private' in config.get('beep_on', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, jid.full, False): + if 'private' in config.get('beep_on').split(): + if not config.get_by_tabname('disable_beep', jid.full): curses.beep() if tab is self.current_tab(): self.refresh_window() @@ -883,7 +900,7 @@ def on_session_start(self, event): # request the roster self.xmpp.get_roster() # send initial presence - if config.get('send_initial_presence', True): + if config.get('send_initial_presence'): pres = self.xmpp.make_presence() pres['show'] = self.status.show pres['status'] = self.status.message @@ -893,13 +910,13 @@ def on_session_start(self, event): def _join_initial_rooms(bookmarks): """Join all rooms given in the iterator `bookmarks`""" for bm in bookmarks: - if bm.autojoin or config.get('open_all_bookmarks', False): + if bm.autojoin or config.get('open_all_bookmarks'): tab = self.get_tab_by_name(bm.jid, tabs.MucTab) nick = bm.nick if bm.nick else self.own_nick if not tab: self.open_new_room(bm.jid, nick, False) self.initial_joins.append(bm.jid) - histo_length = config.get('muc_history_length', 20) + histo_length = config.get('muc_history_length') if histo_length == -1: histo_length = None if histo_length is not None: @@ -915,13 +932,13 @@ def on_session_start(self, event): def _join_remote_only(): remote_bookmarks = (bm for bm in bookmark.bookmarks if (bm.method in ("pep", "privatexml"))) _join_initial_rooms(remote_bookmarks) - if not self.xmpp.anon and config.get('use_remote_bookmarks', True): + if not self.xmpp.anon and config.get('use_remote_bookmarks'): bookmark.get_remote(self.xmpp, _join_remote_only) # join all the available bookmarks. As of yet, this is just the local # ones _join_initial_rooms(bookmark.bookmarks) - if config.get('enable_user_nick', True): + if config.get('enable_user_nick'): self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback) self.xmpp.plugin['xep_0115'].update_caps() # Start the ping's plugin regular event @@ -994,18 +1011,21 @@ def on_groupchat_subject(self, message): room_from = message.get_mucroom() tab = self.get_tab_by_name(room_from, tabs.MucTab) subject = message['subject'] - if not subject or not tab: + if subject is None or not tab: return - if nick_from: - tab.add_message(_("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject}, - time=None, - typ=2) - else: - tab.add_message(_("\x19%(info_col)s}The subject is: %(subject)s") % - {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - time=None, - typ=2) + if subject != tab.topic: + # Do not display the message if the subject did not change or if we + # receive an empty topic when joining the room. + if nick_from: + tab.add_message(_("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % + {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject}, + time=None, + typ=2) + else: + tab.add_message(_("\x19%(info_col)s}The subject is: %(subject)s") % + {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, + time=None, + typ=2) tab.topic = subject tab.topic_from = nick_from if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): @@ -1064,8 +1084,8 @@ def room_error(self, error, room_name): msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') tab.add_message(msg, typ=2) if code == '409': - if config.get('alternative_nickname', '') != '': - self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', ''))) + if config.get('alternative_nickname') != '': + self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname'))) else: if not tab.joined: tab.add_message(_('You can join the room with an other nick, by typing "/join /other_nick"'), typ=2) @@ -1095,9 +1115,9 @@ def validate_ssl(self, pem): """ Check the server certificate using the slixmpp ssl_cert event """ - if config.get('ignore_certificate', False): + if config.get('ignore_certificate'): return - cert = config.get('certificate', '') + cert = config.get('certificate') # update the cert representation when it uses the old one if cert and not ':' in cert: cert = ':'.join(i + j for i, j in zip(cert[::2], cert[1::2])).upper() @@ -1173,7 +1193,7 @@ def _composing_tab_state(tab, state): else: return # should not happen - show = config.get('show_composing_tabs', 'direct') + show = config.get('show_composing_tabs') show = show in values if tab.state != 'composing' and state == 'composing': diff --git a/src/logger.py b/src/logger.py index 7ed0692f..85c7a746 100644 --- a/src/logger.py +++ b/src/logger.py @@ -50,7 +50,7 @@ class Logger(object): and also log the conversations to logfiles """ def __init__(self): - self.logfile = config.get('logfile', 'logs') + self.logfile = config.get('logfile') self.roster_logfile = None # a dict of 'groupchatname': file-object (opened) self.fds = dict() @@ -78,7 +78,7 @@ class Logger(object): Check that the directory where we want to log the messages exists. if not, create it """ - if not config.get_by_tabname('use_log', True, room): + if not config.get_by_tabname('use_log', room): return try: makedirs(log_dir) @@ -106,10 +106,10 @@ class Logger(object): this function is a little bit more complicated than “read the last nb lines”. """ - if config.get_by_tabname('load_log', 10, jid) <= 0: + if config.get_by_tabname('load_log', jid) <= 0: return - if not config.get_by_tabname('use_log', True, jid): + if not config.get_by_tabname('use_log', jid): return if nb <= 0: @@ -197,7 +197,7 @@ class Logger(object): return True jid = str(jid).replace('/', '\\') - if not config.get_by_tabname('use_log', False, jid): + if not config.get_by_tabname('use_log', jid): return True if jid in self.fds.keys(): fd = self.fds[jid] @@ -245,7 +245,7 @@ class Logger(object): """ Log a roster change """ - if not config.get_by_tabname('use_log', False, jid): + if not config.get_by_tabname('use_log', jid): return True self.check_and_create_log_dir('', open_fd=False) if not self.roster_logfile: diff --git a/src/plugin.py b/src/plugin.py index 48ff3bd3..eb2a89e3 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -20,9 +20,8 @@ class PluginConfig(config.Config): and behave like the core Config object. """ def __init__(self, filename, module_name): - self.file_name = filename + config.Config.__init__(self, filename) self.module_name = module_name - RawConfigParser.__init__(self, None) self.read() def get(self, option, default, section=None): diff --git a/src/plugin_manager.py b/src/plugin_manager.py index e449442d..d4cc7384 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -325,7 +325,7 @@ class PluginManager(object): """ Create the plugins_conf_dir """ - plugins_conf_dir = config.get('plugins_conf_dir', '') + plugins_conf_dir = config.get('plugins_conf_dir') if not plugins_conf_dir: config_home = os.environ.get('XDG_CONFIG_HOME') if not config_home: @@ -352,7 +352,7 @@ class PluginManager(object): """ Set the plugins_dir on start """ - plugins_dir = config.get('plugins_dir', '') + plugins_dir = config.get('plugins_dir') plugins_dir = plugins_dir or\ os.path.join(os.environ.get('XDG_DATA_HOME') or\ os.path.join(os.environ.get('HOME'), diff --git a/src/poezio.py b/src/poezio.py index 6a4a0b77..9a26e135 100644 --- a/src/poezio.py +++ b/src/poezio.py @@ -30,6 +30,7 @@ def main(): config.run_cmdline_args(config_path) config.create_global_config() config.check_create_log_dir() + config.check_create_cache_dir() config.setup_logging() config.post_logging_setup() diff --git a/src/roster.py b/src/roster.py index ef556021..d2b99cef 100644 --- a/src/roster.py +++ b/src/roster.py @@ -36,10 +36,8 @@ class Roster(object): self.contact_filter = None # A tuple(function, *args) # function to filter contacts, # on search, for example - self.folded_groups = set(config.get( - 'folded_roster_groups', - '', - section='var').split(':')) + self.folded_groups = set(config.get('folded_roster_groups', + section='var').split(':')) self.groups = {} self.contacts = {} diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py index 9212278d..645a297f 100644 --- a/src/tabs/basetabs.py +++ b/src/tabs/basetabs.py @@ -138,7 +138,7 @@ class Tab(object): Returns 1 or 0, depending on if we are using the vertical tab list or not. """ - if config.get('enable_vertical_tab_list', False): + if config.get('enable_vertical_tab_list'): return 0 return 1 @@ -296,7 +296,7 @@ class Tab(object): return False def refresh_tab_win(self): - if config.get('enable_vertical_tab_list', False): + if config.get('enable_vertical_tab_list'): if self.left_tab_win and not self.size.core_degrade_x: self.left_tab_win.refresh() elif not self.size.core_degrade_y: @@ -471,7 +471,7 @@ class ChatTab(Tab): self.update_keys() # Get the logs - log_nb = config.get('load_log', 10) + log_nb = config.get('load_log') logs = self.load_logs(log_nb) if logs: @@ -532,7 +532,7 @@ class ChatTab(Tab): for word in txt.split(): if len(word) >= 4 and word not in words: words.append(word) - words.extend([word for word in config.get('words', '').split(':') if word]) + words.extend([word for word in config.get('words').split(':') if word]) self.input.auto_completion(words, ' ', quotify=False) def on_enter(self): @@ -587,8 +587,8 @@ class ChatTab(Tab): if not self.is_muc or self.joined: if state in ('active', 'inactive', 'gone') and self.inactive and not always_send: return - if config.get_by_tabname('send_chat_states', True, self.general_jid, True) and \ - self.remote_wants_chatstates is not False: + if (config.get_by_tabname('send_chat_states', self.general_jid) + and self.remote_wants_chatstates is not False): msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = self.message_type msg['chat_state'] = state @@ -602,7 +602,8 @@ class ChatTab(Tab): on the the current status of the input """ name = self.general_jid - if config.get_by_tabname('send_chat_states', True, name, True) and self.remote_wants_chatstates: + if (config.get_by_tabname('send_chat_states', name) + and self.remote_wants_chatstates): needed = 'inactive' if self.inactive else 'active' self.cancel_paused_delay() if not empty_after: @@ -617,7 +618,7 @@ class ChatTab(Tab): we create a timed event that will put us to paused in a few seconds """ - if not config.get_by_tabname('send_chat_states', True, self.general_jid, True): + if not config.get_by_tabname('send_chat_states', self.general_jid): return # First, cancel the delay if it already exists, before rescheduling # it at a new date @@ -803,32 +804,17 @@ class OneToOneTab(ChatTab): else: self.__initial_disco = True - empty = not any((correct, attention, receipts)) - - features = [] - if correct or empty: - features.append(_('message correction (/correct)')) - if attention or empty: - features.append(_('attention requests (/attention)')) - if (receipts or empty) \ - and config.get('request_message_receipts', True): - features.append(_('message delivery receipts')) - if len(features) > 1: - tail = features.pop() - else: - tail = None - features_str = ', '.join(features) - if tail and empty: - features_str += _(', or %s') % tail - elif tail: - features_str += _(' and %s') % tail - - if empty: - msg = _('\x19%s}This contact does not support %s.') - else: - msg = _('\x19%s}This contact supports %s.') + ok = get_theme().CHAR_OK + nope = get_theme().CHAR_EMPTY + + correct = ok if correct else nope + attention = ok if attention else nope + receipts = ok if receipts else nope + + msg = _('\x19%s}Contact supports: correction [%s], ' + 'attention [%s], receipts [%s].') color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - msg = msg % (color, features_str) + msg = msg % (color, correct, attention, receipts) self.add_message(msg, typ=0) self.core.refresh_window() diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py index cc9e6b2e..3d5769f7 100644 --- a/src/tabs/conversationtab.py +++ b/src/tabs/conversationtab.py @@ -108,7 +108,7 @@ class ConversationTab(OneToOneTab): replaced = False if correct or msg['replace']['id']: msg['replace']['id'] = self.last_sent_message['id'] - if config.get_by_tabname('group_corrections', True, self.name): + if config.get_by_tabname('group_corrections', self.name): try: self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], jid=self.core.xmpp.boundjid, nickname=self.core.own_nick) @@ -121,7 +121,8 @@ class ConversationTab(OneToOneTab): msg.enable('html') msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) - if config.get_by_tabname('send_chat_states', True, self.general_jid, True) and self.remote_wants_chatstates is not False: + if (config.get_by_tabname('send_chat_states', self.general_jid) and + self.remote_wants_chatstates is not False): needed = 'inactive' if self.inactive else 'active' msg['chat_state'] = needed if attention and self.remote_supports_attention: @@ -316,7 +317,9 @@ class ConversationTab(OneToOneTab): self.state = 'normal' self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) - if config.get_by_tabname('send_chat_states', True, self.general_jid, True) and (not self.input.get_text() or not self.input.get_text().startswith('//')): + if (config.get_by_tabname('send_chat_states', self.general_jid) + and (not self.input.get_text() + or not self.input.get_text().startswith('//'))): if resource: self.send_chat_state('inactive') self.check_scrolled() @@ -334,7 +337,9 @@ class ConversationTab(OneToOneTab): self.state = 'current' curses.curs_set(1) - if config.get_by_tabname('send_chat_states', True, self.general_jid, True) and (not self.input.get_text() or not self.input.get_text().startswith('//')): + if (config.get_by_tabname('send_chat_states', self.general_jid) + and (not self.input.get_text() + or not self.input.get_text().startswith('//'))): if resource: self.send_chat_state('active') @@ -349,7 +354,7 @@ class ConversationTab(OneToOneTab): def on_close(self): Tab.on_close(self) - if config.get_by_tabname('send_chat_states', True, self.general_jid, True): + if config.get_by_tabname('send_chat_states', self.general_jid): self.send_chat_state('gone') def matching_names(self): diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py index f526ec80..547830cb 100644 --- a/src/tabs/muctab.py +++ b/src/tabs/muctab.py @@ -251,7 +251,7 @@ class MucTab(ChatTab): def completion_nick(self, the_input): """Completion for /nick""" nicks = [os.environ.get('USER'), - config.get('default_nick', ''), + config.get('default_nick'), self.core.get_bookmark_nickname(self.name)] nicks = [i for i in nicks if i] return the_input.auto_completion(nicks, '', quotify=False) @@ -471,8 +471,8 @@ class MucTab(ChatTab): char_quit = get_theme().CHAR_QUIT spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR) - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(get_theme().COLOR_OWN_NICK) else: color = 3 @@ -737,8 +737,8 @@ class MucTab(ChatTab): msg.enable('html') msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', True, self.general_jid, - True) and self.remote_wants_chatstates is not False): + if (config.get_by_tabname('send_chat_states', self.general_jid) + and self.remote_wants_chatstates is not False): msg['chat_state'] = needed if correct: msg['replace']['id'] = self.last_sent_message['id'] @@ -803,7 +803,7 @@ class MucTab(ChatTab): Resize the whole window. i.e. all its sub-windows """ self.need_resize = False - if config.get("hide_user_list", False) or self.size.tab_degrade_x: + if config.get('hide_user_list') or self.size.tab_degrade_x: display_user_list = False text_width = self.width else: @@ -844,7 +844,7 @@ class MucTab(ChatTab): if self.need_resize: self.resize() log.debug(' TAB Refresh: %s', self.__class__.__name__) - if config.get("hide_user_list", False) or self.size.tab_degrade_x: + if config.get('hide_user_list') or self.size.tab_degrade_x: display_user_list = False else: display_user_list = True @@ -887,7 +887,7 @@ class MucTab(ChatTab): for user in sorted(self.users, key=compare_users, reverse=True): if user.nick != self.own_nick: word_list.append(user.nick) - after = config.get('after_completion', ',') + ' ' + after = config.get('after_completion') + ' ' input_pos = self.input.pos if ' ' not in self.input.get_text()[:input_pos] or ( self.input.last_completion and @@ -895,7 +895,7 @@ class MucTab(ChatTab): self.input.last_completion + after): add_after = after else: - if not config.get('add_space_after_completion', True): + if not config.get('add_space_after_completion'): add_after = '' else: add_after = ' ' @@ -907,7 +907,7 @@ class MucTab(ChatTab): self.send_composing_chat_state(empty_after) def get_nick(self): - if not config.get('show_muc_jid', True): + if not config.get('show_muc_jid'): return safeJID(self.name).user return self.name @@ -924,25 +924,25 @@ class MucTab(ChatTab): self.state = 'disconnected' self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) - if config.get_by_tabname('send_chat_states', True, - self.general_jid, True) and not self.input.get_text(): + if (config.get_by_tabname('send_chat_states', self.general_jid) and + not self.input.get_text()): self.send_chat_state('inactive') self.check_scrolled() def on_gain_focus(self): self.state = 'current' if (self.text_win.built_lines and self.text_win.built_lines[-1] is None - and not config.get('show_useless_separator', False)): + and not config.get('show_useless_separator')): self.text_win.remove_line_separator() curses.curs_set(1) - if self.joined and config.get_by_tabname('send_chat_states', True, - self.general_jid, True) and not self.input.get_text(): + if self.joined and config.get_by_tabname('send_chat_states', + self.general_jid) and not self.input.get_text(): self.send_chat_state('active') def on_info_win_size_changed(self): if self.core.information_win_size >= self.height-3: return - if config.get("hide_user_list", False): + if config.get("hide_user_list"): text_width = self.width else: text_width = (self.width//10)*9 @@ -1003,7 +1003,7 @@ class MucTab(ChatTab): new_user.color = get_theme().COLOR_OWN_NICK if config.get_by_tabname('display_user_color_in_join_part', - True, self.general_jid, True): + self.general_jid): color = dump_tuple(new_user.color) else: color = 3 @@ -1122,11 +1122,11 @@ class MucTab(ChatTab): user = User(from_nick, affiliation, show, status, role, jid) self.users.append(user) - hide_exit_join = config.get_by_tabname('hide_exit_join', -1, - self.general_jid, True) + hide_exit_join = config.get_by_tabname('hide_exit_join', + self.general_jid) if hide_exit_join != 0: - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1163,8 +1163,8 @@ class MucTab(ChatTab): self.core.on_muc_own_nickchange(self) user.change_nick(new_nick) - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1206,10 +1206,9 @@ class MucTab(ChatTab): self.refresh_tab_win() self.core.current_tab().input.refresh() self.core.doupdate() - if config.get_by_tabname('autorejoin', False, - self.general_jid, True): - delay = config.get_by_tabname('autorejoin_delay', '5', - self.general_jid, True) + if config.get_by_tabname('autorejoin', self.general_jid): + delay = config.get_by_tabname('autorejoin_delay', + self.general_jid) delay = common.parse_str_to_secs(delay) if delay <= 0: muc.join_groupchat(self.core, self.name, self.own_nick) @@ -1223,7 +1222,7 @@ class MucTab(ChatTab): else: if config.get_by_tabname('display_user_color_in_join_part', - True, self.general_jid, True): + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1278,10 +1277,9 @@ class MucTab(ChatTab): self.core.current_tab().input.refresh() self.core.doupdate() # try to auto-rejoin - if config.get_by_tabname('autorejoin', False, - self.general_jid, True): - delay = config.get_by_tabname('autorejoin_delay', "5", - self.general_jid, True) + if config.get_by_tabname('autorejoin', self.general_jid): + delay = config.get_by_tabname('autorejoin_delay', + self.general_jid) delay = common.parse_str_to_secs(delay) if delay <= 0: muc.join_groupchat(self.core, self.name, self.own_nick) @@ -1293,8 +1291,8 @@ class MucTab(ChatTab): self.name, self.own_nick)) else: - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1327,13 +1325,12 @@ class MucTab(ChatTab): self.core.disable_private_tabs(from_room) self.refresh_tab_win() - hide_exit_join = max(config.get_by_tabname('hide_exit_join', -1, - self.general_jid, True), - -1) + hide_exit_join = config.get_by_tabname('hide_exit_join', + self.general_jid) - if hide_exit_join == -1 or user.has_talked_since(hide_exit_join): - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if hide_exit_join <= -1 or user.has_talked_since(hide_exit_join): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1373,8 +1370,8 @@ class MucTab(ChatTab): # build the message display_message = False # flag to know if something significant enough # to be displayed has changed - if config.get_by_tabname('display_user_color_in_join_part', True, - self.general_jid, True): + if config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): color = dump_tuple(user.color) else: color = 3 @@ -1410,8 +1407,8 @@ class MucTab(ChatTab): if not display_message: return msg = msg[:-2] # remove the last ", " - hide_status_change = config.get_by_tabname('hide_status_change', -1, - self.general_jid, True) + hide_status_change = config.get_by_tabname('hide_status_change', + self.general_jid) if hide_status_change < -1: hide_status_change = -1 if ((hide_status_change == -1 or \ @@ -1471,9 +1468,9 @@ class MucTab(ChatTab): self.state = 'highlight' highlighted = True else: - highlight_words = config.get_by_tabname('highlight_on', '', - self.general_jid, - True).split(':') + highlight_words = config.get_by_tabname('highlight_on', + self.general_jid) + highlight_words = highlight_words.split(':') for word in highlight_words: if word and word.lower() in txt.lower(): if self.state != 'current': @@ -1481,10 +1478,9 @@ class MucTab(ChatTab): highlighted = True break if highlighted: - beep_on = config.get('beep_on', 'highlight private').split() + beep_on = config.get('beep_on').split() if 'highlight' in beep_on and 'message' not in beep_on: - if not config.get_by_tabname('disable_beep', False, - self.name, False): + if not config.get_by_tabname('disable_beep', self.name): curses.beep() return highlighted @@ -1523,8 +1519,7 @@ class MucTab(ChatTab): if (not time and nickname and nickname != self.own_nick and self.state != 'current'): if (self.state != 'highlight' and - config.get_by_tabname('notify_messages', - True, self.name)): + config.get_by_tabname('notify_messages', self.name)): self.state = 'message' if time and not txt.startswith('/me'): txt = '\x19%(info_col)s}%(txt)s' % { diff --git a/src/tabs/privatetab.py b/src/tabs/privatetab.py index c1e8c8e5..4c01cd70 100644 --- a/src/tabs/privatetab.py +++ b/src/tabs/privatetab.py @@ -109,7 +109,7 @@ class PrivateTab(OneToOneTab): compare_users = lambda x: x.last_talked word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\ if user.nick != self.own_nick] - after = config.get('after_completion', ',')+" " + after = config.get('after_completion') + ' ' input_pos = self.input.pos if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\ self.input.get_text()[:input_pos] == self.input.last_completion + after): @@ -139,7 +139,7 @@ class PrivateTab(OneToOneTab): replaced = False if correct or msg['replace']['id']: msg['replace']['id'] = self.last_sent_message['id'] - if config.get_by_tabname('group_corrections', True, self.name): + if config.get_by_tabname('group_corrections', self.name): try: self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], user=user, jid=self.core.xmpp.boundjid, nickname=self.own_nick) @@ -153,7 +153,8 @@ class PrivateTab(OneToOneTab): msg.enable('html') msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) - if config.get_by_tabname('send_chat_states', True, self.general_jid, True) and self.remote_wants_chatstates is not False: + if (config.get_by_tabname('send_chat_states', self.general_jid) and + self.remote_wants_chatstates is not False): needed = 'inactive' if self.inactive else 'active' msg['chat_state'] = needed if attention and self.remote_supports_attention: @@ -278,9 +279,8 @@ class PrivateTab(OneToOneTab): self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname( - 'send_chat_states', True, self.general_jid, True) and\ - not self.input.get_text() and self.on: + if tab and tab.joined and config.get_by_tabname('send_chat_states', + self.general_jid) and not self.input.get_text() and self.on: self.send_chat_state('inactive') self.check_scrolled() @@ -288,9 +288,8 @@ class PrivateTab(OneToOneTab): self.state = 'current' curses.curs_set(1) tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname( - 'send_chat_states', True, self.general_jid, True) and\ - not self.input.get_text() and self.on: + if tab and tab.joined and config.get_by_tabname('send_chat_states', + self.general_jid,) and not self.input.get_text() and self.on: self.send_chat_state('active') def on_info_win_size_changed(self): @@ -334,7 +333,8 @@ class PrivateTab(OneToOneTab): self.check_features() tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) color = 3 - if tab and config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True): + if tab and config.get_by_tabname('display_user_color_in_join_part', + self.general_jid): user = tab.get_user_by_name(nick) if user: color = dump_tuple(user.color) diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py index 1ee98dd8..878e89ed 100644 --- a/src/tabs/rostertab.py +++ b/src/tabs/rostertab.py @@ -351,7 +351,7 @@ class RosterInfoTab(Tab): def callback(iq): if iq['type'] == 'result': self.core.information('Password updated', 'Account') - if config.get('password', ''): + if config.get('password'): config.silent_set('password', arg) else: self.core.information('Unable to change the password', 'Account') @@ -765,7 +765,7 @@ class RosterInfoTab(Tab): Show or hide offline contacts """ option = 'roster_show_offline' - value = config.get(option, False) + value = config.get(option) success = config.silent_set(option, str(not value)) roster.modified() if not success: diff --git a/src/text_buffer.py b/src/text_buffer.py index 4a41fd97..59aa96e1 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -64,7 +64,7 @@ class TextBuffer(object): def __init__(self, messages_nb_limit=None): if messages_nb_limit is None: - messages_nb_limit = config.get('max_messages_in_memory', 2048) + messages_nb_limit = config.get('max_messages_in_memory') self.messages_nb_limit = messages_nb_limit # Message objects self.messages = [] @@ -138,7 +138,7 @@ class TextBuffer(object): self.messages.pop(0) ret_val = None - show_timestamps = config.get('show_timestamps', True) + show_timestamps = config.get('show_timestamps') for window in self.windows: # make the associated windows # build the lines from the new message nb = window.build_new_message(msg, history=history, diff --git a/src/theming.py b/src/theming.py index 9820addf..1e9d6c40 100755 --- a/src/theming.py +++ b/src/theming.py @@ -301,10 +301,13 @@ class Theme(object): CHAR_QUIT = '<---' CHAR_KICK = '-!-' CHAR_NEW_TEXT_SEPARATOR = '- ' - CHAR_ACK_RECEIVED = '✔' + CHAR_OK = '✔' + CHAR_ERROR = '✖' + CHAR_EMPTY = ' ' + CHAR_ACK_RECEIVED = CHAR_OK CHAR_COLUMN_ASC = ' ▲' CHAR_COLUMN_DESC = ' ▼' - CHAR_ROSTER_ERROR = '✖' + CHAR_ROSTER_ERROR = CHAR_ERROR CHAR_ROSTER_TUNE = '♪' CHAR_ROSTER_ASKED = '?' CHAR_ROSTER_ACTIVITY = 'A' @@ -455,7 +458,7 @@ def update_themes_dir(option=None, value=None): # import from the user-defined prefs themes_dir = path.expanduser( value or - config.get('themes_dir', '') or + config.get('themes_dir') or path.join(os.environ.get('XDG_DATA_HOME') or path.join(os.environ.get('HOME'), '.local', 'share'), 'poezio', 'themes') @@ -482,7 +485,7 @@ def update_themes_dir(option=None, value=None): log.debug('Theme load path: %s', load_path) def reload_theme(): - theme_name = config.get('theme', 'default') + theme_name = config.get('theme') global theme if theme_name == 'default' or not theme_name.strip(): theme = Theme() diff --git a/src/windows/funcs.py b/src/windows/funcs.py index 47011faf..d58d4683 100644 --- a/src/windows/funcs.py +++ b/src/windows/funcs.py @@ -20,7 +20,7 @@ def find_first_format_char(text, chars=None): return pos def truncate_nick(nick, size=None): - size = size or config.get('max_nick_length', 25) + size = size or config.get('max_nick_length') if size < 1: size = 1 if nick and len(nick) > size: diff --git a/src/windows/info_bar.py b/src/windows/info_bar.py index 10225d5d..e66343c5 100644 --- a/src/windows/info_bar.py +++ b/src/windows/info_bar.py @@ -24,10 +24,10 @@ class GlobalInfoBar(Win): self._win.erase() self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - create_gaps = config.get('create_gaps', False) - show_names = config.get('show_tab_names', False) - show_nums = config.get('show_tab_numbers', True) - use_nicks = config.get('use_tab_nicks', True) + create_gaps = config.get('create_gaps') + show_names = config.get('show_tab_names') + show_nums = config.get('show_tab_numbers') + use_nicks = config.get('use_tab_nicks') # ignore any remaining gap tabs if the feature is not enabled if create_gaps: sorted_tabs = self.core.tabs[:] @@ -37,7 +37,7 @@ class GlobalInfoBar(Win): for nb, tab in enumerate(sorted_tabs): if not tab: continue color = tab.color - if not config.get('show_inactive_tabs', True) and\ + if not config.get('show_inactive_tabs') and\ color is get_theme().COLOR_TAB_NORMAL: continue try: @@ -70,11 +70,11 @@ class VerticalGlobalInfoBar(Win): height, width = self._win.getmaxyx() self._win.erase() sorted_tabs = [tab for tab in self.core.tabs if tab] - if not config.get('show_inactive_tabs', True): + if not config.get('show_inactive_tabs'): sorted_tabs = [tab for tab in sorted_tabs if\ tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] nb_tabs = len(sorted_tabs) - use_nicks = config.get('use_tab_nicks', True) + use_nicks = config.get('use_tab_nicks') if nb_tabs >= height: for y, tab in enumerate(sorted_tabs): if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT: @@ -89,8 +89,7 @@ class VerticalGlobalInfoBar(Win): sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] for y, tab in enumerate(sorted_tabs): color = tab.vertical_color - - if not config.get('vertical_tab_list_sort', 'desc') != 'asc': + if not config.get('vertical_tab_list_sort') != 'asc': y = height - y - 1 self.addstr(y, 0, "%2d" % tab.nb, to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) diff --git a/src/windows/inputs.py b/src/windows/inputs.py index afce3dd8..d345443b 100644 --- a/src/windows/inputs.py +++ b/src/windows/inputs.py @@ -564,7 +564,7 @@ class HistoryInput(Input): self.current_completed = '' self.key_func['^R'] = self.toggle_search self.search = False - if config.get('separate_history', False): + if config.get('separate_history'): self.history = list() def toggle_search(self): diff --git a/src/windows/muc.py b/src/windows/muc.py index 02bc58ef..7e3541ba 100644 --- a/src/windows/muc.py +++ b/src/windows/muc.py @@ -34,10 +34,10 @@ class UserList(Win): def refresh(self, users): log.debug('Refresh: %s', self.__class__.__name__) - if config.get("hide_user_list", False): + if config.get('hide_user_list'): return # do not refresh if this win is hidden. self._win.erase() - if config.get('user_list_sort', 'desc').lower() == 'asc': + if config.get('user_list_sort').lower() == 'asc': y, x = self._win.getmaxyx() y -= 1 users = sorted(users) @@ -55,7 +55,7 @@ class UserList(Win): self.addstr(y, 2, poopt.cut_by_columns(user.nick, self.width - 2), to_curses_attr(user.color)) - if config.get('user_list_sort', 'desc').lower() == 'asc': + if config.get('user_list_sort').lower() == 'asc': y -= 1 else: y += 1 @@ -63,12 +63,12 @@ class UserList(Win): break # draw indicators of position in the list if self.pos > 0: - if config.get('user_list_sort', 'desc').lower() == 'asc': + if config.get('user_list_sort').lower() == 'asc': self.draw_plus(self.height-1) else: self.draw_plus(0) if self.pos + self.height < len(users): - if config.get('user_list_sort', 'desc').lower() == 'asc': + if config.get('user_list_sort').lower() == 'asc': self.draw_plus(0) else: self.draw_plus(self.height-1) diff --git a/src/windows/roster_win.py b/src/windows/roster_win.py index f9858a3a..6ecb6128 100644 --- a/src/windows/roster_win.py +++ b/src/windows/roster_win.py @@ -99,9 +99,9 @@ class RosterWin(Win): for contact in roster.get_contacts_sorted_filtered(sort): self.roster_cache.append(contact) else: - show_offline = config.get('roster_show_offline', False) or roster.contact_filter - sort = config.get('roster_sort', 'jid:show') or 'jid:show' - group_sort = config.get('roster_group_sort', 'name') or 'name' + show_offline = config.get('roster_show_offline') or roster.contact_filter + sort = config.get('roster_sort') or 'jid:show' + group_sort = config.get('roster_group_sort') or 'name' self.roster_cache = [] # build the cache for group in roster.get_groups(group_sort): @@ -229,7 +229,7 @@ class RosterWin(Win): self.addstr(y, 0, ' ') self.addstr(theme.CHAR_STATUS, to_curses_attr(color)) - show_roster_sub = config.get('show_roster_subscriptions', '') + show_roster_sub = config.get('show_roster_subscriptions') self.addstr(' ') if resource: @@ -237,7 +237,7 @@ class RosterWin(Win): added += 4 if contact.ask: added += len(get_theme().CHAR_ROSTER_ASKED) - if config.get('show_s2s_errors', True) and contact.error: + if config.get('show_s2s_errors') and contact.error: added += len(get_theme().CHAR_ROSTER_ERROR) if contact.tune: added += len(get_theme().CHAR_ROSTER_TUNE) @@ -250,7 +250,7 @@ class RosterWin(Win): if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): added += len(theme.char_subscription(contact.subscription, keep=show_roster_sub)) - if not config.get('show_roster_jids', True) and contact.name: + if not config.get('show_roster_jids') and contact.name: display_name = '%s' % contact.name elif contact.name and contact.name != contact.bare_jid: display_name = '%s (%s)' % (contact.name, contact.bare_jid) @@ -268,7 +268,7 @@ class RosterWin(Win): self.addstr(theme.char_subscription(contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION)) if contact.ask: self.addstr(get_theme().CHAR_ROSTER_ASKED, to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) - if config.get('show_s2s_errors', True) and contact.error: + if config.get('show_s2s_errors') and contact.error: self.addstr(get_theme().CHAR_ROSTER_ERROR, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) if contact.tune: self.addstr(get_theme().CHAR_ROSTER_TUNE, to_curses_attr(get_theme().COLOR_ROSTER_TUNE)) diff --git a/src/windows/text_win.py b/src/windows/text_win.py index 86aea1ef..8a8f75ae 100644 --- a/src/windows/text_win.py +++ b/src/windows/text_win.py @@ -19,7 +19,9 @@ from theming import to_curses_attr, get_theme, dump_tuple class TextWin(Win): - def __init__(self, lines_nb_limit=config.get('max_lines_in_memory', 2048)): + def __init__(self, lines_nb_limit=None): + if lines_nb_limit is None: + lines_nb_limit = config.get('max_lines_in_memory') Win.__init__(self) self.lines_nb_limit = lines_nb_limit self.pos = 0 @@ -265,7 +267,7 @@ class TextWin(Win): lines = self.built_lines[-self.height:] else: lines = self.built_lines[-self.height-self.pos:-self.pos] - with_timestamps = config.get("show_timestamps", True) + with_timestamps = config.get("show_timestamps") self._win.move(0, 0) self._win.erase() for y, line in enumerate(lines): @@ -402,7 +404,7 @@ class TextWin(Win): def rebuild_everything(self, room): self.built_lines = [] - with_timestamps = config.get("show_timestamps", True) + with_timestamps = config.get('show_timestamps') for message in room.messages: self.build_new_message(message, clean=False, timestamp=with_timestamps) if self.separator_after is message: @@ -415,7 +417,7 @@ class TextWin(Win): Find a message, and replace it with a new one (instead of rebuilding everything in order to correct a message) """ - with_timestamps = config.get("show_timestamps", True) + with_timestamps = config.get('show_timestamps') for i in range(len(self.built_lines)-1, -1, -1): if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id: index = i diff --git a/src/xhtml.py b/src/xhtml.py index b00fe9a9..01e2dfcd 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -12,9 +12,13 @@ xhtml code to shell colors, poezio colors to xhtml code """ -import re +import base64 import curses +import hashlib +import re +from os import path from slixmpp.xmlstream import ET +from urllib.parse import unquote from io import BytesIO from xml import sax @@ -178,10 +182,12 @@ colors = { whitespace_re = re.compile(r'\s+') xhtml_attr_re = re.compile(r'\x19-?\d[^}]*}|\x19[buaio]') +xhtml_data_re = re.compile(r'data:image/([a-z]+);base64,(.+)') xhtml_simple_attr_re = re.compile(r'\x19\d') -def get_body_from_message_stanza(message, use_xhtml=False): +def get_body_from_message_stanza(message, use_xhtml=False, + tmp_dir=None, extract_images=False): """ Returns a string with xhtml markups converted to poezio colors if there's an xhtml_im element, or @@ -191,7 +197,8 @@ def get_body_from_message_stanza(message, use_xhtml=False): xhtml = message['html'].xml xhtml_body = xhtml.find('{http://www.w3.org/1999/xhtml}body') if xhtml_body: - content = xhtml_to_poezio_colors(xhtml_body) + content = xhtml_to_poezio_colors(xhtml_body, tmp_dir=tmp_dir, + extract_images=extract_images) content = content if content else message['body'] return content or " " return message['body'] @@ -281,7 +288,7 @@ def trim(string): return re.sub(whitespace_re, ' ', string) class XHTMLHandler(sax.ContentHandler): - def __init__(self, force_ns=False): + def __init__(self, force_ns=False, tmp_dir=None, extract_images=False): self.builder = [] self.formatting = [] self.attrs = [] @@ -291,6 +298,9 @@ class XHTMLHandler(sax.ContentHandler): # do not care about xhtml-in namespace self.force_ns = force_ns + self.tmp_dir = tmp_dir + self.extract_images = extract_images + @property def result(self): return ''.join(self.builder).strip() @@ -331,7 +341,22 @@ class XHTMLHandler(sax.ContentHandler): elif name == 'em': self.append_formatting('\x19i') elif name == 'img': - builder.append(trim(attrs['src'])) + if re.match(xhtml_data_re, attrs['src']) and self.extract_images: + type_, data = [i for i in re.split(xhtml_data_re, attrs['src']) if i] + bin_data = base64.b64decode(unquote(data)) + filename = hashlib.sha1(bin_data).hexdigest() + '.' + type_ + filepath = path.join(self.tmp_dir, filename) + if not path.exists(filepath): + try: + with open(filepath, 'wb') as fd: + fd.write(bin_data) + builder.append('file://%s' % filepath) + except Exception as e: + builder.append('[Error while saving image: %s]' % e) + else: + builder.append('file://%s' % filepath) + else: + builder.append(trim(attrs['src'])) if 'alt' in attrs: builder.append(' (%s)' % trim(attrs['alt'])) elif name == 'ul': @@ -389,13 +414,14 @@ class XHTMLHandler(sax.ContentHandler): if 'title' in attrs: builder.append(' [' + attrs['title'] + ']') -def xhtml_to_poezio_colors(xml, force=False): +def xhtml_to_poezio_colors(xml, force=False, tmp_dir=None, extract_images=None): if isinstance(xml, str): xml = xml.encode('utf8') elif not isinstance(xml, bytes): xml = ET.tostring(xml) - handler = XHTMLHandler(force_ns=force) + handler = XHTMLHandler(force_ns=force, tmp_dir=tmp_dir, + extract_images=extract_images) parser = sax.make_parser() parser.setFeature(sax.handler.feature_namespaces, True) parser.setContentHandler(handler) diff --git a/test/test_common.py b/test/test_common.py new file mode 100644 index 00000000..315318bd --- /dev/null +++ b/test/test_common.py @@ -0,0 +1,79 @@ +""" +Test the functions in the `common` module +""" + +import sys +sys.path.append('src') + +import time +import pytest +import datetime +from sleekxmpp import JID +from datetime import timedelta +from common import (datetime_tuple, get_utc_time, get_local_time, shell_split, + find_argument_quoted, find_argument_unquoted, + parse_str_to_secs, parse_secs_to_str, safeJID) + +def test_datetime_tuple(): + time.timezone = 0 + time.altzone = 0 + + assert datetime_tuple('20130226T06:23:12') == datetime.datetime(2013, 2, 26, 6, 23, 12) + assert datetime_tuple('2013-02-26T06:23:12+02:00') == datetime.datetime(2013, 2, 26, 4, 23, 12) + + time.timezone = -3600 + time.altzone = -3600 + + assert datetime_tuple('20130226T07:23:12') == datetime.datetime(2013, 2, 26, 8, 23, 12) + assert datetime_tuple('2013-02-26T07:23:12+02:00') == datetime.datetime(2013, 2, 26, 6, 23, 12) + +def test_utc_time(): + delta = timedelta(seconds=-3600) + d = datetime.datetime.now() + time.timezone = -3600; time.altzone = -3600 + assert get_utc_time(local_time=d) == d + delta + +def test_local_time(): + delta = timedelta(seconds=-3600) + d = datetime.datetime.now() + time.timezone = -3600 + time.altzone = -3600 + assert get_local_time(d) == d - delta + +def test_shell_split(): + assert shell_split('"sdf 1" "toto 2"') == ['sdf 1', 'toto 2'] + assert shell_split('toto "titi"') == ['toto', 'titi'] + assert shell_split('toto ""') == ['toto', ''] + assert shell_split('to"to titi "a" b') == ['to"to', 'titi', 'a', 'b'] + assert shell_split('"toto titi" toto ""') == ['toto titi', 'toto', ''] + assert shell_split('toto "titi') == ['toto', 'titi'] + +def test_argument_quoted(): + assert find_argument_quoted(4, 'toto titi tata') == 3 + assert find_argument_quoted(4, '"toto titi" tata') == 0 + assert find_argument_quoted(8, '"toto" "titi tata"') == 1 + assert find_argument_quoted(8, '"toto" "titi tata') == 1 + assert find_argument_quoted(3, '"toto" "titi tata') == 0 + assert find_argument_quoted(18, '"toto" "titi tata" ') == 2 + +def test_argument_unquoted(): + assert find_argument_unquoted(2, 'toto titi tata') == 0 + assert find_argument_unquoted(3, 'toto titi tata') == 0 + assert find_argument_unquoted(6, 'toto titi tata') == 1 + assert find_argument_unquoted(4, 'toto titi tata') == 3 + assert find_argument_unquoted(25, 'toto titi tata') == 3 + +def test_parse_str_to_secs(): + assert parse_str_to_secs("1d3m1h") == 90180 + assert parse_str_to_secs("1d3mfaiiiiil") == 0 + +def test_parse_secs_to_str(): + assert parse_secs_to_str(3601) == '1h1s' + assert parse_secs_to_str(0) == '0s' + + with pytest.raises(TypeError): + parse_secs_to_str('toto') + +def test_safeJID(): + assert safeJID('toto@titi/tata') == JID('toto@titi/tata') + assert safeJID('é_è') == JID('') diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 00000000..f8d06258 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,114 @@ +""" +Test the config module +""" + +import tempfile +import pytest +import sys +import os + + +sys.path.append('src') + +import config + +@pytest.yield_fixture(scope="module") +def config_obj(): + file_ = tempfile.NamedTemporaryFile(delete=False) + conf = config.Config(file_name=file_.name) + yield conf + del conf + os.unlink(file_.name) + +class TestConfigSimple(object): + def test_get_set(self, config_obj): + config_obj.set_and_save('test', value='coucou') + config_obj.set_and_save('test2', value='true') + assert config_obj.get('test') == 'coucou' + assert config_obj.get('test2') == 'true' + assert config_obj.get('toto') == '' + + def test_file_content(self, config_obj): + with open(config_obj.file_name, 'r') as fd: + data = fd.read() + supposed_content = '[Poezio]\ntest = coucou\ntest2 = true\n' + assert data == supposed_content + + def test_get_types(self, config_obj): + + config_obj.set_and_save('test_int', '99') + config_obj.set_and_save('test_int_neg', '-1') + config_obj.set_and_save('test_bool_t', 'true') + config_obj.set_and_save('test_bool_f', 'false') + config_obj.set_and_save('test_float', '1.5') + + assert config_obj.get('test_int', default=0) == 99 + assert config_obj.get('test_int_neg', default=0) == -1 + assert config_obj.get('test_bool_t', default=False) == True + assert config_obj.get('test_bool_f', default=True) == False + assert config_obj.get('test_float', default=1.0) == 1.5 + + def test_remove(self, config_obj): + with open(config_obj.file_name, 'r') as fd: + data = fd.read() + + supposed_content = ('[Poezio]\ntest = coucou\ntest2 = true\n' + 'test_int = 99\ntest_int_neg = -1\ntest_bool_t =' + ' true\ntest_bool_f = false\ntest_float = 1.5\n') + + assert data == supposed_content + + config_obj.remove_and_save('test_int') + config_obj.remove_and_save('test_int_neg') + config_obj.remove_and_save('test_bool_t') + config_obj.remove_and_save('test_bool_f') + config_obj.remove_and_save('test_float') + + with open(config_obj.file_name, 'r') as fd: + data = fd.read() + + supposed_content = '[Poezio]\ntest = coucou\ntest2 = true\n' + + assert data == supposed_content + + + def test_toggle(self, config_obj): + config_obj.set_and_save('test2', value='toggle') + assert config_obj.get('test2') == 'false' + config_obj.set_and_save('test2', value='toggle') + assert config_obj.get('test2') == 'true' + + def test_get_set_default(self, config_obj): + assert config_obj.get('doesnotexist', 'toto@tata') == 'toto@tata' + assert config_obj.get('doesnotexist2', '1234') == '1234' + +class TestConfigSections(object): + def test_set_section(self, config_obj): + config_obj.set_and_save('option1', 'test', section='NotPoezio') + config_obj.set_and_save('option2', 'test2', section='NotPoezio') + + assert config_obj.get('option1', section='NotPoezio') == 'test' + assert config_obj.get('option2', section='NotPoezio') == 'test2' + + def test_file_content(self, config_obj): + with open(config_obj.file_name, 'r') as fd: + data = fd.read() + supposed_content = ('[Poezio]\ntest = coucou\ntest2 = true\n' + '[NotPoezio]\noption1 = test\noption2 = test2\n') + assert data == supposed_content + +class TestTabNames(object): + def test_get_tabname(self, config_obj): + config.post_logging_setup() + config_obj.set_and_save('test', value='value.toto@toto.com', + section='toto@toto.com') + config_obj.set_and_save('test2', value='value2@toto.com', + section='@toto.com') + + assert config_obj.get_by_tabname('test', 'toto@toto.com') == 'value.toto@toto.com' + assert config_obj.get_by_tabname('test2', 'toto@toto.com') == 'value2@toto.com' + assert config_obj.get_by_tabname('test2', 'toto@toto.com', fallback=False) == 'value2@toto.com' + assert config_obj.get_by_tabname('test2', 'toto@toto.com', fallback_server=False) == 'true' + assert config_obj.get_by_tabname('test_int', 'toto@toto.com', fallback=False) == '' + + diff --git a/test/test_poopt.py b/test/test_poopt.py new file mode 100644 index 00000000..9b640ff0 --- /dev/null +++ b/test/test_poopt.py @@ -0,0 +1,14 @@ +""" +Test of the poopt module +""" + +import pytest +import sys +sys.path.append('src') + +from poopt import cut_text + +def test_cut_text(): + + text = '12345678901234567890' + assert cut_text(text, 5) == [(0, 5), (5, 10), (10, 15), (15, 20)] diff --git a/test/test_theming.py b/test/test_theming.py new file mode 100644 index 00000000..9cdb4829 --- /dev/null +++ b/test/test_theming.py @@ -0,0 +1,26 @@ +""" +Test the functions in the `theming` module +""" + +import sys +import pytest +sys.path.append('src') + +from theming import dump_tuple, read_tuple + +def test_read_tuple(): + assert read_tuple('1,-1,u') == ((1, -1), 'u') + assert read_tuple('1,2') == ((1, 2), None) + + with pytest.raises(IndexError): + read_tuple('1') + + with pytest.raises(ValueError): + read_tuple('toto') + +def test_dump_tuple(): + assert dump_tuple((1, 2)) == '1,2' + assert dump_tuple((1, )) == '1' + assert dump_tuple((1, 2, 'u')) == '1,2,u' + + diff --git a/test/test_windows.py b/test/test_windows.py new file mode 100644 index 00000000..8fb85295 --- /dev/null +++ b/test/test_windows.py @@ -0,0 +1,76 @@ +import pytest +import sys +sys.path.append('src') + +class ConfigShim(object): + def get(self, *args, **kwargs): + return '' + +import config +config.config = ConfigShim() +import core + +from windows import Input, HistoryInput, MessageInput, CommandInput + +@pytest.fixture +def input(): + input = Input() + input.rewrite_text = lambda: None + return input + +class TestInput(object): + + def test_do_command(self, input): + + input.do_command('a') + assert input.text == 'a' + + for char in 'coucou': + input.do_command(char) + assert input.text == 'acoucou' + + def test_empty(self, input): + assert input.is_empty() == True + input.do_command('a') + assert input.is_empty() == False + + def test_key_left(self, input): + for char in 'this is a line': + input.do_command(char) + for i in range(4): + input.key_left() + for char in 'long ': + input.do_command(char) + + assert input.text == 'this is a long line' + + def test_key_right(self, input): + for char in 'this is a line': + input.do_command(char) + for i in range(4): + input.key_left() + input.key_right() + + for char in 'iii': + input.do_command(char) + + assert input.text == 'this is a liiiine' + + def test_key_home(self, input): + for char in 'this is a line of text': + input.do_command(char) + input.do_command('z') + input.key_home() + input.do_command('a') + + assert input.text == 'athis is a line of textz' + + def test_key_end(self, input): + for char in 'this is a line of text': + input.do_command(char) + input.key_home() + input.key_end() + input.do_command('z') + + assert input.text == 'this is a line of textz' + diff --git a/test/test_xhtml.py b/test/test_xhtml.py new file mode 100644 index 00000000..58857d67 --- /dev/null +++ b/test/test_xhtml.py @@ -0,0 +1,50 @@ +""" +Test the functions in the `xhtml` module +""" + +import pytest +import sys +import xml +sys.path.append('src') + +from xhtml import (poezio_colors_to_html, xhtml_to_poezio_colors, + parse_css, clean_text) + +def test_clean_text(): + example_string = '\x191}Toto \x192,-1}titi\x19b Tata' + assert clean_text(example_string) == 'Toto titi Tata' + + clean_string = 'toto titi tata' + assert clean_text(clean_string) == clean_string + +def test_poezio_colors_to_html(): + base = "<body xmlns='http://www.w3.org/1999/xhtml'><p>" + end = "</p></body>" + text = '\x191}coucou' + assert poezio_colors_to_html(text) == base + '<span style="color: red;">coucou</span>' + end + + text = '\x19bcoucou\x19o toto \x194}titi' + assert poezio_colors_to_html(text) == base + '<span style="font-weight: bold;">coucou</span> toto <span style="color: blue;">titi</span>' + end + +def test_xhtml_to_poezio_colors(): + start = b'<body xmlns="http://www.w3.org/1999/xhtml"><p>' + end = b'</p></body>' + xhtml = start + b'test' + end + assert xhtml_to_poezio_colors(xhtml) == 'test' + + xhtml = start + b'<a href="http://perdu.com">salut</a>' + end + assert xhtml_to_poezio_colors(xhtml) == '\x19usalut\x19o (http://perdu.com)' + + xhtml = start + b'<a href="http://perdu.com">http://perdu.com</a>' + end + assert xhtml_to_poezio_colors(xhtml) == '\x19uhttp://perdu.com\x19o' + + with pytest.raises(xml.sax._exceptions.SAXParseException): + xhtml_to_poezio_colors(b'<p>Invalid xml') + +def test_parse_css(): + example_css = 'text-decoration: underline; color: red;' + assert parse_css(example_css) == '\x19u\x19196}' + + example_css = 'text-decoration: underline coucou color: red;' + assert parse_css(example_css) == '' + |