From 332a5c2553db41de777473a1e1be9cd1522c9496 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 31 Mar 2016 18:54:41 +0100 Subject: Move the src directory to poezio, for better cython compatibility. --- Makefile | 2 +- doc/source/conf.py | 2 +- doc/source/keys.rst | 2 +- launch.sh | 2 +- plugins/link.py | 2 +- poezio/args.py | 28 + poezio/bookmarks.py | 289 +++++ poezio/common.py | 483 ++++++++ poezio/config.py | 685 +++++++++++ poezio/connection.py | 223 ++++ poezio/contact.py | 197 ++++ poezio/core/__init__.py | 8 + poezio/core/commands.py | 999 ++++++++++++++++ poezio/core/completions.py | 387 +++++++ poezio/core/core.py | 2102 ++++++++++++++++++++++++++++++++++ poezio/core/handlers.py | 1354 ++++++++++++++++++++++ poezio/core/structs.py | 49 + poezio/daemon.py | 82 ++ poezio/decorators.py | 139 +++ poezio/events.py | 87 ++ poezio/fifo.py | 71 ++ poezio/fixes.py | 97 ++ poezio/keyboard.py | 168 +++ poezio/logger.py | 284 +++++ poezio/multiuserchat.py | 196 ++++ poezio/pep.py | 221 ++++ poezio/plugin.py | 485 ++++++++ poezio/plugin_manager.py | 384 +++++++ poezio/poezio.py | 115 ++ poezio/poezio_shlex.py | 276 +++++ poezio/pooptmodule.c | 486 ++++++++ poezio/roster.py | 334 ++++++ poezio/roster_sorting.py | 90 ++ poezio/singleton.py | 20 + poezio/size_manager.py | 46 + poezio/tabs/__init__.py | 13 + poezio/tabs/adhoc_commands_list.py | 57 + poezio/tabs/basetabs.py | 881 ++++++++++++++ poezio/tabs/bookmarkstab.py | 145 +++ poezio/tabs/conversationtab.py | 484 ++++++++ poezio/tabs/data_forms.py | 75 ++ poezio/tabs/listtab.py | 202 ++++ poezio/tabs/muclisttab.py | 70 ++ poezio/tabs/muctab.py | 1720 ++++++++++++++++++++++++++++ poezio/tabs/privatetab.py | 362 ++++++ poezio/tabs/rostertab.py | 1280 +++++++++++++++++++++ poezio/tabs/xmltab.py | 360 ++++++ poezio/text_buffer.py | 242 ++++ poezio/theming.py | 534 +++++++++ poezio/timed_events.py | 58 + poezio/user.py | 121 ++ poezio/windows/__init__.py | 20 + poezio/windows/base_wins.py | 168 +++ poezio/windows/bookmark_forms.py | 278 +++++ poezio/windows/data_forms.py | 472 ++++++++ poezio/windows/funcs.py | 54 + poezio/windows/info_bar.py | 106 ++ poezio/windows/info_wins.py | 311 +++++ poezio/windows/input_placeholders.py | 77 ++ poezio/windows/inputs.py | 768 +++++++++++++ poezio/windows/list.py | 236 ++++ poezio/windows/misc.py | 60 + poezio/windows/muc.py | 143 +++ poezio/windows/roster_win.py | 387 +++++++ poezio/windows/text_win.py | 597 ++++++++++ poezio/xhtml.py | 543 +++++++++ setup.py | 12 +- src/__init__.py | 1 - src/args.py | 28 - src/bookmarks.py | 289 ----- src/common.py | 483 -------- src/config.py | 685 ----------- src/connection.py | 223 ---- src/contact.py | 197 ---- src/core/__init__.py | 8 - src/core/commands.py | 999 ---------------- src/core/completions.py | 387 ------- src/core/core.py | 2102 ---------------------------------- src/core/handlers.py | 1354 ---------------------- src/core/structs.py | 49 - src/daemon.py | 82 -- src/decorators.py | 139 --- src/events.py | 87 -- src/fifo.py | 71 -- src/fixes.py | 97 -- src/keyboard.py | 168 --- src/logger.py | 284 ----- src/multiuserchat.py | 196 ---- src/pep.py | 221 ---- src/plugin.py | 485 -------- src/plugin_manager.py | 384 ------- src/poezio.py | 115 -- src/poezio_shlex.py | 276 ----- src/pooptmodule.c | 486 -------- src/roster.py | 334 ------ src/roster_sorting.py | 90 -- src/singleton.py | 20 - src/size_manager.py | 46 - src/tabs/__init__.py | 13 - src/tabs/adhoc_commands_list.py | 57 - src/tabs/basetabs.py | 881 -------------- src/tabs/bookmarkstab.py | 145 --- src/tabs/conversationtab.py | 484 -------- src/tabs/data_forms.py | 75 -- src/tabs/listtab.py | 202 ---- src/tabs/muclisttab.py | 70 -- src/tabs/muctab.py | 1720 ---------------------------- src/tabs/privatetab.py | 362 ------ src/tabs/rostertab.py | 1280 --------------------- src/tabs/xmltab.py | 360 ------ src/text_buffer.py | 242 ---- src/theming.py | 534 --------- src/timed_events.py | 58 - src/user.py | 121 -- src/windows/__init__.py | 20 - src/windows/base_wins.py | 168 --- src/windows/bookmark_forms.py | 278 ----- src/windows/data_forms.py | 472 -------- src/windows/funcs.py | 54 - src/windows/info_bar.py | 106 -- src/windows/info_wins.py | 311 ----- src/windows/input_placeholders.py | 77 -- src/windows/inputs.py | 768 ------------- src/windows/list.py | 236 ---- src/windows/misc.py | 60 - src/windows/muc.py | 143 --- src/windows/roster_win.py | 387 ------- src/windows/text_win.py | 597 ---------- src/xhtml.py | 543 --------- test/test_common.py | 2 +- test/test_completion.py | 2 +- test/test_config.py | 2 +- test/test_poopt.py | 2 +- test/test_theming.py | 2 +- test/test_windows.py | 2 +- test/test_xhtml.py | 2 +- update.sh | 10 - 137 files changed, 21227 insertions(+), 21238 deletions(-) create mode 100644 poezio/args.py create mode 100644 poezio/bookmarks.py create mode 100644 poezio/common.py create mode 100644 poezio/config.py create mode 100644 poezio/connection.py create mode 100644 poezio/contact.py create mode 100644 poezio/core/__init__.py create mode 100644 poezio/core/commands.py create mode 100644 poezio/core/completions.py create mode 100644 poezio/core/core.py create mode 100644 poezio/core/handlers.py create mode 100644 poezio/core/structs.py create mode 100755 poezio/daemon.py create mode 100644 poezio/decorators.py create mode 100644 poezio/events.py create mode 100644 poezio/fifo.py create mode 100644 poezio/fixes.py create mode 100755 poezio/keyboard.py create mode 100644 poezio/logger.py create mode 100644 poezio/multiuserchat.py create mode 100644 poezio/pep.py create mode 100644 poezio/plugin.py create mode 100644 poezio/plugin_manager.py create mode 100644 poezio/poezio.py create mode 100644 poezio/poezio_shlex.py create mode 100644 poezio/pooptmodule.c create mode 100644 poezio/roster.py create mode 100644 poezio/roster_sorting.py create mode 100644 poezio/singleton.py create mode 100644 poezio/size_manager.py create mode 100644 poezio/tabs/__init__.py create mode 100644 poezio/tabs/adhoc_commands_list.py create mode 100644 poezio/tabs/basetabs.py create mode 100644 poezio/tabs/bookmarkstab.py create mode 100644 poezio/tabs/conversationtab.py create mode 100644 poezio/tabs/data_forms.py create mode 100644 poezio/tabs/listtab.py create mode 100644 poezio/tabs/muclisttab.py create mode 100644 poezio/tabs/muctab.py create mode 100644 poezio/tabs/privatetab.py create mode 100644 poezio/tabs/rostertab.py create mode 100644 poezio/tabs/xmltab.py create mode 100644 poezio/text_buffer.py create mode 100755 poezio/theming.py create mode 100644 poezio/timed_events.py create mode 100644 poezio/user.py create mode 100644 poezio/windows/__init__.py create mode 100644 poezio/windows/base_wins.py create mode 100644 poezio/windows/bookmark_forms.py create mode 100644 poezio/windows/data_forms.py create mode 100644 poezio/windows/funcs.py create mode 100644 poezio/windows/info_bar.py create mode 100644 poezio/windows/info_wins.py create mode 100644 poezio/windows/input_placeholders.py create mode 100644 poezio/windows/inputs.py create mode 100644 poezio/windows/list.py create mode 100644 poezio/windows/misc.py create mode 100644 poezio/windows/muc.py create mode 100644 poezio/windows/roster_win.py create mode 100644 poezio/windows/text_win.py create mode 100644 poezio/xhtml.py delete mode 100644 src/__init__.py delete mode 100644 src/args.py delete mode 100644 src/bookmarks.py delete mode 100644 src/common.py delete mode 100644 src/config.py delete mode 100644 src/connection.py delete mode 100644 src/contact.py delete mode 100644 src/core/__init__.py delete mode 100644 src/core/commands.py delete mode 100644 src/core/completions.py delete mode 100644 src/core/core.py delete mode 100644 src/core/handlers.py delete mode 100644 src/core/structs.py delete mode 100755 src/daemon.py delete mode 100644 src/decorators.py delete mode 100644 src/events.py delete mode 100644 src/fifo.py delete mode 100644 src/fixes.py delete mode 100755 src/keyboard.py delete mode 100644 src/logger.py delete mode 100644 src/multiuserchat.py delete mode 100644 src/pep.py delete mode 100644 src/plugin.py delete mode 100644 src/plugin_manager.py delete mode 100644 src/poezio.py delete mode 100644 src/poezio_shlex.py delete mode 100644 src/pooptmodule.c delete mode 100644 src/roster.py delete mode 100644 src/roster_sorting.py delete mode 100644 src/singleton.py delete mode 100644 src/size_manager.py delete mode 100644 src/tabs/__init__.py delete mode 100644 src/tabs/adhoc_commands_list.py delete mode 100644 src/tabs/basetabs.py delete mode 100644 src/tabs/bookmarkstab.py delete mode 100644 src/tabs/conversationtab.py delete mode 100644 src/tabs/data_forms.py delete mode 100644 src/tabs/listtab.py delete mode 100644 src/tabs/muclisttab.py delete mode 100644 src/tabs/muctab.py delete mode 100644 src/tabs/privatetab.py delete mode 100644 src/tabs/rostertab.py delete mode 100644 src/tabs/xmltab.py delete mode 100644 src/text_buffer.py delete mode 100755 src/theming.py delete mode 100644 src/timed_events.py delete mode 100644 src/user.py delete mode 100644 src/windows/__init__.py delete mode 100644 src/windows/base_wins.py delete mode 100644 src/windows/bookmark_forms.py delete mode 100644 src/windows/data_forms.py delete mode 100644 src/windows/funcs.py delete mode 100644 src/windows/info_bar.py delete mode 100644 src/windows/info_wins.py delete mode 100644 src/windows/input_placeholders.py delete mode 100644 src/windows/inputs.py delete mode 100644 src/windows/list.py delete mode 100644 src/windows/misc.py delete mode 100644 src/windows/muc.py delete mode 100644 src/windows/roster_win.py delete mode 100644 src/windows/text_win.py delete mode 100644 src/xhtml.py diff --git a/Makefile b/Makefile index 2b3e3a51..6b901279 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ clean: rm -rf poezio.egg-info rm -rf dist rm -rf build - rm -f src/*.so + rm -f poezio/*.so install: all python3 setup.py install --root=$(DESTDIR) --optimize=1 diff --git a/doc/source/conf.py b/doc/source/conf.py index 4ce97717..61f17b38 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -15,7 +15,7 @@ import sys, os, time sys.path.insert(0, os.path.abspath('../stub')) -sys.path.append(os.path.abspath('../../src/')) +sys.path.append(os.path.abspath('../../poezio/')) sys.path.append(os.path.abspath('../../plugins/')) # If extensions (or modules to document with autodoc) are in another directory, diff --git a/doc/source/keys.rst b/doc/source/keys.rst index ac4428e5..f1d47b91 100644 --- a/doc/source/keys.rst +++ b/doc/source/keys.rst @@ -249,7 +249,7 @@ To know exactly what the code of a key is, just run .. code-block:: bash - python3 src/keyboard.py + python3 poezio/keyboard.py And enter any key. diff --git a/launch.sh b/launch.sh index ca100524..267c90f4 100755 --- a/launch.sh +++ b/launch.sh @@ -25,5 +25,5 @@ else fi $PYTHON3 -c 'import sys;(print("Python 3.4 or newer is required") and exit(1)) if sys.version_info < (3, 4) else exit(0)' || exit 1 -exec "$PYTHON3" "$poezio_dir/src/poezio.py" -v "$args" "$@" +exec "$PYTHON3" "$poezio_dir/poezio/poezio.py" -v "$args" "$@" diff --git a/plugins/link.py b/plugins/link.py index d39d01b9..bca592a1 100644 --- a/plugins/link.py +++ b/plugins/link.py @@ -76,7 +76,7 @@ Options Set the default browser started by the plugin .. _Unix FIFO: https://en.wikipedia.org/wiki/Named_pipe -.. _daemon.py: http://dev.louiz.org/projects/poezio/repository/revisions/master/raw/src/daemon.py +.. _daemon.py: http://dev.louiz.org/projects/poezio/repository/revisions/master/raw/poezio/daemon.py """ import platform diff --git a/poezio/args.py b/poezio/args.py new file mode 100644 index 00000000..63e77927 --- /dev/null +++ b/poezio/args.py @@ -0,0 +1,28 @@ +""" +Module related to the argument parsing + +There is a fallback to the deprecated optparse if argparse is not found +""" +from os import path +from argparse import ArgumentParser, SUPPRESS + +def parse_args(CONFIG_PATH=''): + """ + Parse the arguments from the command line + """ + parser = ArgumentParser('poezio') + parser.add_argument("-c", "--check-config", dest="check_config", + action='store_true', + help='Check the config file') + parser.add_argument("-d", "--debug", dest="debug", + help="The file where debug will be written", + metavar="DEBUG_FILE") + parser.add_argument("-f", "--file", dest="filename", + default=path.join(CONFIG_PATH, 'poezio.cfg'), + help="The config file you want to use", + metavar="CONFIG_FILE") + parser.add_argument("-v", "--version", dest="version", + help=SUPPRESS, metavar="VERSION", + default="1.0-dev") + options = parser.parse_args() + return options diff --git a/poezio/bookmarks.py b/poezio/bookmarks.py new file mode 100644 index 00000000..c7d26a51 --- /dev/null +++ b/poezio/bookmarks.py @@ -0,0 +1,289 @@ +""" +Bookmarks module + +Therein the bookmark class is defined, representing one conference room. +This object is used to generate elements for both local and remote +bookmark storage. It can also parse xml Elements. + +This module also defines several functions for retrieving and updating +bookmarks, both local and remote. + +Poezio start scenario: + +- upon inital connection, poezio will disco#info the server +- the available storage methods will be stored in the available_storage dict + (either 'pep' or 'privatexml') +- if only one is available, poezio will set the use_bookmarks_method config option + to it. If both are, it will be set to 'privatexml' (or if it was previously set, the + value will be kept). +- it will then query the preferred storages for bookmarks and cache them locally + (Bookmark objects with a method='remote' attribute) + +Adding a remote bookmark: + +- New Bookmark object added to the list with storage='remote' +- All bookmarks are sent to the storage selected in use_bookmarks_method + if there was an error, the user is notified. + + +""" + +import functools +import logging + +from slixmpp.plugins.xep_0048 import Bookmarks, Conference, URL +from slixmpp import JID +from common import safeJID +from config import config + +log = logging.getLogger(__name__) + + +class Bookmark(object): + + def __init__(self, jid, name=None, autojoin=False, nick=None, password=None, method='local'): + self.jid = jid + self.name = name or jid + self.autojoin = autojoin + self.nick = nick + self.password = password + self._method = method + + @property + def method(self): + return self._method + + @method.setter + def method(self, value): + if value not in ('local', 'remote'): + log.debug('Could not set bookmark storing method: %s', value) + return + self._method = value + + def __repr__(self): + return '<%s%s|%s>' % (self.jid, + ('/'+self.nick) if self.nick else '', + self.method) + + def stanza(self): + """ + Generate a stanza from the instance + """ + el = Conference() + el['name'] = self.name + el['jid'] = self.jid + el['autojoin'] = 'true' if self.autojoin else 'false' + if self.nick: + el['nick'] = self.nick + if self.password: + el['password'] = self.password + return el + + def local(self): + """Generate a str for local storage""" + local = self.jid + if self.nick: + local += '/%s' % self.nick + local += ':' + if self.password: + config.set_and_save('password', self.password, section=self.jid) + return local + + @functools.singledispatch + @staticmethod + def parse(el): + """ + Generate a Bookmark object from a element + (this is a fallback for raw XML Elements) + """ + jid = el.get('jid') + name = el.get('name') + autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False + nick = None + for n in el.iter('nick'): + nick = n.text + password = None + for p in el.iter('password'): + password = p.text + + return Bookmark(jid, name, autojoin, nick, password, method='remote') + + @staticmethod + @parse.register(Conference) + def parse_from_stanza(el): + """ + Parse a Conference element into a Bookmark object + """ + jid = el['jid'] + autojoin = el['autojoin'] + password = el['password'] + nick = el['nick'] + name = el['name'] + return Bookmark(jid, name, autojoin, nick, password, method='remote') + +class BookmarkList(object): + + def __init__(self): + self.bookmarks = [] + preferred = config.get('use_bookmarks_method').lower() + if preferred not in ('pep', 'privatexml'): + preferred = 'privatexml' + self.preferred = preferred + self.available_storage = { + 'privatexml': False, + 'pep': False, + } + + def __getitem__(self, key): + if isinstance(key, (str, JID)): + for i in self.bookmarks: + if key == i.jid: + return i + else: + return self.bookmarks[key] + + def __in__(self, key): + if isinstance(key, (str, JID)): + for bookmark in self.bookmarks: + if bookmark.jid == key: + return True + else: + return key in self.bookmarks + return False + + def remove(self, key): + if isinstance(key, (str, JID)): + for i in self.bookmarks[:]: + if i.jid == key: + self.bookmarks.remove(i) + else: + self.bookmarks.remove(key) + + def __iter__(self): + return iter(self.bookmarks) + + def local(self): + return [bm for bm in self.bookmarks if bm.method == 'local'] + + def remote(self): + return [bm for bm in self.bookmarks if bm.method == 'remote'] + + def set(self, new): + self.bookmarks = new + + def append(self, bookmark): + bookmark_exists = self[bookmark.jid] + if not bookmark_exists: + self.bookmarks.append(bookmark) + else: + self.bookmarks.remove(bookmark_exists) + self.bookmarks.append(bookmark) + + def set_bookmarks_method(self, value): + if self.available_storage.get(value): + self.preferred = value + config.set_and_save('use_bookmarks_method', value) + + def save_remote(self, xmpp, callback): + """Save the remote bookmarks.""" + if not any(self.available_storage.values()): + return + method = 'xep_0049' if self.preferred == 'privatexml' else 'xep_0223' + + if method: + xmpp.plugin['xep_0048'].set_bookmarks(stanza_storage(self.bookmarks), + method=method, + callback=callback) + def save_local(self): + """Save the local bookmarks.""" + local = ''.join(bookmark.local() for bookmark in self if bookmark.method == 'local') + config.set_and_save('rooms', local) + + def save(self, xmpp, core=None, callback=None): + """Save all the bookmarks.""" + self.save_local() + def _cb(iq): + if callback: + callback(iq) + if iq["type"] == "error" and core: + core.information('Could not save remote bookmarks.', 'Error') + elif core: + core.information('Bookmarks saved', 'Info') + if config.get('use_remote_bookmarks'): + self.save_remote(xmpp, _cb) + + def get_pep(self, xmpp, callback): + """Add the remotely stored bookmarks via pep to the list.""" + def _cb(iq): + if iq['type'] == 'result': + for conf in iq['pubsub']['items']['item']['bookmarks']['conferences']: + if isinstance(conf, URL): + continue + b = Bookmark.parse(conf) + self.append(b) + if callback: + callback(iq) + + xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0223', callback=_cb) + + def get_privatexml(self, xmpp, callback): + """ + Fetch the remote bookmarks stored via privatexml. + """ + def _cb(iq): + if iq['type'] == 'result': + for conf in iq['private']['bookmarks']['conferences']: + b = Bookmark.parse(conf) + self.append(b) + if callback: + callback(iq) + + xmpp.plugin['xep_0048'].get_bookmarks(method='xep_0049', callback=_cb) + + def get_remote(self, xmpp, information, callback): + """Add the remotely stored bookmarks to the list.""" + force = config.get('force_remote_bookmarks') + if xmpp.anon or not (any(self.available_storage.values()) or force): + information('No remote bookmark storage available', 'Warning') + return + + if force and not any(self.available_storage.values()): + old_callback = callback + method = 'pep' if self.preferred == 'pep' else 'privatexml' + def new_callback(result): + if result['type'] != 'error': + self.available_storage[method] = True + old_callback(result) + else: + information('No remote bookmark storage available', 'Warning') + callback = new_callback + + if self.preferred == 'pep': + self.get_pep(xmpp, callback=callback) + else: + self.get_privatexml(xmpp, callback=callback) + + def get_local(self): + """Add the locally stored bookmarks to the list.""" + rooms = config.get('rooms') + if not rooms: + return + rooms = rooms.split(':') + for room in rooms: + jid = safeJID(room) + if jid.bare == '': + continue + if jid.resource != '': + nick = jid.resource + else: + nick = 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') + self.append(b) + +def stanza_storage(bookmarks): + """Generate a stanza with the conference elements.""" + storage = Bookmarks() + for b in (b for b in bookmarks if b.method == 'remote'): + storage.append(b.stanza()) + return storage diff --git a/poezio/common.py b/poezio/common.py new file mode 100644 index 00000000..a62c83f1 --- /dev/null +++ b/poezio/common.py @@ -0,0 +1,483 @@ +# Copyright 2010-2011 Florent Le Coz +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Various useful functions. +""" + +from sys import version_info +from datetime import datetime, timedelta +from slixmpp import JID, InvalidJID + +import base64 +import os +import mimetypes +import hashlib +import subprocess +import time +import string +import poezio_shlex as shlex + + +# Needed to avoid datetime.datetime.timestamp() +# on python < 3.3. Older versions do not get good dst detection. +OLD_PYTHON = (version_info.major + version_info.minor/10) < 3.3 + +ROOM_STATE_NONE = 11 +ROOM_STATE_CURRENT = 10 +ROOM_STATE_PRIVATE = 15 +ROOM_STATE_MESSAGE = 12 +ROOM_STATE_HL = 13 + +def get_base64_from_file(path): + """ + Convert the content of a file to base64 + + :param str path: The path of the file to convert. + :return: A tuple of (encoded data, mime type, sha1 hash) if + the file exists and does not exceeds the upper size limit of 16384. + :return: (None, None, error message) if it fails + :rtype: :py:class:`tuple` + + """ + if not os.path.isfile(path): + return (None, None, "File does not exist") + size = os.path.getsize(path) + if size > 16384: + return (None, None,"File is too big") + fdes = open(path, 'rb') + data = fdes.read() + encoded = base64.encodestring(data) + sha1 = hashlib.sha1(data).hexdigest() + mime_type = mimetypes.guess_type(path)[0] + return (encoded, mime_type, sha1) + +def get_output_of_command(command): + """ + Runs a command and returns its output. + + :param str command: The command to run. + :return: The output or None + :rtype: :py:class:`str` + """ + try: + return subprocess.check_output(command.split()).decode('utf-8').split('\n') + except subprocess.CalledProcessError: + return None + +def is_in_path(command, return_abs_path=False): + """ + Check if *command* is in the $PATH or not. + + :param str command: The command to be checked. + :param bool return_abs_path: Return the absolute path of the command instead + of True if the command is found. + :return: True if the command is found, the command path if the command is found + and *return_abs_path* is True, otherwise False. + + """ + for directory in os.getenv('PATH').split(os.pathsep): + try: + if command in os.listdir(directory): + if return_abs_path: + return os.path.join(directory, command) + else: + return True + except OSError: + # If the user has non directories in his path + pass + return False + +DISTRO_INFO = { + 'Arch Linux': '/etc/arch-release', + 'Aurox Linux': '/etc/aurox-release', + 'Conectiva Linux': '/etc/conectiva-release', + 'CRUX': '/usr/bin/crux', + 'Debian GNU/Linux': '/etc/debian_version', + 'Fedora Linux': '/etc/fedora-release', + 'Gentoo Linux': '/etc/gentoo-release', + 'Linux from Scratch': '/etc/lfs-release', + 'Mandrake Linux': '/etc/mandrake-release', + 'Slackware Linux': '/etc/slackware-version', + 'Solaris/Sparc': '/etc/release', + 'Source Mage': '/etc/sourcemage_version', + 'SUSE Linux': '/etc/SuSE-release', + 'Sun JDS': '/etc/sun-release', + 'PLD Linux': '/etc/pld-release', + 'Yellow Dog Linux': '/etc/yellowdog-release', + # many distros use the /etc/redhat-release for compatibility + # so Redhat is the last + 'Redhat Linux': '/etc/redhat-release' +} + +def get_os_info(): + """ + Returns a detailed and well formated string containing + informations about the operating system + + :rtype: str + """ + if os.name == 'posix': + executable = 'lsb_release' + params = ' --description --codename --release --short' + full_path_to_executable = is_in_path(executable, return_abs_path = True) + if full_path_to_executable: + command = executable + params + process = subprocess.Popen([command], shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + close_fds=True) + process.wait() + output = process.stdout.readline().decode('utf-8').strip() + # some distros put n/a in places, so remove those + output = output.replace('n/a', '').replace('N/A', '') + return output + + # lsb_release executable not available, so parse files + for distro_name in DISTRO_INFO: + path_to_file = DISTRO_INFO[distro_name] + if os.path.exists(path_to_file): + if os.access(path_to_file, os.X_OK): + # the file is executable (f.e. CRUX) + # yes, then run it and get the first line of output. + text = get_output_of_command(path_to_file)[0] + else: + fdes = open(path_to_file, encoding='utf-8') + text = fdes.readline().strip() # get only first line + fdes.close() + if path_to_file.endswith('version'): + # sourcemage_version and slackware-version files + # have all the info we need (name and version of distro) + if not os.path.basename(path_to_file).startswith( + 'sourcemage') or not\ + os.path.basename(path_to_file).startswith('slackware'): + text = distro_name + ' ' + text + elif path_to_file.endswith('aurox-release') or \ + path_to_file.endswith('arch-release'): + # file doesn't have version + text = distro_name + elif path_to_file.endswith('lfs-release'): + # file just has version + text = distro_name + ' ' + text + os_info = text.replace('\n', '') + return os_info + + # our last chance, ask uname and strip it + uname_output = get_output_of_command('uname -sr') + if uname_output is not None: + os_info = uname_output[0] # only first line + return os_info + os_info = 'N/A' + return os_info + +def datetime_tuple(timestamp): + """ + Convert a timestamp using strptime and the format: %Y%m%dT%H:%M:%S. + + Because various datetime formats are used, the following exceptions + are handled: + + * Optional milliseconds appened to the string are removed + * Optional Z (that means UTC) appened to the string are removed + * XEP-082 datetime strings have all '-' chars removed to meet the above format. + + :param str timestamp: The string containing the formatted date. + :return: The date. + :rtype: :py:class:`datetime.datetime` + """ + timestamp = timestamp.replace('-', '', 2).replace(':', '') + date = timestamp[:15] + tz_msg = timestamp[15:] + try: + ret = datetime.strptime(date, '%Y%m%dT%H%M%S') + except Exception: + ret = datetime.now() + # add the message timezone if any + try: + if tz_msg and tz_msg != 'Z': + tz_mod = -1 if tz_msg[0] == '-' else 1 + tz_msg = time.strptime(tz_msg[1:], '%H%M') + tz_msg = tz_msg.tm_hour * 3600 + tz_msg.tm_min * 60 + tz_msg = timedelta(seconds=tz_mod * tz_msg) + ret -= tz_msg + except Exception: + pass # ignore if we got a badly-formatted offset + # convert UTC to local time, with DST etc. + if time.daylight and time.localtime().tm_isdst: + tz = timedelta(seconds=-time.altzone) + else: + tz = timedelta(seconds=-time.timezone) + ret += tz + return ret + +def get_utc_time(local_time=None): + """ + Get the current UTC time + + :param datetime local_time: The current local time + :return: The current UTC time + """ + if local_time is None: + local_time = datetime.now() + isdst = time.localtime().tm_isdst + else: + if OLD_PYTHON: + isdst = time.localtime(int(local_time.strftime("%s"))).tm_isdst + else: + isdst = time.localtime(int(local_time.timestamp())).tm_isdst + + if time.daylight and isdst: + tz = timedelta(seconds=time.altzone) + else: + tz = timedelta(seconds=time.timezone) + + utc_time = local_time + tz + + return utc_time + +def get_local_time(utc_time): + """ + Get the local time from an UTC time + """ + if OLD_PYTHON: + isdst = time.localtime(int(utc_time.strftime("%s"))).tm_isdst + else: + isdst = time.localtime(int(utc_time.timestamp())).tm_isdst + + if time.daylight and isdst: + tz = timedelta(seconds=time.altzone) + else: + tz = timedelta(seconds=time.timezone) + + local_time = utc_time - tz + + return local_time + +def find_delayed_tag(message): + """ + Check if a message is delayed or not. + + :param slixmpp.Message message: The message to check. + :return: A tuple containing (True, the datetime) or (False, None) + :rtype: :py:class:`tuple` + """ + + delay_tag = message.find('{urn:xmpp:delay}delay') + if delay_tag is not None: + delayed = True + date = datetime_tuple(delay_tag.attrib['stamp']) + else: + # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html + # But it sucks, please, Jabber servers, don't do this :( + delay_tag = message.find('{jabber:x:delay}x') + if delay_tag is not None: + delayed = True + date = datetime_tuple(delay_tag.attrib['stamp']) + else: + delayed = False + date = None + return (delayed, date) + +def shell_split(st): + """ + Split a string correctly according to the quotes + around the elements. + + :param str st: The string to split. + :return: A list of the different of the string. + :rtype: :py:class:`list` + + >>> shell_split('"sdf 1" "toto 2"') + ['sdf 1', 'toto 2'] + """ + sh = shlex.shlex(st) + ret = [] + w = sh.get_token() + while w and w[2] is not None: + ret.append(w[2]) + if w[1] == len(st): + return ret + w = sh.get_token() + return ret + +def find_argument(pos, text, quoted=True): + """ + Split an input into a list of arguments, return the number of the + argument selected by pos. + + If the position searched is outside the string, or in a space between words, + then it will return the position of an hypothetical new argument. + + See the doctests of the two methods for example behaviors. + + :param int pos: The position to search. + :param str text: The text to analyze. + :param quoted: Whether to take quotes into account or not. + :rtype: int + """ + if quoted: + return find_argument_quoted(pos, text) + else: + return find_argument_unquoted(pos, text) + +def find_argument_quoted(pos, text): + """ + Get the number of the argument at position pos in + a string with possibly quoted text. + """ + sh = shlex.shlex(text) + count = -1 + w = sh.get_token() + while w and w[2] is not None: + count += 1 + if w[0] <= pos < w[1]: + return count + w = sh.get_token() + + return count + 1 + +def find_argument_unquoted(pos, text): + """ + Get the number of the argument at position pos in + a string without interpreting quotes. + """ + ret = text.split() + search = 0 + argnum = 0 + for i, elem in enumerate(ret): + elem_start = text.find(elem, search) + elem_end = elem_start + len(elem) + search = elem_end + if elem_start <= pos < elem_end: + return i + argnum = i + return argnum + 1 + +def parse_str_to_secs(duration=''): + """ + Parse a string of with a number of d, h, m, s. + + :param str duration: The formatted string. + :return: The number of seconds represented by the string + :rtype: :py:class:`int` + + >>> parse_str_to_secs("1d3m1h") + 90180 + """ + values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} + result = 0 + tmp = '0' + for char in duration: + if char in string.digits: + tmp += char + elif char in values: + tmp_i = int(tmp) + result += tmp_i * values[char] + tmp = '0' + else: + return 0 + if tmp != '0': + result += int(tmp) + return result + +def parse_secs_to_str(duration=0): + """ + Do the reverse operation of :py:func:`parse_str_to_secs`. + + Parse a number of seconds to a human-readable string. + The string has the form XdXhXmXs. 0 units are removed. + + :param int duration: The duration, in seconds. + :return: A formatted string containing the duration. + :rtype: :py:class:`str` + + >>> parse_secs_to_str(3601) + '1h1s' + """ + secs, mins, hours, days = 0, 0, 0, 0 + result = '' + secs = duration % 60 + mins = (duration % 3600) // 60 + hours = (duration % 86400) // 3600 + days = duration // 86400 + + result += '%sd' % days if days else '' + result += '%sh' % hours if hours else '' + result += '%sm' % mins if mins else '' + result += '%ss' % secs if secs else '' + if not result: + result = '0s' + return result + +def format_tune_string(infos): + """ + Contruct a string from a dict created from an "User tune" event. + + :param dict infos: The informations + :return: The formatted string + :rtype: :py:class:`str` + """ + elems = [] + track = infos.get('track') + if track: + elems.append(track) + title = infos.get('title') + if title: + elems.append(title) + else: + elems.append('Unknown title') + elems.append('-') + artist = infos.get('artist') + if artist: + elems.append(artist) + else: + elems.append('Unknown artist') + + rating = infos.get('rating') + if rating: + elems.append('[ ' + rating + '/10' + ' ]') + length = infos.get('length') + if length: + length = int(length) + secs = length % 60 + mins = length // 60 + secs = str(secs).zfill(2) + mins = str(mins).zfill(2) + elems.append('[' + mins + ':' + secs + ']') + return ' '.join(elems) + +def format_gaming_string(infos): + """ + Construct a string from a dict containing the "user gaming" + informations. + (for now, only use address and name) + + :param dict infos: The informations + :returns: The formatted string + :rtype: :py:class:`str` + """ + name = infos.get('name') + if not name: + return '' + + server_address = infos.get('server_address') + if server_address: + return '%s on %s' % (name, server_address) + return name + +def safeJID(*args, **kwargs): + """ + Construct a :py:class:`slixmpp.JID` object from a string. + + Used to avoid tracebacks during is stringprep fails + (fall back to a JID with an empty string). + """ + try: + return JID(*args, **kwargs) + except InvalidJID: + return JID('') + diff --git a/poezio/config.py b/poezio/config.py new file mode 100644 index 00000000..7f0c75f6 --- /dev/null +++ b/poezio/config.py @@ -0,0 +1,685 @@ +""" +Defines the global config instance, used to get or set (and save) values +from/to the config file. + +This module has the particularity that some imports and global variables +are delayed because it would mean doing an incomplete setup of the python +loggers. + +TODO: get http://bugs.python.org/issue1410680 fixed, one day, in order +to remove our ugly custom I/O methods. +""" + +DEFSECTION = "Poezio" + +import logging.config +import os +import stat +import sys +import pkg_resources + +from configparser import RawConfigParser, NoOptionError, NoSectionError +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': True, + 'autorejoin_delay': '5', + 'autorejoin': False, + 'beep_on': 'highlight private invite', + 'ca_cert_path': '', + 'certificate': '', + 'certfile': '', + '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': '', + 'deterministic_nick_colors': True, + 'nick_color_aliases': True, + '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': True, + '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, + 'eval_password': '', + 'exec_remote': False, + 'extract_inline_images': True, + 'filter_info_messages': '', + 'force_encryption': True, + 'force_remote_bookmarks': False, + 'go_to_previous_tab_on_alt_number': False, + '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': '', + 'keyfile': '', + 'lang': 'en', + 'lazy_resize': True, + 'load_log': 10, + 'log_dir': '', + '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, + 'self_ping_delay': 0, + '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_jid_in_conversations': 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': True, + '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 + }, + 'muc_colors': { + } +} + +class Config(RawConfigParser): + """ + load/save the config to a file + """ + 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: + RawConfigParser.read(self, self.file_name, encoding='utf-8') + except TypeError: # python < 3.2 sucks + RawConfigParser.read(self, self.file_name) + # Check config integrity and fix it if it’s wrong + # only when the object is the main config + if self.__class__ is Config: + for section in ('bindings', 'var'): + if not self.has_section(section): + self.add_section(section) + + 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) + elif type(default) == float: + res = self.getfloat(option, section) + elif type(default) == bool: + res = self.getboolean(option, section) + else: + 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, 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 + return self.get(option, default, tabname) + if fallback_server: + return self.get_by_servname(tabname, option, default, fallback) + if fallback: + # We fallback to the global option + return self.get(option, default) + return default + + def get_by_servname(self, jid, option, default, fallback=True): + """ + Try to get the value of an option for a server + """ + server = safeJID(jid).server + if server: + server = '@' + server + if server in self.sections() and option in self.options(server): + return self.get(option, default, server) + if fallback: + return self.get(option, default) + return default + + + def __get(self, option, section=DEFSECTION, **kwargs): + """ + facility for RawConfigParser.get + """ + return RawConfigParser.get(self, section, option, **kwargs) + + def _get(self, section, conv, option, **kwargs): + """ + Redirects RawConfigParser._get + """ + return conv(self.__get(option, section, **kwargs)) + + def getstr(self, option, section=DEFSECTION): + """ + get a value and returns it as a string + """ + return self.__get(option, section) + + def getint(self, option, section=DEFSECTION): + """ + get a value and returns it as an int + """ + return RawConfigParser.getint(self, section, option) + + def getfloat(self, option, section=DEFSECTION): + """ + get a value and returns it as a float + """ + return RawConfigParser.getfloat(self, section, option) + + def getboolean(self, option, section=DEFSECTION): + """ + get a value and returns it as a boolean + """ + return RawConfigParser.getboolean(self, section, option) + + def write_in_file(self, section, option, value): + """ + Our own way to save write the value in the file + Just find the right section, and then find the + right option, and edit it. + """ + result = self._parse_file() + if not result: + return False + else: + sections, result_lines = result + + if not section in sections: + result_lines.append('[%s]' % section) + result_lines.append('%s = %s' % (option, value)) + else: + begin, end = sections[section] + pos = find_line(result_lines, begin, end, option) + + if pos is -1: + result_lines.insert(end, '%s = %s' % (option, value)) + else: + result_lines[pos] = '%s = %s' % (option, value) + + return self._write_file(result_lines) + + def remove_in_file(self, section, option): + """ + Our own way to remove an option from the file. + """ + result = self._parse_file() + if not result: + return False + else: + sections, result_lines = result + + if not section in sections: + log.error('Tried to remove the option %s from a non-' + 'existing section (%s)', option, section) + return True + else: + begin, end = sections[section] + pos = find_line(result_lines, begin, end, option) + + if pos is -1: + log.error('Tried to remove a non-existing option %s' + ' from section %s', option, section) + return True + else: + del result_lines[pos] + + return self._write_file(result_lines) + + def _write_file(self, lines): + """ + Write the config file, write to a temporary file + before copying it to the final destination + """ + try: + prefix, file = path.split(self.file_name) + filename = path.join(prefix, '.%s.tmp' % file) + fd = os.fdopen( + os.open( + filename, + os.O_WRONLY | os.O_CREAT, + 0o600), + 'w') + for line in lines: + fd.write('%s\n' % line) + fd.close() + copy2(filename, self.file_name) + remove(filename) + except: + success = False + log.error('Unable to save the config file.', exc_info=True) + else: + success = True + return success + + def _parse_file(self): + """ + Parse the config file and return the list of sections with + their start and end positions, and the lines in the file. + + Duplicate sections are preserved but ignored for the parsing. + + Returns an empty tuple if reading fails + """ + if file_ok(self.file_name): + try: + with open(self.file_name, 'r', encoding='utf-8') as df: + lines_before = [line.strip() for line in df] + except: + log.error('Unable to read the config file %s', + self.file_name, + exc_info=True) + return tuple() + else: + lines_before = [] + + sections = {} + duplicate_section = False + current_section = '' + current_line = 0 + + for line in lines_before: + if line.startswith('['): + if not duplicate_section and current_section: + sections[current_section][1] = current_line + + duplicate_section = False + current_section = line[1:-1] + + if current_section in sections: + log.error('Error while reading the configuration file,' + ' skipping until next section') + duplicate_section = True + else: + sections[current_section] = [current_line, current_line] + + current_line += 1 + if not duplicate_section and current_section: + sections[current_section][1] = current_line + + return (sections, lines_before) + + def set_and_save(self, option, value, section=DEFSECTION): + """ + set the value in the configuration then save it + to the file + """ + # Special case for a 'toggle' value. We take the current value + # and set the opposite. Warning if the no current value exists + # or it is not a bool. + if value == "toggle": + current = self.get(option, "", section) + if isinstance(current, bool): + value = str(not current) + else: + if current.lower() == "false": + value = "true" + elif current.lower() == "true": + value = "false" + else: + return ('Could not toggle option: %s.' + ' Current value is %s.' % + (option, current or "empty"), + 'Warning') + if self.has_section(section): + RawConfigParser.set(self, section, option, value) + else: + self.add_section(section) + RawConfigParser.set(self, section, option, value) + if not self.write_in_file(section, option, value): + return ('Unable to write in the config file', 'Error') + return ("%s=%s" % (option, value), 'Info') + + def remove_and_save(self, option, section=DEFSECTION): + """ + Remove an option and then save it the config file + """ + if self.has_section(section): + RawConfigParser.remove_option(self, section, option) + if not self.remove_in_file(section, option): + return ('Unable to save the config file', 'Error') + return ('Option %s deleted' % option, 'Info') + + def silent_set(self, option, value, section=DEFSECTION): + """ + Set a value, save, and return True on success and False on failure + """ + if self.has_section(section): + RawConfigParser.set(self, section, option, value) + else: + self.add_section(section) + RawConfigParser.set(self, section, option, value) + return self.write_in_file(section, option, value) + + def set(self, option, value, section=DEFSECTION): + """ + Set the value of an option temporarily + """ + try: + RawConfigParser.set(self, section, option, value) + except NoSectionError: + pass + + def to_dict(self): + """ + Returns a dict of the form {section: {option: value, option: value}, …} + """ + res = {} + for section in self.sections(): + res[section] = {} + for option in self.options(section): + res[section][option] = self.get(option, "", section) + return res + + +def find_line(lines, start, end, option): + """ + Get the number of the line containing the option in the + relevant part of the config file. + + Returns -1 if the option isn’t found + """ + current = start + for line in lines[start:end]: + if (line.startswith('%s ' % option) or + line.startswith('%s=' % option)): + return current + current += 1 + return -1 + +def file_ok(filepath): + """ + Returns True if the file exists and is readable and writeable, + False otherwise. + """ + val = path.exists(filepath) + val &= os.access(filepath, os.R_OK | os.W_OK) + return bool(val) + +def check_create_config_dir(): + """ + create the configuration directory if it doesn't exist + """ + CONFIG_HOME = environ.get("XDG_CONFIG_HOME") + if not CONFIG_HOME: + CONFIG_HOME = path.join(environ.get('HOME'), '.config') + CONFIG_PATH = path.join(CONFIG_HOME, 'poezio') + + try: + makedirs(CONFIG_PATH) + except OSError: + 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 check_config(): + """ + Check the config file and print results + """ + result = {'missing': [], 'changed': []} + for option in DEFAULT_CONFIG['Poezio']: + value = config.get(option) + if value != DEFAULT_CONFIG['Poezio'][option]: + result['changed'].append((option, value, DEFAULT_CONFIG['Poezio'][option])) + else: + value = config.get(option, default='') + upper = value.upper() + default = str(DEFAULT_CONFIG['Poezio'][option]).upper() + if upper != default: + result['missing'].append(option) + + result['changed'].sort(key=lambda x: x[0]) + result['missing'].sort() + if result['changed']: + print('\033[1mOptions changed from the default configuration:\033[0m\n') + for option, new_value, default in result['changed']: + print(' \033[1m%s\033[0m = \033[33m%s\033[0m (default: \033[32m%s\033[0m)' % (option, new_value, default)) + + if result['missing']: + print('\n\033[1mMissing options:\033[0m (the defaults are used)\n') + for option in result['missing']: + print(' \033[31m%s\033[0m' % option) + +def run_cmdline_args(CONFIG_PATH): + "Parse the command line arguments" + global options + options = parse_args(CONFIG_PATH) + + # Copy a default file if none exists + if not path.isfile(options.filename): + default = path.join(path.dirname(__file__), '../data/default_config.cfg') + other = pkg_resources.resource_filename('poezio', 'default_config.cfg') + if path.isfile(default): + copy2(default, options.filename) + elif path.isfile(other): + copy2(other, options.filename) + + # Inside the nixstore and possibly other distributions, the reference + # file is readonly, so is the copy. + # Make it writable by the user who just created it. + if os.path.exists(options.filename): + os.chmod(options.filename, + os.stat(options.filename).st_mode | stat.S_IWUSR) + + global firstrun + firstrun = True + +def create_global_config(): + "Create the global config object, or crash" + try: + global config + config = Config(options.filename, DEFAULT_CONFIG) + except: + import traceback + sys.stderr.write('Poezio was unable to read or' + ' parse the config file.\n') + traceback.print_exc(limit=0) + sys.exit(1) + +def check_create_log_dir(): + "Create the poezio logging directory if it doesn’t exist" + global LOG_DIR + LOG_DIR = config.get('log_dir') + + if not LOG_DIR: + + data_dir = environ.get('XDG_DATA_HOME') + if not data_dir: + home = environ.get('HOME') + data_dir = path.join(home, '.local', 'share') + + LOG_DIR = path.join(data_dir, 'poezio', 'logs') + + LOG_DIR = path.expanduser(LOG_DIR) + + try: + makedirs(LOG_DIR) + except: + pass + +def setup_logging(): + "Change the logging config according to the cmdline options and config" + if config.get('log_errors'): + LOGGING_CONFIG['root']['handlers'].append('error') + LOGGING_CONFIG['handlers']['error'] = { + 'level': 'ERROR', + 'class': 'logging.FileHandler', + 'filename': path.join(LOG_DIR, 'errors.log'), + 'formatter': 'simple', + } + + if options.debug: + LOGGING_CONFIG['root']['handlers'].append('debug') + LOGGING_CONFIG['handlers']['debug'] = { + 'level':'DEBUG', + 'class':'logging.FileHandler', + 'filename': options.debug, + 'formatter': 'simple', + } + + + if LOGGING_CONFIG['root']['handlers']: + logging.config.dictConfig(LOGGING_CONFIG) + else: + logging.basicConfig(level=logging.CRITICAL) + + global log + log = logging.getLogger(__name__) + +def post_logging_setup(): + # common imports slixmpp, which creates then its loggers, so + # it needs to be after logger configuration + from common import safeJID as JID + global safeJID + safeJID = JID + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'simple': { + 'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s' + } + }, + 'handlers': { + }, + 'root': { + 'handlers': [], + 'propagate': True, + 'level': 'DEBUG', + } +} + +# True if this is the first run, in this case we will display +# some help in the info buffer +firstrun = False + +# Global config object. Is setup in poezio.py +config = None + +# The logger object for this module +log = None + +# The command-line options +options = None + +# delayed import from common.py +safeJID = None + +# the global log dir +LOG_DIR = '' + +# the global cache dir +CACHE_DIR = '' diff --git a/poezio/connection.py b/poezio/connection.py new file mode 100644 index 00000000..c4cc8b6b --- /dev/null +++ b/poezio/connection.py @@ -0,0 +1,223 @@ +# Copyright 2010-2011 Florent Le Coz +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Defines the Connection class +""" + +import logging +log = logging.getLogger(__name__) + + +import getpass +import subprocess +import sys + +import slixmpp +from slixmpp.plugins.xep_0184 import XEP_0184 + +import common +import fixes +from common import safeJID +from config import config, options + +class Connection(slixmpp.ClientXMPP): + """ + Receives everything from Jabber and emits the + appropriate signals + """ + __init = False + def __init__(self): + resource = config.get('resource') + + keyfile = config.get('keyfile') + certfile = config.get('certfile') + + 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') + if resource: + jid = '%s/%s'% (jid, resource) + password = config.get('password') + eval_password = config.get('eval_password') + if not password and not eval_password and not (keyfile and certfile): + password = getpass.getpass() + elif not password and not (keyfile and certfile): + sys.stderr.write("No password or certificates provided, using the eval_password command.\n") + process = subprocess.Popen(['sh', '-c', eval_password], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, close_fds=True) + code = process.wait() + if code != 0: + sys.stderr.write('The eval_password command (%s) returned a ' + 'nonzero status code: %s.\n' % (eval_password, code)) + sys.stderr.write('Poezio will now exit\n') + sys.exit(code) + password = process.stdout.readline().decode('utf-8').strip('\n') + else: # anonymous auth + self.anon = True + 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')) + + force_encryption = config.get('force_encryption') + if force_encryption: + self['feature_mechanisms'].unencrypted_plain = False + self['feature_mechanisms'].unencrypted_digest = False + self['feature_mechanisms'].unencrypted_cram = False + self['feature_mechanisms'].unencrypted_scram = False + + self.keyfile = config.get('keyfile') + self.certfile = config.get('certfile') + if keyfile and not certfile: + log.error('keyfile is present in configuration file without certfile') + elif certfile and not keyfile: + log.error('certfile is present in configuration file without keyfile') + + 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 + # for anything that came out after 2002 + 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') + if int(interval) > 0: + self.whitespace_keepalive_interval = int(interval) + else: + self.whitespace_keepalive = False + self.register_plugin('xep_0004') + self.register_plugin('xep_0012') + self.register_plugin('xep_0030') + self.register_plugin('xep_0045') + self.register_plugin('xep_0048') + self.register_plugin('xep_0050') + self.register_plugin('xep_0054') + self.register_plugin('xep_0060') + self.register_plugin('xep_0066') + self.register_plugin('xep_0071') + self.register_plugin('xep_0077') + self.plugin['xep_0077'].create_account = False + self.register_plugin('xep_0085') + self.register_plugin('xep_0115') + + # monkey-patch xep_0184 to avoid requesting receipts for messages + # 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') + self.plugin['xep_0184'].auto_request = config.get('request_message_receipts') + + self.register_plugin('xep_0191') + self.register_plugin('xep_0198') + self.register_plugin('xep_0199') + + if config.get('enable_user_tune'): + self.register_plugin('xep_0118') + + if config.get('enable_user_nick'): + self.register_plugin('xep_0172') + + if config.get('enable_user_mood'): + self.register_plugin('xep_0107') + + if config.get('enable_user_activity'): + self.register_plugin('xep_0108') + + if config.get('enable_user_gaming'): + self.register_plugin('xep_0196') + + if config.get('send_poezio_info'): + info = {'name':'poezio', + 'version': options.version} + if config.get('send_os_info'): + info['os'] = common.get_os_info() + self.plugin['xep_0030'].set_identities( + identities=set([('client', 'pc', None, 'Poezio')])) + else: + info = {'name': '', 'version': ''} + self.plugin['xep_0030'].set_identities( + identities=set([('client', 'pc', None, '')])) + self.register_plugin('xep_0092', pconfig=info) + if config.get('send_time'): + self.register_plugin('xep_0202') + self.register_plugin('xep_0224') + self.register_plugin('xep_0231') + self.register_plugin('xep_0249') + self.register_plugin('xep_0257') + self.register_plugin('xep_0280') + self.register_plugin('xep_0297') + self.register_plugin('xep_0308') + self.register_plugin('xep_0319') + self.register_plugin('xep_0334') + self.register_plugin('xep_0352') + self.init_plugins() + + def set_keepalive_values(self, option=None, value=None): + """ + Called after the XMPP session has been started, or triggered when one of + "connection_timeout_delay" and "connection_check_interval" options + is changed. Unload and reload the ping plugin, with the new values. + """ + if not self.is_connected(): + # 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') + 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 + # passed) + # 1 second is short, but, well + timeout_delay = 1 + self.plugin['xep_0199'].disable_keepalive() + # If the ping_interval is 0 or less, we just disable the keepalive + if ping_interval > 0: + self.plugin['xep_0199'].enable_keepalive(ping_interval, + timeout_delay) + + def start(self): + """ + Connect and process events. + """ + custom_host = config.get('custom_host') + custom_port = config.get('custom_port', 5222) + if custom_port == -1: + custom_port = 5222 + if custom_host: + self.connect((custom_host, custom_port)) + elif custom_port != 5222 and custom_port != -1: + self.connect((self.boundjid.host, custom_port)) + else: + self.connect() + + def send_raw(self, data): + """ + Overrides XMLStream.send_raw, with an event added + """ + if self.core: + self.core.outgoing_stanza(data) + slixmpp.ClientXMPP.send_raw(self, data) + +class MatchAll(slixmpp.xmlstream.matcher.base.MatcherBase): + """ + Callback to retrieve all the stanzas for the XML tab + """ + def match(self, xml): + "match everything" + return True diff --git a/poezio/contact.py b/poezio/contact.py new file mode 100644 index 00000000..c670e5bc --- /dev/null +++ b/poezio/contact.py @@ -0,0 +1,197 @@ +# Copyright 2010-2011 Florent Le Coz +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Defines the Resource and Contact classes, which are used in +the roster. +""" + +import logging +log = logging.getLogger(__name__) + +from common import safeJID +from collections import defaultdict + +class Resource(object): + """ + Defines a roster item. + It's a precise resource. + """ + def __init__(self, jid, data): + """ + data: the dict to use as a source + """ + self._jid = jid # Full jid + self._data = data + + @property + def jid(self): + return self._jid + + @property + def priority(self): + return self._data.get('priority') or 0 + + @property + def presence(self): + return self._data.get('show') or '' + + show = presence + + @property + def status(self): + return self._data.get('status') or '' + + def __repr__(self): + return '<%s>' % self._jid + + def __eq__(self, value): + if not isinstance(value, Resource): + return False + return self.jid == value.jid and self._data == value._data + +class Contact(object): + """ + This a way to gather multiple resources from the same bare JID. + This class contains zero or more Resource object and useful methods + to get the resource with the highest priority, etc + """ + def __init__(self, item): + """ + item: a slixmpp RosterItem pointing to that contact + """ + self.__item = item + self.folded_states = defaultdict(lambda: True) + self._name = '' + self.error = None + self.tune = {} + self.gaming = {} + self.mood = '' + self.activity = '' + + @property + def groups(self): + """Name of the groups the contact is in""" + return self.__item['groups'] or ['none'] + + @property + def bare_jid(self): + """The bare jid of the contact""" + return self.__item.jid + + @property + def name(self): + """The name of the contact or an empty string.""" + return self.__item['name'] or self._name or '' + + @name.setter + def name(self, value): + """Set the name of the contact with user nickname""" + self._name = value + + @property + def ask(self): + if self.__item['pending_out']: + return 'asked' + + @property + def pending_in(self): + """We received a subscribe stanza from this contact.""" + return self.__item['pending_in'] + + @pending_in.setter + def pending_in(self, value): + self.__item['pending_in'] = value + + @property + def pending_out(self): + """We sent a subscribe stanza to this contact.""" + return self.__item['pending_out'] + + @pending_out.setter + def pending_out(self, value): + self.__item['pending_out'] = value + + @property + def resources(self): + """List of the available resources as Resource objects""" + return (Resource( + '%s%s' % (self.bare_jid, ('/' + key) if key else ''), + self.__item.resources[key] + ) for key in self.__item.resources.keys()) + + @property + def subscription(self): + return self.__item['subscription'] + + def __contains__(self, value): + return value in self.__item.resources or safeJID(value).resource in self.__item.resources + + def __len__(self): + """Number of resources""" + return len(self.__item.resources) + + def __bool__(self): + """This contacts exists even when he has no resources""" + return True + + def __getitem__(self, key): + """Return the corresponding Resource object, or None""" + res = safeJID(key).resource + resources = self.__item.resources + item = resources.get(res, None) or resources.get(key, None) + return Resource(key, item) if item else None + + def subscribe(self): + """Subscribe to this JID""" + self.__item.subscribe() + + def authorize(self): + """Authorize this JID""" + self.__item.authorize() + + def unauthorize(self): + """Unauthorize this JID""" + self.__item.unauthorize() + + def unsubscribe(self): + """Unsubscribe from this JID""" + self.__item.unsubscribe() + + def get(self, key, default=None): + """Same as __getitem__, but with a configurable default""" + return self[key] or default + + def get_resources(self): + """Return all resources, sorted by priority """ + compare_resources = lambda x: x.priority + return sorted(self.resources, key=compare_resources, reverse=True) + + def get_highest_priority_resource(self): + """Return the resource with the highest priority""" + resources = self.get_resources() + if resources: + return resources[0] + return None + + def folded(self, group_name='none'): + """ + Return the Folded state of a contact for this group + """ + return self.folded_states[group_name] + + def toggle_folded(self, group='none'): + """ + Fold if it's unfolded, and vice versa + """ + self.folded_states[group] = not self.folded_states[group] + + def __repr__(self): + ret = '\n' diff --git a/poezio/core/__init__.py b/poezio/core/__init__.py new file mode 100644 index 00000000..6a82e2bb --- /dev/null +++ b/poezio/core/__init__.py @@ -0,0 +1,8 @@ +""" +Core class, splitted into smaller chunks +""" + +from . core import Core +from . structs import Command, Status, possible_show, DEPRECATED_ERRORS, \ + ERROR_AND_STATUS_CODES + diff --git a/poezio/core/commands.py b/poezio/core/commands.py new file mode 100644 index 00000000..a0a636c1 --- /dev/null +++ b/poezio/core/commands.py @@ -0,0 +1,999 @@ +""" +Global commands which are to be linked to the Core class +""" + +import logging + +log = logging.getLogger(__name__) + +import os +from datetime import datetime +from xml.etree import cElementTree as ET + +from slixmpp.xmlstream.stanzabase import StanzaBase +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath + +import common +import fixes +import pep +import tabs +from bookmarks import Bookmark +from common import safeJID +from config import config, DEFAULT_CONFIG, options as config_opts +import multiuserchat as muc +from plugin import PluginConfig +from roster import roster +from theming import dump_tuple, get_theme +from decorators import command_args_parser + +from . structs import Command, possible_show + + +@command_args_parser.quoted(0, 1) +def command_help(self, args): + """ + /help [command_name] + """ + if not args: + color = dump_tuple(get_theme().COLOR_HELP_COMMANDS) + acc = [] + buff = ['Global commands:'] + for command in self.commands: + if isinstance(self.commands[command], Command): + acc.append(' \x19%s}%s\x19o - %s' % ( + color, + command, + self.commands[command].short)) + else: + acc.append(' \x19%s}%s\x19o' % (color, command)) + acc = sorted(acc) + buff.extend(acc) + acc = [] + buff.append('Tab-specific commands:') + commands = self.current_tab().commands + for command in commands: + if isinstance(commands[command], Command): + acc.append(' \x19%s}%s\x19o - %s' % ( + color, + command, + commands[command].short)) + else: + acc.append(' \x19%s}%s\x19o' % (color, command)) + acc = sorted(acc) + buff.extend(acc) + + msg = '\n'.join(buff) + msg += "\nType /help to know what each command does" + else: + command = args[0].lstrip('/').strip() + + if command in self.current_tab().commands: + tup = self.current_tab().commands[command] + elif command in self.commands: + tup = self.commands[command] + else: + self.information('Unknown command: %s' % command, 'Error') + return + if isinstance(tup, Command): + msg = 'Usage: /%s %s\n' % (command, tup.usage) + msg += tup.desc + else: + msg = tup[1] + self.information(msg, 'Help') + +@command_args_parser.quoted(1) +def command_runkey(self, args): + """ + /runkey + """ + def replace_line_breaks(key): + "replace ^J with \n" + if key == '^J': + return '\n' + return key + if args is None: + return self.command_help('runkey') + char = args[0] + func = self.key_func.get(char, None) + if func: + func() + else: + res = self.do_command(replace_line_breaks(char), False) + if res: + self.refresh_window() + +@command_args_parser.quoted(1, 1, [None]) +def command_status(self, args): + """ + /status [msg] + """ + if args is None: + return self.command_help('status') + + if not args[0] in possible_show.keys(): + return self.command_help('status') + + show = possible_show[args[0]] + msg = args[1] + + pres = self.xmpp.make_presence() + if msg: + pres['status'] = msg + pres['type'] = show + self.events.trigger('send_normal_presence', pres) + pres.send() + current = self.current_tab() + is_muctab = isinstance(current, tabs.MucTab) + if is_muctab and current.joined and show in ('away', 'xa'): + current.send_chat_state('inactive') + for tab in self.tabs: + if isinstance(tab, tabs.MucTab) and tab.joined: + muc.change_show(self.xmpp, tab.name, tab.own_nick, show, msg) + if hasattr(tab, 'directed_presence'): + del tab.directed_presence + self.set_status(show, msg) + if is_muctab and current.joined and show not in ('away', 'xa'): + current.send_chat_state('active') + +@command_args_parser.quoted(1, 2, [None, None]) +def command_presence(self, args): + """ + /presence [type] [status] + """ + if args is None: + return self.command_help('presence') + + jid, type, status = args[0], args[1], args[2] + if jid == '.' and isinstance(self.current_tab(), tabs.ChatTab): + jid = self.current_tab().name + if type == 'available': + type = None + try: + pres = self.xmpp.make_presence(pto=jid, ptype=type, pstatus=status) + self.events.trigger('send_normal_presence', pres) + pres.send() + except: + self.information('Could not send directed presence', 'Error') + log.debug('Could not send directed presence to %s', jid, exc_info=True) + return + tab = self.get_tab_by_name(jid) + if tab: + if type in ('xa', 'away'): + tab.directed_presence = False + chatstate = 'inactive' + else: + tab.directed_presence = True + chatstate = 'active' + if tab == self.current_tab(): + tab.send_chat_state(chatstate, True) + if isinstance(tab, tabs.MucTab): + for private in tab.privates: + private.directed_presence = tab.directed_presence + if self.current_tab() in tab.privates: + self.current_tab().send_chat_state(chatstate, True) + +@command_args_parser.quoted(1) +def command_theme(self, args=None): + """/theme """ + if args is None: + return self.command_help('theme') + self.command_set('theme %s' % (args[0],)) + +@command_args_parser.quoted(1) +def command_win(self, args): + """ + /win + """ + if args is None: + return self.command_help('win') + + nb = args[0] + try: + nb = int(nb) + except ValueError: + pass + if self.current_tab_nb == nb: + return + self.previous_tab_nb = self.current_tab_nb + old_tab = self.current_tab() + if isinstance(nb, int): + if 0 <= nb < len(self.tabs): + if not self.tabs[nb]: + return + self.current_tab_nb = nb + else: + matchs = [] + for tab in self.tabs: + for name in tab.matching_names(): + if nb.lower() in name[1].lower(): + matchs.append((name[0], tab)) + self.current_tab_nb = tab.nb + if not matchs: + return + tab = min(matchs, key=lambda m: m[0])[1] + self.current_tab_nb = tab.nb + old_tab.on_lose_focus() + self.current_tab().on_gain_focus() + self.refresh_window() + +@command_args_parser.quoted(2) +def command_move_tab(self, args): + """ + /move_tab old_pos new_pos + """ + if args is None: + return self.command_help('move_tab') + + current_tab = self.current_tab() + if args[0] == '.': + args[0] = current_tab.nb + if args[1] == '.': + args[1] = current_tab.nb + + def get_nb_from_value(value): + "parse the cmdline to guess the tab the users wants" + ref = None + try: + ref = int(value) + except ValueError: + old_tab = None + for tab in self.tabs: + if not old_tab and value == tab.name: + old_tab = tab + if not old_tab: + self.information("Tab %s does not exist" % args[0], "Error") + return None + ref = old_tab.nb + return ref + old = get_nb_from_value(args[0]) + new = get_nb_from_value(args[1]) + if new is None or old is None: + return self.information('Unable to move the tab.', 'Info') + result = self.insert_tab(old, new) + if not result: + self.information('Unable to move the tab.', 'Info') + else: + self.current_tab_nb = self.tabs.index(current_tab) + self.refresh_window() + +@command_args_parser.quoted(0, 1) +def command_list(self, args): + """ + /list [server] + Opens a MucListTab containing the list of the room in the specified server + """ + if args is None: + return self.command_help('list') + elif args: + jid = safeJID(args[0]) + else: + if not isinstance(self.current_tab(), tabs.MucTab): + return self.information('Please provide a server', 'Error') + jid = safeJID(self.current_tab().name).server + list_tab = tabs.MucListTab(jid) + self.add_tab(list_tab, True) + cb = list_tab.on_muc_list_item_received + self.xmpp.plugin['xep_0030'].get_items(jid=jid, + callback=cb) + +@command_args_parser.quoted(1) +def command_version(self, args): + """ + /version + """ + def callback(res): + "Callback for /version" + if not res: + return self.information('Could not get the software' + ' version from %s' % jid, + 'Warning') + version = '%s is running %s version %s on %s' % ( + jid, + res.get('name') or 'an unknown software', + res.get('version') or 'unknown', + res.get('os') or 'an unknown platform') + self.information(version, 'Info') + + if args is None: + return self.command_help('version') + + jid = safeJID(args[0]) + if jid.resource or jid not in roster: + fixes.get_version(self.xmpp, jid, callback=callback) + elif jid in roster: + for resource in roster[jid].resources: + fixes.get_version(self.xmpp, resource.jid, callback=callback) + else: + fixes.get_version(self.xmpp, jid, callback=callback) + +@command_args_parser.quoted(0, 2) +def command_join(self, args): + """ + /join [room][/nick] [password] + """ + password = None + if len(args) == 0: + tab = self.current_tab() + if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)): + return + room = safeJID(tab.name).bare + nick = tab.own_nick + else: + if args[0].startswith('@'): # we try to join a server directly + server_root = True + info = safeJID(args[0][1:]) + else: + info = safeJID(args[0]) + server_root = False + if info == '' and len(args[0]) > 1 and args[0][0] == '/': + nick = args[0][1:] + elif info.resource == '': + nick = self.own_nick + else: + nick = info.resource + if info.bare == '': # happens with /join /nickname, which is OK + tab = self.current_tab() + if not isinstance(tab, tabs.MucTab): + return + room = tab.name + if nick == '': + nick = tab.own_nick + else: + room = info.bare + # no server is provided, like "/join hello": + # use the server of the current room if available + # check if the current room's name has a server + if room.find('@') == -1 and not server_root: + if isinstance(self.current_tab(), tabs.MucTab) and\ + self.current_tab().name.find('@') != -1: + domain = safeJID(self.current_tab().name).domain + room += '@%s' % domain + else: + room = args[0] + room = room.lower() + if room in self.pending_invites: + del self.pending_invites[room] + tab = self.get_tab_by_name(room, tabs.MucTab) + if tab is not None: + self.focus_tab_named(tab.name) + if tab.own_nick == nick and tab.joined: + self.information('/join: Nothing to do.', 'Info') + else: + tab.command_part('') + tab.own_nick = nick + tab.join() + + return + + if room.startswith('@'): + room = room[1:] + if len(args) == 2: # a password is provided + password = args[1] + if password is None: # try to use a saved password + password = config.get_by_tabname('password', room, fallback=False) + if tab is not None: + if password: + tab.password = password + tab.join() + else: + tab = self.open_new_room(room, nick, password=password) + tab.join() + + if tab.joined: + self.enable_private_tabs(room) + tab.state = "normal" + if tab == self.current_tab(): + tab.refresh() + self.doupdate() + +@command_args_parser.quoted(0, 2) +def command_bookmark_local(self, args): + """ + /bookmark_local [room][/nick] [password] + """ + if not args and not isinstance(self.current_tab(), tabs.MucTab): + return + password = args[1] if len(args) > 1 else None + jid = args[0] if args else None + + _add_bookmark(self, jid, True, password, 'local') + +@command_args_parser.quoted(0, 3) +def command_bookmark(self, args): + """ + /bookmark [room][/nick] [autojoin] [password] + """ + if not args and not isinstance(self.current_tab(), tabs.MucTab): + return + jid = args[0] if args else '' + password = args[2] if len(args) > 2 else None + + if not config.get('use_remote_bookmarks'): + return _add_bookmark(self, jid, True, password, 'local') + + if len(args) > 1: + autojoin = False if args[1].lower() != 'true' else True + else: + autojoin = True + + _add_bookmark(self, jid, autojoin, password, 'remote') + +def _add_bookmark(self, jid, autojoin, password, method): + nick = None + if not jid: + tab = self.current_tab() + roomname = tab.name + if tab.joined and tab.own_nick != self.own_nick: + nick = tab.own_nick + if password is None and tab.password is not None: + password = tab.password + elif jid == '*': + return _add_wildcard_bookmarks(self, method) + else: + info = safeJID(jid) + roomname, nick = info.bare, info.resource + if roomname == '': + if not isinstance(self.current_tab(), tabs.MucTab): + return + roomname = self.current_tab().name + bookmark = self.bookmarks[roomname] + if bookmark is None: + bookmark = Bookmark(roomname) + self.bookmarks.append(bookmark) + bookmark.method = method + bookmark.autojoin = autojoin + if nick: + bookmark.nick = nick + if password: + bookmark.password = password + def callback(iq): + if iq["type"] != "error": + self.information('Bookmark added.', 'Info') + else: + self.information("Could not add the bookmarks.", "Info") + self.bookmarks.save_local() + self.bookmarks.save_remote(self.xmpp, callback) + +def _add_wildcard_bookmarks(self, method): + new_bookmarks = [] + for tab in self.get_tabs(tabs.MucTab): + bookmark = self.bookmarks[tab.name] + if not bookmark: + bookmark = Bookmark(tab.name, autojoin=True, + method=method) + new_bookmarks.append(bookmark) + else: + bookmark.method = method + new_bookmarks.append(bookmark) + self.bookmarks.remove(bookmark) + new_bookmarks.extend(self.bookmarks.bookmarks) + self.bookmarks.set(new_bookmarks) + def _cb(iq): + if iq["type"] != "error": + self.information("Bookmarks saved.", "Info") + else: + self.information("Could not save the remote bookmarks.", "Info") + self.bookmarks.save_local() + self.bookmarks.save_remote(self.xmpp, _cb) + +@command_args_parser.ignored +def command_bookmarks(self): + """/bookmarks""" + tab = self.get_tab_by_name('Bookmarks', tabs.BookmarksTab) + old_tab = self.current_tab() + if tab: + self.current_tab_nb = tab.nb + else: + tab = tabs.BookmarksTab(self.bookmarks) + self.tabs.append(tab) + self.current_tab_nb = tab.nb + old_tab.on_lose_focus() + tab.on_gain_focus() + self.refresh_window() + +@command_args_parser.quoted(0, 1) +def command_remove_bookmark(self, args): + """/remove_bookmark [jid]""" + + def cb(success): + if success: + self.information('Bookmark deleted', 'Info') + else: + self.information('Error while deleting the bookmark', 'Error') + + if not args: + tab = self.current_tab() + if isinstance(tab, tabs.MucTab) and self.bookmarks[tab.name]: + self.bookmarks.remove(tab.name) + self.bookmarks.save(self.xmpp, callback=cb) + else: + self.information('No bookmark to remove', 'Info') + else: + if self.bookmarks[args[0]]: + self.bookmarks.remove(args[0]) + self.bookmarks.save(self.xmpp, callback=cb) + else: + self.information('No bookmark to remove', 'Info') + +@command_args_parser.quoted(0, 3) +def command_set(self, args): + """ + /set [module|][section]