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.
---
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 +++++++++
61 files changed, 21209 insertions(+)
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
(limited to 'poezio')
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]
")
+ text = ''.join(build)
+ return text.replace('\n', '
')
--
cgit v1.2.3