diff options
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | 2016-03-31 18:54:41 +0100 |
---|---|---|
committer | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> | 2016-06-11 20:49:43 +0100 |
commit | 332a5c2553db41de777473a1e1be9cd1522c9496 (patch) | |
tree | 3ee06a59f147ccc4009b35cccfbe2461bcd18310 /src | |
parent | cf44cf7cdec9fdb35caa372563d57e7045dc29dd (diff) | |
download | poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.gz poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.bz2 poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.xz poezio-332a5c2553db41de777473a1e1be9cd1522c9496.zip |
Move the src directory to poezio, for better cython compatibility.
Diffstat (limited to 'src')
62 files changed, 0 insertions, 21210 deletions
diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 9fdbcc02..00000000 --- a/src/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from poezio.poezio import main diff --git a/src/args.py b/src/args.py deleted file mode 100644 index 63e77927..00000000 --- a/src/args.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -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/src/bookmarks.py b/src/bookmarks.py deleted file mode 100644 index c7d26a51..00000000 --- a/src/bookmarks.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -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 <conference/> 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 <conference/> 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 <storage/> 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/src/common.py b/src/common.py deleted file mode 100644 index a62c83f1..00000000 --- a/src/common.py +++ /dev/null @@ -1,483 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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/src/config.py b/src/config.py deleted file mode 100644 index 7f0c75f6..00000000 --- a/src/config.py +++ /dev/null @@ -1,685 +0,0 @@ -""" -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/src/connection.py b/src/connection.py deleted file mode 100644 index c4cc8b6b..00000000 --- a/src/connection.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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/src/contact.py b/src/contact.py deleted file mode 100644 index c670e5bc..00000000 --- a/src/contact.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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 = '<Contact: %s' % self.bare_jid - for resource in self.resources: - ret += '\n\t\t%s' % resource - return ret + ' />\n' diff --git a/src/core/__init__.py b/src/core/__init__.py deleted file mode 100644 index 6a82e2bb..00000000 --- a/src/core/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -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/src/core/commands.py b/src/core/commands.py deleted file mode 100644 index a0a636c1..00000000 --- a/src/core/commands.py +++ /dev/null @@ -1,999 +0,0 @@ -""" -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 <command_name> 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 <key> - """ - 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 <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 <JID> [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 <theme name>""" - 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 <number> - """ - 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 <jid> - """ - 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] <option> [value] - """ - if args is None or len(args) == 0: - config_dict = config.to_dict() - lines = [] - theme = get_theme() - for section_name, section in config_dict.items(): - lines.append('\x19%(section_col)s}[%(section)s]\x19o' % - { - 'section': section_name, - 'section_col': dump_tuple(theme.COLOR_INFORMATION_TEXT), - }) - for option_name, option_value in section.items(): - lines.append('%s\x19%s}=\x19o%s' % (option_name, - dump_tuple(theme.COLOR_REVISIONS_MESSAGE), - option_value)) - info = ('Current options:\n%s' % '\n'.join(lines), 'Info') - elif len(args) == 1: - option = args[0] - value = config.get(option) - if value is None and '=' in option: - args = option.split('=', 1) - info = ('%s=%s' % (option, value), 'Info') - if len(args) == 2: - if '|' in args[0]: - plugin_name, section = args[0].split('|')[:2] - if not section: - section = plugin_name - option = args[1] - if not plugin_name in self.plugin_manager.plugins: - file_name = self.plugin_manager.plugins_conf_dir - file_name = os.path.join(file_name, plugin_name + '.cfg') - plugin_config = PluginConfig(file_name, plugin_name) - else: - plugin_config = self.plugin_manager.plugins[plugin_name].config - value = plugin_config.get(option, default='', section=section) - info = ('%s=%s' % (option, value), 'Info') - else: - possible_section = args[0] - if config.has_section(possible_section): - section = possible_section - option = args[1] - value = config.get(option, section=section) - info = ('%s=%s' % (option, value), 'Info') - else: - option = args[0] - value = args[1] - info = config.set_and_save(option, value) - self.trigger_configuration_change(option, value) - elif len(args) == 3: - if '|' in args[0]: - plugin_name, section = args[0].split('|')[:2] - if not section: - section = plugin_name - option = args[1] - value = args[2] - if not plugin_name in self.plugin_manager.plugins: - file_name = self.plugin_manager.plugins_conf_dir - file_name = os.path.join(file_name, plugin_name + '.cfg') - plugin_config = PluginConfig(file_name, plugin_name) - else: - plugin_config = self.plugin_manager.plugins[plugin_name].config - info = plugin_config.set_and_save(option, value, section) - else: - if args[0] == '.': - name = safeJID(self.current_tab().name).bare - if not name: - self.information('Invalid tab to use the "." argument.', - 'Error') - return - section = name - else: - section = args[0] - option = args[1] - value = args[2] - info = config.set_and_save(option, value, section) - self.trigger_configuration_change(option, value) - elif len(args) > 3: - return self.command_help('set') - self.information(*info) - -@command_args_parser.quoted(1, 2) -def command_set_default(self, args): - """ - /set_default [section] <option> - """ - if len(args) == 1: - option = args[0] - section = 'Poezio' - elif len(args) == 2: - section = args[0] - option = args[1] - else: - return self.command_help('set_default') - - default_config = DEFAULT_CONFIG.get(section, tuple()) - if option not in default_config: - info = ("Option %s has no default value" % (option), "Error") - return self.information(*info) - self.command_set('%s %s %s' % (section, option, default_config[option])) - -@command_args_parser.quoted(1) -def command_toggle(self, args): - """ - /toggle <option> - shortcut for /set <option> toggle - """ - if args is None: - return self.command_help('toggle') - - if args[0]: - self.command_set('%s toggle' % args[0]) - -@command_args_parser.quoted(1, 1) -def command_server_cycle(self, args): - """ - Do a /cycle on each room of the given server. - If none, do it on the current tab - """ - tab = self.current_tab() - message = "" - if args: - domain = args[0] - if len(args) == 2: - message = args[1] - else: - if isinstance(tab, tabs.MucTab): - domain = safeJID(tab.name).domain - else: - return self.information("No server specified", "Error") - for tab in self.get_tabs(tabs.MucTab): - if tab.name.endswith(domain): - if tab.joined: - muc.leave_groupchat(tab.core.xmpp, - tab.name, - tab.own_nick, - message) - tab.joined = False - if tab.name == domain: - self.command_join('"@%s/%s"' %(tab.name, tab.own_nick)) - else: - self.command_join('"%s/%s"' %(tab.name, tab.own_nick)) - -@command_args_parser.quoted(1) -def command_last_activity(self, args): - """ - /last_activity <jid> - """ - def callback(iq): - "Callback for the last activity" - if iq['type'] != 'result': - if iq['error']['type'] == 'auth': - self.information('You are not allowed to see the ' - 'activity of this contact.', - 'Error') - else: - self.information('Error retrieving the activity', 'Error') - return - seconds = iq['last_activity']['seconds'] - status = iq['last_activity']['status'] - from_ = iq['from'] - if not safeJID(from_).user: - msg = 'The uptime of %s is %s.' % ( - from_, - common.parse_secs_to_str(seconds)) - else: - msg = 'The last activity of %s was %s ago%s' % ( - from_, - common.parse_secs_to_str(seconds), - (' and his/her last status was %s' % status) if status else '') - self.information(msg, 'Info') - - if args is None: - return self.command_help('last_activity') - jid = safeJID(args[0]) - self.xmpp.plugin['xep_0012'].get_last_activity(jid, - callback=callback) - -@command_args_parser.quoted(0, 2) -def command_mood(self, args): - """ - /mood [<mood> [text]] - """ - if not args: - return self.xmpp.plugin['xep_0107'].stop() - - mood = args[0] - if mood not in pep.MOODS: - return self.information('%s is not a correct value for a mood.' - % mood, - 'Error') - if len(args) == 2: - text = args[1] - else: - text = None - self.xmpp.plugin['xep_0107'].publish_mood(mood, text, - callback=dumb_callback) - -@command_args_parser.quoted(0, 3) -def command_activity(self, args): - """ - /activity [<general> [specific] [text]] - """ - length = len(args) - if not length: - return self.xmpp.plugin['xep_0108'].stop() - - general = args[0] - if general not in pep.ACTIVITIES: - return self.information('%s is not a correct value for an activity' - % general, - 'Error') - specific = None - text = None - if length == 2: - if args[1] in pep.ACTIVITIES[general]: - specific = args[1] - else: - text = args[1] - elif length == 3: - specific = args[1] - text = args[2] - if specific and specific not in pep.ACTIVITIES[general]: - return self.information('%s is not a correct value ' - 'for an activity' % specific, - 'Error') - self.xmpp.plugin['xep_0108'].publish_activity(general, specific, text, - callback=dumb_callback) - -@command_args_parser.quoted(0, 2) -def command_gaming(self, args): - """ - /gaming [<game name> [server address]] - """ - if not args: - return self.xmpp.plugin['xep_0196'].stop() - - name = args[0] - if len(args) > 1: - address = args[1] - else: - address = None - return self.xmpp.plugin['xep_0196'].publish_gaming(name=name, - server_address=address, - callback=dumb_callback) - -@command_args_parser.quoted(2, 1, [None]) -def command_invite(self, args): - """/invite <to> <room> [reason]""" - - if args is None: - return self.command_help('invite') - - reason = args[2] - to = safeJID(args[0]) - room = safeJID(args[1]).bare - self.invite(to.full, room, reason=reason) - self.information('Invited %s to %s' % (to.bare, room), 'Info') - -@command_args_parser.quoted(1, 1, ['']) -def command_decline(self, args): - """/decline <room@server.tld> [reason]""" - if args is None: - return self.command_help('decline') - jid = safeJID(args[0]) - if jid.bare not in self.pending_invites: - return - reason = args[1] - del self.pending_invites[jid.bare] - self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, - self.pending_invites[jid.bare], - reason) - -### Commands without a completion in this class ### - -@command_args_parser.ignored -def command_invitations(self): - """/invitations""" - build = "" - for invite in self.pending_invites: - build += "%s by %s" % (invite, - safeJID(self.pending_invites[invite]).bare) - if self.pending_invites: - build = "You are invited to the following rooms:\n" + build - else: - build = "You do not have any pending invitations." - self.information(build, 'Info') - -@command_args_parser.quoted(0, 1, [None]) -def command_quit(self, args): - """ - /quit [message] - """ - if not self.xmpp.is_connected(): - self.exit() - return - - msg = args[0] - if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop() - self.save_config() - self.plugin_manager.disable_plugins() - self.disconnect(msg) - self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) - -@command_args_parser.quoted(0, 1, ['']) -def command_destroy_room(self, args): - """ - /destroy_room [JID] - """ - room = safeJID(args[0]).bare - if room: - muc.destroy_room(self.xmpp, room) - elif isinstance(self.current_tab(), tabs.MucTab) and not args[0]: - muc.destroy_room(self.xmpp, self.current_tab().general_jid) - else: - self.information('Invalid JID: "%s"' % args[0], 'Error') - -@command_args_parser.quoted(1, 1, ['']) -def command_bind(self, args): - """ - Bind a key. - """ - if args is None: - return self.command_help('bind') - - if not config.silent_set(args[0], args[1], section='bindings'): - self.information('Unable to write in the config file', 'Error') - - if args[1]: - self.information('%s is now bound to %s' % (args[0], args[1]), 'Info') - else: - self.information('%s is now unbound' % args[0], 'Info') - -@command_args_parser.raw -def command_rawxml(self, args): - """ - /rawxml <xml stanza> - """ - - if not args: - return - - stanza = args - try: - stanza = StanzaBase(self.xmpp, xml=ET.fromstring(stanza)) - if stanza.xml.tag == 'iq' and stanza.xml.attrib.get('type') in ('get', 'set'): - iq_id = stanza.xml.attrib.get('id') - if not iq_id: - iq_id = self.xmpp.new_id() - stanza['id'] = iq_id - - def iqfunc(iq): - "handler for an iq reply" - self.information('%s' % iq, 'Iq') - self.xmpp.remove_handler('Iq %s' % iq_id) - - self.xmpp.register_handler( - Callback('Iq %s' % iq_id, - StanzaPath('iq@id=%s' % iq_id), - iqfunc - ) - ) - log.debug('handler') - log.debug('%s %s', stanza.xml.tag, stanza.xml.attrib) - - stanza.send() - except: - self.information('Could not send custom stanza', 'Error') - log.debug('/rawxml: Could not send custom stanza (%s)', - repr(stanza), - exc_info=True) - - -@command_args_parser.quoted(1, 256) -def command_load(self, args): - """ - /load <plugin> [<otherplugin> …] - # TODO: being able to load more than 256 plugins at once, hihi. - """ - for plugin in args: - self.plugin_manager.load(plugin) - -@command_args_parser.quoted(1, 256) -def command_unload(self, args): - """ - /unload <plugin> [<otherplugin> …] - """ - for plugin in args: - self.plugin_manager.unload(plugin) - -@command_args_parser.ignored -def command_plugins(self): - """ - /plugins - """ - self.information("Plugins currently in use: %s" % - repr(list(self.plugin_manager.plugins.keys())), - 'Info') - -@command_args_parser.quoted(1, 1) -def command_message(self, args): - """ - /message <jid> [message] - """ - if args is None: - return self.command_help('message') - jid = safeJID(args[0]) - if not jid.user and not jid.domain and not jid.resource: - return self.information('Invalid JID.', 'Error') - tab = self.get_conversation_by_jid(jid.full, False, fallback_barejid=False) - muc = self.get_tab_by_name(jid.bare, typ=tabs.MucTab) - if not tab and not muc: - tab = self.open_conversation_window(jid.full, focus=True) - elif muc: - tab = self.get_tab_by_name(jid.full, typ=tabs.PrivateTab) - if tab: - self.focus_tab_named(tab.name) - else: - tab = self.open_private_window(jid.bare, jid.resource) - else: - self.focus_tab_named(tab.name) - if len(args) == 2: - tab.command_say(args[1]) - -@command_args_parser.ignored -def command_xml_tab(self): - """/xml_tab""" - xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab) - if not xml_tab: - tab = tabs.XMLTab() - self.add_tab(tab, True) - self.xml_tab = tab - -@command_args_parser.quoted(1) -def command_adhoc(self, args): - if not args: - return self.command_help('ad-hoc') - jid = safeJID(args[0]) - list_tab = tabs.AdhocCommandsListTab(jid) - self.add_tab(list_tab, True) - cb = list_tab.on_list_received - self.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False, - callback=cb) - -@command_args_parser.ignored -def command_self(self): - """ - /self - """ - status = self.get_status() - show, message = status.show, status.message - nick = self.own_nick - jid = self.xmpp.boundjid.full - info = ('Your JID is %s\nYour current status is "%s" (%s)' - '\nYour default nickname is %s\nYou are running poezio %s' % ( - jid, - message if message else '', - show if show else 'available', - nick, - config_opts.version)) - self.information(info, 'Info') - - -@command_args_parser.ignored -def command_reload(self): - """ - /reload - """ - self.reload_config() - -def dumb_callback(*args, **kwargs): - "mock callback" - diff --git a/src/core/completions.py b/src/core/completions.py deleted file mode 100644 index 9fd44f1b..00000000 --- a/src/core/completions.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Completions for the global commands -""" -import logging - -log = logging.getLogger(__name__) - -import os -from functools import reduce - -import common -import pep -import tabs -from common import safeJID -from config import config -from roster import roster - -from . structs import possible_show - - -def completion_help(self, the_input): - """Completion for /help.""" - commands = sorted(self.commands.keys()) + sorted(self.current_tab().commands.keys()) - return the_input.new_completion(commands, 1, quotify=False) - - -def completion_status(self, the_input): - """ - Completion of /status - """ - if the_input.get_argument_position() == 1: - return the_input.new_completion([status for status in possible_show], 1, ' ', quotify=False) - - -def completion_presence(self, the_input): - """ - Completion of /presence - """ - arg = the_input.get_argument_position() - if arg == 1: - return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=True) - elif arg == 2: - return the_input.auto_completion([status for status in possible_show], '', quotify=True) - - -def completion_theme(self, the_input): - """ Completion for /theme""" - themes_dir = config.get('themes_dir') - themes_dir = themes_dir or\ - os.path.join(os.environ.get('XDG_DATA_HOME') or\ - os.path.join(os.environ.get('HOME'), '.local', 'share'), - 'poezio', 'themes') - themes_dir = os.path.expanduser(themes_dir) - try: - names = os.listdir(themes_dir) - except OSError as e: - log.error('Completion for /theme failed', exc_info=True) - return - theme_files = [name[:-3] for name in names if name.endswith('.py') and name != '__init__.py'] - if not 'default' in theme_files: - theme_files.append('default') - return the_input.new_completion(theme_files, 1, '', quotify=False) - - -def completion_win(self, the_input): - """Completion for /win""" - l = [] - for tab in self.tabs: - l.extend(tab.matching_names()) - l = [i[1] for i in l] - return the_input.new_completion(l, 1, '', quotify=False) - - -def completion_join(self, the_input): - """ - Completion for /join - - Try to complete the MUC JID: - if only a resource is provided, complete with the default nick - if only a server is provided, complete with the rooms from the - disco#items of that server - if only a nodepart is provided, complete with the servers of the - current joined rooms - """ - n = the_input.get_argument_position(quoted=True) - args = common.shell_split(the_input.text) - if n != 1: - # we are not on the 1st argument of the command line - return False - if len(args) == 1: - args.append('') - jid = safeJID(args[1]) - if args[1].endswith('@') and not jid.user and not jid.server: - jid.user = args[1][:-1] - - relevant_rooms = [] - relevant_rooms.extend(sorted(self.pending_invites.keys())) - bookmarks = {str(elem.jid): False for elem in self.bookmarks} - for tab in self.get_tabs(tabs.MucTab): - name = tab.name - if name in bookmarks and not tab.joined: - bookmarks[name] = True - relevant_rooms.extend(sorted(room[0] for room in bookmarks.items() if room[1])) - - if the_input.last_completion: - return the_input.new_completion([], 1, quotify=True) - - if jid.user: - # we are writing the server: complete the server - serv_list = [] - for tab in self.get_tabs(tabs.MucTab): - if tab.joined: - serv_list.append('%s@%s'% (jid.user, safeJID(tab.name).host)) - serv_list.extend(relevant_rooms) - return the_input.new_completion(serv_list, 1, quotify=True) - elif args[1].startswith('/'): - # we completing only a resource - return the_input.new_completion(['/%s' % self.own_nick], 1, quotify=True) - else: - return the_input.new_completion(relevant_rooms, 1, quotify=True) - - -def completion_version(self, the_input): - """Completion for /version""" - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) - return the_input.new_completion(sorted(comp), 1, quotify=False) - - -def completion_list(self, the_input): - """Completion for /list""" - muc_serv_list = [] - for tab in self.get_tabs(tabs.MucTab): # TODO, also from an history - if tab.name not in muc_serv_list: - muc_serv_list.append(safeJID(tab.name).server) - if muc_serv_list: - return the_input.new_completion(muc_serv_list, 1, quotify=False) - - -def completion_move_tab(self, the_input): - """Completion for /move_tab""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - nodes = [tab.name for tab in self.tabs if tab] - nodes.remove('Roster') - return the_input.new_completion(nodes, 1, ' ', quotify=True) - - -def completion_runkey(self, the_input): - """ - Completion for /runkey - """ - list_ = [] - list_.extend(self.key_func.keys()) - list_.extend(self.current_tab().key_func.keys()) - return the_input.new_completion(list_, 1, quotify=False) - - -def completion_bookmark(self, the_input): - """Completion for /bookmark""" - args = common.shell_split(the_input.text) - n = the_input.get_argument_position(quoted=True) - - if n == 2: - return the_input.new_completion(['true', 'false'], 2, quotify=True) - if n >= 3: - return - - if len(args) == 1: - args.append('') - jid = safeJID(args[1]) - - if jid.server and (jid.resource or jid.full.endswith('/')): - tab = self.get_tab_by_name(jid.bare, tabs.MucTab) - nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick') - if not nick: - if not default in nicks: - nicks.append(default) - else: - if not nick in nicks: - nicks.append(nick) - jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return the_input.new_completion(jids_list, 1, quotify=True) - muc_list = [tab.name for tab in self.get_tabs(tabs.MucTab)] - muc_list.sort() - muc_list.append('*') - return the_input.new_completion(muc_list, 1, quotify=True) - - -def completion_remove_bookmark(self, the_input): - """Completion for /remove_bookmark""" - return the_input.new_completion([bm.jid for bm in self.bookmarks], 1, quotify=False) - - -def completion_decline(self, the_input): - """Completion for /decline""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - return the_input.auto_completion(sorted(self.pending_invites.keys()), 1, '', quotify=True) - - -def completion_bind(self, the_input): - n = the_input.get_argument_position() - if n == 1: - args = [key for key in self.key_func if not key.startswith('_')] - elif n == 2: - args = [key for key in self.key_func] - else: - return - - return the_input.new_completion(args, n, '', quotify=False) - - -def completion_message(self, the_input): - """Completion for /message""" - n = the_input.get_argument_position(quoted=True) - if n >= 2: - return - l = [] - for jid in roster.jids(): - if len(roster[jid]): - l.append(jid) - for resource in roster[jid].resources: - l.append(resource.jid) - return the_input.new_completion(l, 1, '', quotify=True) - - -def completion_invite(self, the_input): - """Completion for /invite""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) - comp = sorted(comp) - bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact])) - off = sorted(jid for jid in roster.jids() if jid not in bares) - comp = comp + bares + off - return the_input.new_completion(comp, n, quotify=True) - elif n == 2: - rooms = [] - for tab in self.get_tabs(tabs.MucTab): - if tab.joined: - rooms.append(tab.name) - rooms.sort() - return the_input.new_completion(rooms, n, '', quotify=True) - - -def completion_activity(self, the_input): - """Completion for /activity""" - n = the_input.get_argument_position(quoted=True) - args = common.shell_split(the_input.text) - if n == 1: - return the_input.new_completion(sorted(pep.ACTIVITIES.keys()), n, quotify=True) - elif n == 2: - if args[1] in pep.ACTIVITIES: - l = list(pep.ACTIVITIES[args[1]]) - l.remove('category') - l.sort() - return the_input.new_completion(l, n, quotify=True) - - -def completion_mood(self, the_input): - """Completion for /mood""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - return the_input.new_completion(sorted(pep.MOODS.keys()), 1, quotify=True) - - -def completion_last_activity(self, the_input): - """ - Completion for /last_activity <jid> - """ - n = the_input.get_argument_position(quoted=False) - if n >= 2: - return - comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), []) - return the_input.new_completion(sorted(comp), 1, '', quotify=False) - - -def completion_server_cycle(self, the_input): - """Completion for /server_cycle""" - serv_list = set() - for tab in self.get_tabs(tabs.MucTab): - serv = safeJID(tab.name).server - serv_list.add(serv) - return the_input.new_completion(sorted(serv_list), 1, ' ') - - -def completion_set(self, the_input): - """Completion for /set""" - args = common.shell_split(the_input.text) - n = the_input.get_argument_position(quoted=True) - if n >= len(args): - args.append('') - if n == 1: - if '|' in args[1]: - plugin_name, section = args[1].split('|')[:2] - if not plugin_name in self.plugin_manager.plugins: - return the_input.new_completion([], n, quotify=True) - plugin = self.plugin_manager.plugins[plugin_name] - end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()] - else: - end_list = set(config.options('Poezio')) - end_list.update(config.default.get('Poezio', {})) - end_list = list(end_list) - end_list.sort() - elif n == 2: - if '|' in args[1]: - plugin_name, section = args[1].split('|')[:2] - if not plugin_name in self.plugin_manager.plugins: - return the_input.new_completion([''], n, quotify=True) - plugin = self.plugin_manager.plugins[plugin_name] - end_list = set(plugin.config.options(section or plugin_name)) - if plugin.config.default: - end_list.update(plugin.config.default.get(section or plugin_name, {})) - end_list = list(end_list) - end_list.sort() - elif not config.has_option('Poezio', args[1]): - if config.has_section(args[1]): - end_list = config.options(args[1]) - end_list.append('') - else: - end_list = [] - else: - end_list = [str(config.get(args[1], '')), ''] - elif n == 3: - if '|' in args[1]: - plugin_name, section = args[1].split('|')[:2] - if not plugin_name in self.plugin_manager.plugins: - return the_input.new_completion([''], n, quotify=True) - plugin = self.plugin_manager.plugins[plugin_name] - end_list = [str(plugin.config.get(args[2], '', section or plugin_name)), ''] - else: - if not config.has_section(args[1]): - end_list = [''] - else: - end_list = [str(config.get(args[2], '', args[1])), ''] - else: - return - return the_input.new_completion(end_list, n, quotify=True) - - -def completion_set_default(self, the_input): - """ Completion for /set_default - """ - args = common.shell_split(the_input.text) - n = the_input.get_argument_position(quoted=True) - if n >= len(args): - args.append('') - if n == 1 or (n == 2 and config.has_section(args[1])): - return self.completion_set(the_input) - return [] - - -def completion_toggle(self, the_input): - "Completion for /toggle" - return the_input.new_completion(config.options('Poezio'), 1, quotify=False) - - -def completion_bookmark_local(self, the_input): - """Completion for /bookmark_local""" - n = the_input.get_argument_position(quoted=True) - args = common.shell_split(the_input.text) - - if n >= 2: - return - if len(args) == 1: - args.append('') - jid = safeJID(args[1]) - - if jid.server and (jid.resource or jid.full.endswith('/')): - tab = self.get_tab_by_name(jid.bare, tabs.MucTab) - nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick') - if not nick: - if not default in nicks: - nicks.append(default) - else: - if not nick in nicks: - nicks.append(nick) - jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return the_input.new_completion(jids_list, 1, quotify=True) - muc_list = [tab.name for tab in self.get_tabs(tabs.MucTab)] - muc_list.append('*') - return the_input.new_completion(muc_list, 1, quotify=True) - diff --git a/src/core/core.py b/src/core/core.py deleted file mode 100644 index f32099f1..00000000 --- a/src/core/core.py +++ /dev/null @@ -1,2102 +0,0 @@ -""" -Module defining the Core class, which is the central orchestrator -of poezio and contains the main loop, the list of tabs, sets the state -of everything; it also contains global commands, completions and event -handlers but those are defined in submodules in order to avoir cluttering -this file. -""" -import logging - -log = logging.getLogger(__name__) - -import asyncio -import shutil -import curses -import os -import pipes -import sys -import time - -from slixmpp.xmlstream.handler import Callback - -import connection -import decorators -import events -import singleton -import tabs -import theming -import timed_events -import windows - -from bookmarks import BookmarkList -from common import safeJID -from config import config, firstrun -from contact import Contact, Resource -from daemon import Executor -from fifo import Fifo -from logger import logger -from plugin_manager import PluginManager -from roster import roster -from size_manager import SizeManager -from text_buffer import TextBuffer -from theming import get_theme -import keyboard - -from . import completions -from . import commands -from . import handlers -from . structs import possible_show, DEPRECATED_ERRORS, \ - ERROR_AND_STATUS_CODES, Command, Status - - -class Core(object): - """ - “Main” class of poezion - """ - - def __init__(self): - # All uncaught exception are given to this callback, instead - # of being displayed on the screen and exiting the program. - sys.excepthook = self.on_exception - self.connection_time = time.time() - self.stdscr = None - status = config.get('status') - status = possible_show.get(status, None) - self.status = Status(show=status, - message=config.get('status_message')) - self.running = True - self.xmpp = singleton.Singleton(connection.Connection) - self.xmpp.core = self - self.keyboard = keyboard.Keyboard() - roster.set_node(self.xmpp.client_roster) - decorators.refresh_wrapper.core = self - self.bookmarks = BookmarkList() - self.debug = False - self.remote_fifo = None - # a unique buffer used to store global informations - # that are displayed in almost all tabs, in an - # information window. - self.information_buffer = TextBuffer() - self.information_win_size = config.get('info_win_height', section='var') - self.information_win = windows.TextWin(300) - self.information_buffer.add_window(self.information_win) - self.left_tab_win = None - - self.tab_win = windows.GlobalInfoBar() - # Whether the XML tab is opened - self.xml_tab = None - self.xml_buffer = TextBuffer() - - self.tabs = [] - self._current_tab_nb = 0 - self.previous_tab_nb = 0 - - own_nick = config.get('default_nick') - own_nick = own_nick or self.xmpp.boundjid.user - own_nick = own_nick or os.environ.get('USER') - own_nick = own_nick or 'poezio' - self.own_nick = own_nick - - self.plugins_autoloaded = False - self.plugin_manager = PluginManager(self) - self.events = events.EventHandler() - - self.size = SizeManager(self, windows.Win) - - # Set to True whenever we consider that we have been disconnected - # from the server because of a legitimate reason (bad credentials, - # or explicit disconnect from the user for example), in that case we - # should not try to auto-reconnect, even if auto_reconnect is true - # in the user config. - self.legitimate_disconnect = False - - # global commands, available from all tabs - # a command is tuple of the form: - # (the function executing the command. Takes a string as argument, - # a string representing the help message, - # a completion function, taking a Input as argument. Can be None) - # The completion function should return True if a completion was - # made ; False otherwise - self.commands = {} - self.register_initial_commands() - - # We are invisible - if not config.get('send_initial_presence'): - del self.commands['status'] - del self.commands['show'] - - # A list of integers. For example if the user presses Alt+j, 2, 1, - # we will insert 2, then 1 in that list, and we will finally build - # the number 21 and use it with command_win, before clearing the - # list. - self.room_number_jump = [] - self.key_func = KeyDict() - # Key bindings associated with handlers - # and pseudo-keys used to map actions below. - key_func = { - "KEY_PPAGE": self.scroll_page_up, - "KEY_NPAGE": self.scroll_page_down, - "^B": self.scroll_line_up, - "^F": self.scroll_line_down, - "^X": self.scroll_half_down, - "^S": self.scroll_half_up, - "KEY_F(5)": self.rotate_rooms_left, - "^P": self.rotate_rooms_left, - "M-[-D": self.rotate_rooms_left, - 'kLFT3': self.rotate_rooms_left, - "KEY_F(6)": self.rotate_rooms_right, - "^N": self.rotate_rooms_right, - "M-[-C": self.rotate_rooms_right, - 'kRIT3': self.rotate_rooms_right, - "KEY_F(4)": self.toggle_left_pane, - "KEY_F(7)": self.shrink_information_win, - "KEY_F(8)": self.grow_information_win, - "KEY_RESIZE": self.call_for_resize, - 'M-e': self.go_to_important_room, - 'M-r': self.go_to_roster, - 'M-z': self.go_to_previous_tab, - '^L': self.full_screen_redraw, - 'M-j': self.go_to_room_number, - 'M-D': self.scroll_info_up, - 'M-C': self.scroll_info_down, - 'M-k': self.escape_next_key, - ######## actions mappings ########## - '_bookmark': self.command_bookmark, - '_bookmark_local': self.command_bookmark_local, - '_close_tab': self.close_tab, - '_disconnect': self.disconnect, - '_quit': self.command_quit, - '_redraw_screen': self.full_screen_redraw, - '_reload_theme': self.command_theme, - '_remove_bookmark': self.command_remove_bookmark, - '_room_left': self.rotate_rooms_left, - '_room_right': self.rotate_rooms_right, - '_show_roster': self.go_to_roster, - '_scroll_down': self.scroll_page_down, - '_scroll_up': self.scroll_page_up, - '_scroll_info_up': self.scroll_info_up, - '_scroll_info_down': self.scroll_info_down, - '_server_cycle': self.command_server_cycle, - '_show_bookmarks': self.command_bookmarks, - '_show_important_room': self.go_to_important_room, - '_show_invitations': self.command_invitations, - '_show_plugins': self.command_plugins, - '_show_xmltab': self.command_xml_tab, - '_toggle_pane': self.toggle_left_pane, - ###### status actions ###### - '_available': lambda: self.command_status('available'), - '_away': lambda: self.command_status('away'), - '_chat': lambda: self.command_status('chat'), - '_dnd': lambda: self.command_status('dnd'), - '_xa': lambda: self.command_status('xa'), - ##### Custom actions ######## - '_exc_': self.try_execute, - } - self.key_func.update(key_func) - - # Add handlers - self.xmpp.add_event_handler('connecting', self.on_connecting) - self.xmpp.add_event_handler('connected', self.on_connected) - self.xmpp.add_event_handler('connection_failed', self.on_failed_connection) - self.xmpp.add_event_handler('disconnected', self.on_disconnected) - self.xmpp.add_event_handler('stream_error', self.on_stream_error) - self.xmpp.add_event_handler('failed_all_auth', self.on_failed_all_auth) - self.xmpp.add_event_handler('no_auth', self.on_no_auth) - self.xmpp.add_event_handler("session_start", self.on_session_start) - self.xmpp.add_event_handler("session_start", - self.on_session_start_features) - self.xmpp.add_event_handler("groupchat_presence", - self.on_groupchat_presence) - self.xmpp.add_event_handler("groupchat_message", - self.on_groupchat_message) - self.xmpp.add_event_handler("groupchat_invite", - self.on_groupchat_invitation) - self.xmpp.add_event_handler("groupchat_direct_invite", - self.on_groupchat_direct_invitation) - self.xmpp.add_event_handler("groupchat_decline", - self.on_groupchat_decline) - self.xmpp.add_event_handler("groupchat_config_status", - self.on_status_codes) - self.xmpp.add_event_handler("groupchat_subject", - self.on_groupchat_subject) - self.xmpp.add_event_handler("message", self.on_message) - self.xmpp.add_event_handler("message_error", self.on_error_message) - self.xmpp.add_event_handler("receipt_received", self.on_receipt) - self.xmpp.add_event_handler("got_online", self.on_got_online) - self.xmpp.add_event_handler("got_offline", self.on_got_offline) - self.xmpp.add_event_handler("roster_update", self.on_roster_update) - self.xmpp.add_event_handler("changed_status", self.on_presence) - self.xmpp.add_event_handler("presence_error", self.on_presence_error) - self.xmpp.add_event_handler("roster_subscription_request", - self.on_subscription_request) - self.xmpp.add_event_handler("roster_subscription_authorized", - self.on_subscription_authorized) - self.xmpp.add_event_handler("roster_subscription_remove", - self.on_subscription_remove) - self.xmpp.add_event_handler("roster_subscription_removed", - self.on_subscription_removed) - self.xmpp.add_event_handler("message_xform", self.on_data_form) - self.xmpp.add_event_handler("chatstate_active", - self.on_chatstate_active) - self.xmpp.add_event_handler("chatstate_composing", - self.on_chatstate_composing) - self.xmpp.add_event_handler("chatstate_paused", - self.on_chatstate_paused) - self.xmpp.add_event_handler("chatstate_gone", - self.on_chatstate_gone) - self.xmpp.add_event_handler("chatstate_inactive", - self.on_chatstate_inactive) - self.xmpp.add_event_handler("attention", self.on_attention) - self.xmpp.add_event_handler("ssl_cert", self.validate_ssl) - self.xmpp.add_event_handler("ssl_invalid_chain", self.ssl_invalid_chain) - self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) - self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent) - - self.all_stanzas = Callback('custom matcher', - connection.MatchAll(None), - self.incoming_stanza) - self.xmpp.register_handler(self.all_stanzas) - if config.get('enable_user_tune'): - self.xmpp.add_event_handler("user_tune_publish", - self.on_tune_event) - if config.get('enable_user_nick'): - self.xmpp.add_event_handler("user_nick_publish", - self.on_nick_received) - if config.get('enable_user_mood'): - self.xmpp.add_event_handler("user_mood_publish", - self.on_mood_event) - if config.get('enable_user_activity'): - self.xmpp.add_event_handler("user_activity_publish", - self.on_activity_event) - if config.get('enable_user_gaming'): - self.xmpp.add_event_handler("user_gaming_publish", - self.on_gaming_event) - - self.initial_joins = [] - - self.connected_events = {} - - self.pending_invites = {} - - # a dict of the form {'config_option': [list, of, callbacks]} - # Whenever a configuration option is changed (using /set or by - # reloading a new config using a signal), all the associated - # callbacks are triggered. - # Use Core.add_configuration_handler("option", callback) to add a - # handler - # Note that the callback will be called when it’s changed in the - # global section, OR in a special section. - # As a special case, handlers can be associated with the empty - # string option (""), they will be called for every option change - # The callback takes two argument: the config option, and the new - # value - self.configuration_change_handlers = {"": []} - self.add_configuration_handler("create_gaps", - self.on_gaps_config_change) - self.add_configuration_handler("request_message_receipts", - self.on_request_receipts_config_change) - self.add_configuration_handler("ack_message_receipts", - self.on_ack_receipts_config_change) - self.add_configuration_handler("plugins_dir", - self.on_plugins_dir_config_change) - self.add_configuration_handler("plugins_conf_dir", - self.on_plugins_conf_dir_config_change) - self.add_configuration_handler("connection_timeout_delay", - self.xmpp.set_keepalive_values) - self.add_configuration_handler("connection_check_interval", - self.xmpp.set_keepalive_values) - self.add_configuration_handler("themes_dir", - theming.update_themes_dir) - self.add_configuration_handler("theme", - self.on_theme_config_change) - self.add_configuration_handler("use_bookmarks_method", - self.on_bookmarks_method_config_change) - self.add_configuration_handler("password", - self.on_password_change) - self.add_configuration_handler("enable_vertical_tab_list", - self.on_vertical_tab_list_config_change) - self.add_configuration_handler("vertical_tab_list_size", - self.on_vertical_tab_list_config_change) - self.add_configuration_handler("deterministic_nick_colors", - self.on_nick_determinism_changed) - self.add_configuration_handler("enable_carbons", - self.on_carbons_switch) - self.add_configuration_handler("hide_user_list", - self.on_hide_user_list_change) - - self.add_configuration_handler("", self.on_any_config_change) - - def on_any_config_change(self, option, value): - """ - Update the roster, in case a roster option changed. - """ - roster.modified() - - def add_configuration_handler(self, option, callback): - """ - Add a callback, associated with the given option. It will be called - each time the configuration option is changed using /set or by - reloading the configuration with a signal - """ - if option not in self.configuration_change_handlers: - self.configuration_change_handlers[option] = [] - self.configuration_change_handlers[option].append(callback) - - def trigger_configuration_change(self, option, value): - """ - Triggers all the handlers associated with the given configuration - option - """ - # First call the callbacks associated with any configuration change - for callback in self.configuration_change_handlers[""]: - callback(option, value) - # and then the callbacks associated with this specific option, if - # any - if option not in self.configuration_change_handlers: - return - for callback in self.configuration_change_handlers[option]: - callback(option, value) - - def on_hide_user_list_change(self, option, value): - """ - Called when the hide_user_list option changes - """ - self.call_for_resize() - - def on_bookmarks_method_config_change(self, option, value): - """ - Called when the use_bookmarks_method option changes - """ - if value not in ('pep', 'privatexml'): - return - self.bookmarks.preferred = value - self.bookmarks.save(self.xmpp, core=self) - - def on_gaps_config_change(self, option, value): - """ - Called when the option create_gaps is changed. - Remove all gaptabs if switching from gaps to nogaps. - """ - if value.lower() == "false": - self.tabs = list(tab for tab in self.tabs if tab) - - def on_request_receipts_config_change(self, option, value): - """ - Called when the request_message_receipts option changes - """ - self.xmpp.plugin['xep_0184'].auto_request = config.get(option, - default=True) - - def on_ack_receipts_config_change(self, option, value): - """ - Called when the ack_message_receipts option changes - """ - self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, default=True) - - def on_plugins_dir_config_change(self, option, value): - """ - Called when the plugins_dir option is changed - """ - path = os.path.expanduser(value) - self.plugin_manager.on_plugins_dir_change(path) - - def on_vertical_tab_list_config_change(self, option, value): - """ - Called when the enable_vertical_tab_list option is changed - """ - self.call_for_resize() - - def on_plugins_conf_dir_config_change(self, option, value): - """ - Called when the plugins_conf_dir option is changed - """ - path = os.path.expanduser(value) - self.plugin_manager.on_plugins_conf_dir_change(path) - - def on_theme_config_change(self, option, value): - """ - Called when the theme option is changed - """ - error_msg = theming.reload_theme() - if error_msg: - self.information(error_msg, 'Warning') - self.refresh_window() - - def on_password_change(self, option, value): - """ - Set the new password in the slixmpp.ClientXMPP object - """ - self.xmpp.password = value - - - def on_nick_determinism_changed(self, option, value): - """If we change the value to true, we call /recolor on all the MucTabs, to - make the current nick colors reflect their deterministic value. - """ - if value.lower() == "true": - for tab in self.get_tabs(tabs.MucTab): - tab.command_recolor('') - - def on_carbons_switch(self, option, value): - """Whenever the user enables or disables carbons using /set, we should - inform the server immediately, this way we do not require a restart - for the change to take effect - """ - if value: - self.xmpp.plugin['xep_0280'].enable() - else: - self.xmpp.plugin['xep_0280'].disable() - - def reload_config(self): - # reload all log files - log.debug("Reloading the log files…") - logger.reload_all() - log.debug("Log files reloaded.") - # reload the theme - log.debug("Reloading the theme…") - theming.reload_theme() - log.debug("Theme reloaded.") - # reload the config from the disk - log.debug("Reloading the config…") - # Copy the old config in a dict - old_config = config.to_dict() - config.read_file() - # Compare old and current config, to trigger the callbacks of all - # modified options - for section in config.sections(): - old_section = old_config.get(section, {}) - for option in config.options(section): - old_value = old_section.get(option) - new_value = config.get(option, default="", section=section) - if new_value != old_value: - self.trigger_configuration_change(option, new_value) - log.debug("Config reloaded.") - # in case some roster options have changed - roster.modified() - - def sigusr_handler(self, num, stack): - """ - Handle SIGUSR1 (10) - When caught, reload all the possible files. - """ - log.debug("SIGUSR1 caught, reloading the files…") - self.reload_config() - - def exit_from_signal(self, *args, **kwargs): - """ - Quit when receiving SIGHUP or SIGTERM or SIGPIPE - - do not save the config because it is not a normal exit - (and only roster UI things are not yet saved) - """ - sig = args[0] - signals = { - 1: 'SIGHUP', - 13: 'SIGPIPE', - 15: 'SIGTERM', - } - - log.error("%s received. Exiting…", signals[sig]) - if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop() - self.plugin_manager.disable_plugins() - self.disconnect('%s received' % signals.get(sig)) - self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) - - def autoload_plugins(self): - """ - Load the plugins on startup. - """ - plugins = config.get('plugins_autoload') - if ':' in plugins: - for plugin in plugins.split(':'): - self.plugin_manager.load(plugin) - else: - for plugin in plugins.split(): - self.plugin_manager.load(plugin) - self.plugins_autoloaded = True - - def start(self): - """ - Init curses, create the first tab, etc - """ - self.stdscr = curses.initscr() - self.init_curses(self.stdscr) - self.call_for_resize() - default_tab = tabs.RosterInfoTab() - default_tab.on_gain_focus() - self.tabs.append(default_tab) - self.information('Welcome to poezio!', 'Info') - if firstrun: - self.information( - 'It seems that it is the first time you start poezio.\n' - 'The online help is here http://doc.poez.io/\n' - 'No room is joined by default, but you can join poezio’s' - ' chatroom (with /join poezio@muc.poez.io), where you can' - ' ask for help or tell us how great it is.', - 'Help') - self.refresh_window() - self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) - - def exit(self, event=None): - log.debug("exit(%s)" % (event,)) - asyncio.get_event_loop().stop() - - def on_exception(self, typ, value, trace): - """ - When an exception is raised, just reset curses and call - the original exception handler (will nicely print the traceback) - """ - try: - self.reset_curses() - except: - pass - sys.__excepthook__(typ, value, trace) - - def sigwinch_handler(self): - """A work-around for ncurses resize stuff, which sucks. Normally, ncurses - catches SIGWINCH itself. In its signal handler, it updates the - windows structures (for example the size, etc) and it - ungetch(KEY_RESIZE). That way, the next time we call getch() we know - that a resize occured and we can act on it. BUT poezio doesn’t call - getch() until it knows it will return something. The problem is we - can’t know that, because stdin is not affected by this KEY_RESIZE - value (it is only inserted in a ncurses internal fifo that we can’t - access). - - The (ugly) solution is to handle SIGWINCH ourself, trigger the - change of the internal windows sizes stored in ncurses module, using - sizes that we get using shutil, ungetch the KEY_RESIZE value and - then call getch to handle the resize on poezio’s side properly. - """ - size = shutil.get_terminal_size() - curses.resizeterm(size.lines, size.columns) - curses.ungetch(curses.KEY_RESIZE) - self.on_input_readable() - - def on_input_readable(self): - """ - main loop waiting for the user to press a key - """ - def replace_line_breaks(key): - "replace ^J with \n" - if key == '^J': - return '\n' - return key - def separate_chars_from_bindings(char_list): - """ - returns a list of lists. For example if you give - ['a', 'b', 'KEY_BACKSPACE', 'n', 'u'], this function returns - [['a', 'b'], ['KEY_BACKSPACE'], ['n', 'u']] - - This way, in case of lag (for example), we handle the typed text - by “batch” as much as possible (instead of one char at a time, - which implies a refresh after each char, which is very slow), - but we still handle the special chars (backspaces, arrows, - ctrl+x ou alt+x, etc) one by one, which avoids the issue of - printing them OR ignoring them in that case. This should - resolve the “my ^W are ignored when I lag ;(”. - """ - res = [] - current = [] - for char in char_list: - assert char - # Transform that stupid char into what we actually meant - if char == '\x1f': - char = '^/' - if len(char) == 1: - current.append(char) - else: - # special case for the ^I key, it’s considered as \t - # only when pasting some text, otherwise that’s the ^I - # (or M-i) key, which stands for completion by default. - if char == '^I' and len(char_list) != 1: - current.append('\t') - continue - if current: - res.append(current) - current = [] - res.append([char]) - if current: - res.append(current) - return res - - log.debug("Input is readable.") - big_char_list = [replace_key_with_bound(key)\ - for key in self.read_keyboard()] - log.debug("Got from keyboard: %s", (big_char_list,)) - - # whether to refresh after ALL keys have been handled - for char_list in separate_chars_from_bindings(big_char_list): - # Special case for M-x where x is a number - if len(char_list) == 1: - char = char_list[0] - if char.startswith('M-') and len(char) == 3: - try: - nb = int(char[2]) - except ValueError: - pass - else: - if self.current_tab().nb == nb and config.get('go_to_previous_tab_on_alt_number'): - self.go_to_previous_tab() - else: - self.command_win('%d' % nb) - # search for keyboard shortcut - func = self.key_func.get(char, None) - if func: - func() - else: - self.do_command(replace_line_breaks(char), False) - else: - self.do_command(''.join(char_list), True) - if self.status.show not in ('xa', 'away'): - self.xmpp.plugin['xep_0319'].idle() - self.doupdate() - - def save_config(self): - """ - Save config in the file just before exit - """ - ok = roster.save_to_config_file() - ok = ok and config.silent_set('info_win_height', - self.information_win_size, - 'var') - if not ok: - self.information('Unable to save runtime preferences' - ' in the config file', - 'Error') - - def on_roster_enter_key(self, roster_row): - """ - when enter is pressed on the roster window - """ - if isinstance(roster_row, Contact): - if not self.get_conversation_by_jid(roster_row.bare_jid, False): - self.open_conversation_window(roster_row.bare_jid) - else: - self.focus_tab_named(roster_row.bare_jid) - if isinstance(roster_row, Resource): - if not self.get_conversation_by_jid(roster_row.jid, - False, - fallback_barejid=False): - self.open_conversation_window(roster_row.jid) - else: - self.focus_tab_named(roster_row.jid) - self.refresh_window() - - def get_conversation_messages(self): - """ - Returns a list of all the messages in the current chat. - If the current tab is not a ChatTab, returns None. - - Messages are namedtuples of the form - ('txt nick_color time str_time nickname user') - """ - if not isinstance(self.current_tab(), tabs.ChatTab): - return None - return self.current_tab().get_conversation_messages() - - def insert_input_text(self, text): - """ - Insert the given text into the current input - """ - self.do_command(text, True) - - -##################### Anything related to command execution ################### - - def execute(self, line): - """ - Execute the /command or just send the line on the current room - """ - if line == "": - return - if line.startswith('/'): - command = line.strip()[:].split()[0][1:] - arg = line[2+len(command):] # jump the '/' and the ' ' - # example. on "/link 0 open", command = "link" and arg = "0 open" - if command in self.commands: - func = self.commands[command][0] - func(arg) - return - else: - self.information("Unknown command (%s)" % (command), - 'Error') - - def exec_command(self, command): - """ - Execute an external command on the local or a remote machine, - depending on the conf. For example, to open a link in a browser, do - exec_command(["firefox", "http://poezio.eu"]), and this will call - the command on the correct computer. - - The command argument is a list of strings, not quoted or escaped in - any way. The escaping is done here if needed. - - The remote execution is done - by writing the command on a fifo. That fifo has to be on the - machine where poezio is running, and accessible (through sshfs for - example) from the local machine (where poezio is not running). A - very simple daemon (daemon.py) reads on that fifo, and executes any - command that is read in it. Since we can only write strings to that - fifo, each argument has to be pipes.quote()d. That way the - shlex.split on the reading-side of the daemon will be safe. - - You cannot use a real command line with pipes, redirections etc, but - this function supports a simple case redirection to file: if the - before-last argument of the command is ">" or ">>", then the last - argument is considered to be a filename where the command stdout - will be written. For example you can do exec_command(["echo", - "coucou les amis coucou coucou", ">", "output.txt"]) and this will - work. If you try to do anything else, your |, [, <<, etc will be - interpreted as normal command arguments, not shell special tokens. - """ - if config.get('exec_remote'): - # We just write the command in the fifo - fifo_path = config.get('remote_fifo_path') - if not self.remote_fifo: - try: - self.remote_fifo = Fifo(os.path.join(fifo_path, - 'poezio.fifo'), - 'w') - except (OSError, IOError) as exc: - log.error('Could not open the fifo for writing (%s)', - os.path.join(fifo_path, './', 'poezio.fifo'), - exc_info=True) - self.information('Could not open the fifo ' - 'file for writing: %s' % exc, - 'Error') - return - - args = (pipes.quote(arg.replace('\n', ' ')) for arg in command) - command_str = ' '.join(args) + '\n' - try: - self.remote_fifo.write(command_str) - except (IOError) as exc: - log.error('Could not write in the fifo (%s): %s', - os.path.join(fifo_path, './', 'poezio.fifo'), - repr(command), - exc_info=True) - self.information('Could not execute %s: %s' % (command, exc), - 'Error') - self.remote_fifo = None - else: - executor = Executor(command) - try: - executor.start() - except ValueError as exc: - log.error('Could not execute command (%s)', - repr(command), - exc_info=True) - self.information('%s' % exc, 'Error') - - - def do_command(self, key, raw): - """ - Execute the action associated with a key - - Or if keyboard.continuation_keys_callback is set, call it instead. See - the comment of this variable. - """ - if not key: - return - if keyboard.continuation_keys_callback is not None: - # Reset the callback to None BEFORE calling it, because this - # callback MAY set a new callback itself, and we don’t want to - # erase it in that case - cb = keyboard.continuation_keys_callback - keyboard.continuation_keys_callback = None - cb(key) - else: - self.current_tab().on_input(key, raw) - - - def try_execute(self, line): - """ - Try to execute a command in the current tab - """ - line = '/' + line - try: - self.current_tab().execute_command(line) - except: - log.error('Execute failed (%s)', line, exc_info=True) - - -########################## TImed Events ####################################### - - def remove_timed_event(self, event): - """Remove an existing timed event""" - event.handler.cancel() - - def add_timed_event(self, event): - """Add a new timed event""" - event.handler = asyncio.get_event_loop().call_later(event.delay, - event.callback, - *event.args) - -####################### XMPP-related actions ################################## - - def get_status(self): - """ - Get the last status that was previously set - """ - return self.status - - def set_status(self, pres, msg): - """ - Set our current status so we can remember - it and use it back when needed (for example to display it - or to use it when joining a new muc) - """ - self.status = Status(show=pres, message=msg) - if config.get('save_status'): - ok = config.silent_set('status', pres if pres else '') - msg = msg.replace('\n', '|') if msg else '' - ok = ok and config.silent_set('status_message', msg) - if not ok: - self.information('Unable to save the status in ' - 'the config file', 'Error') - - def get_bookmark_nickname(self, room_name): - """ - Returns the nickname associated with a bookmark - or the default nickname - """ - bm = self.bookmarks[room_name] - if bm: - return bm.nick - return self.own_nick - - def disconnect(self, msg='', reconnect=False): - """ - Disconnect from remote server and correctly set the states of all - parts of the client (for example, set the MucTabs as not joined, etc) - """ - self.legitimate_disconnect = True - msg = msg or '' - for tab in self.get_tabs(tabs.MucTab): - tab.command_part(msg) - self.xmpp.disconnect() - if reconnect: - # Add a one-time event to reconnect as soon as we are - # effectively disconnected - self.xmpp.add_event_handler('disconnected', lambda event: self.xmpp.connect(), disposable=True) - - def send_message(self, msg): - """ - Function to use in plugins to send a message in the current - conversation. - Returns False if the current tab is not a conversation tab - """ - if not isinstance(self.current_tab(), tabs.ChatTab): - return False - self.current_tab().command_say(msg) - return True - - def invite(self, jid, room, reason=None): - """ - Checks if the sender supports XEP-0249, then send an invitation, - or a mediated one if it does not. - TODO: allow passwords - """ - def callback(iq): - if not iq: - return - if 'jabber:x:conference' in iq['disco_info'].get_features(): - self.xmpp.plugin['xep_0249'].send_invitation( - jid, - room, - reason=reason) - else: # fallback - self.xmpp.plugin['xep_0045'].invite(room, jid, - reason=reason or '') - - self.xmpp.plugin['xep_0030'].get_info(jid=jid, timeout=5, - callback=callback) - - def get_error_message(self, stanza, deprecated=False): - """ - Takes a stanza of the form <message type='error'><error/></message> - and return a well formed string containing the error informations - """ - sender = stanza.attrib['from'] - msg = stanza['error']['type'] - condition = stanza['error']['condition'] - code = stanza['error']['code'] - body = stanza['error']['text'] - if not body: - if deprecated: - if code in DEPRECATED_ERRORS: - body = DEPRECATED_ERRORS[code] - else: - body = condition or 'Unknown error' - else: - if code in ERROR_AND_STATUS_CODES: - body = ERROR_AND_STATUS_CODES[code] - else: - body = condition or 'Unknown error' - if code: - message = '%(from)s: %(code)s - %(msg)s: %(body)s' % { - 'from': sender, 'msg': msg, 'body': body, 'code': code} - else: - message = '%(from)s: %(msg)s: %(body)s' % { - 'from': sender, 'msg': msg, 'body': body} - return message - - -####################### Tab logic-related things ############################## - - ### Tab getters ### - - def get_tabs(self, cls=tabs.Tab): - "Get all the tabs of a type" - return filter(lambda tab: isinstance(tab, cls), self.tabs) - - def current_tab(self): - """ - returns the current room, the one we are viewing - """ - self.current_tab_nb = self.current_tab_nb - return self.tabs[self.current_tab_nb] - - def get_conversation_by_jid(self, jid, create=True, fallback_barejid=True): - """ - From a JID, get the tab containing the conversation with it. - If none already exist, and create is "True", we create it - and return it. Otherwise, we return None. - - If fallback_barejid is True, then this method will seek other - tabs with the same barejid, instead of searching only by fulljid. - """ - jid = safeJID(jid) - # We first check if we have a static conversation opened - # with this precise resource - conversation = self.get_tab_by_name(jid.full, - tabs.StaticConversationTab) - if jid.bare == jid.full and not conversation: - conversation = self.get_tab_by_name(jid.full, - tabs.DynamicConversationTab) - - if not conversation and fallback_barejid: - # If not, we search for a conversation with the bare jid - conversation = self.get_tab_by_name(jid.bare, - tabs.DynamicConversationTab) - if not conversation: - if create: - # We create a dynamic conversation with the bare Jid if - # nothing was found (and we lock it to the resource - # later) - conversation = self.open_conversation_window(jid.bare, - False) - else: - conversation = None - return conversation - - def get_tab_by_name(self, name, typ=None): - """ - Get the tab with the given name. - If typ is provided, return a tab of this type only - """ - for tab in self.tabs: - if tab.name == name: - if (typ and isinstance(tab, typ)) or\ - not typ: - return tab - return None - - def get_tab_by_number(self, number): - if 0 <= number < len(self.tabs): - return self.tabs[number] - return None - - def add_tab(self, new_tab, focus=False): - """ - Appends the new_tab in the tab list and - focus it if focus==True - """ - self.tabs.append(new_tab) - if focus: - self.command_win("%s" % new_tab.nb) - - def insert_tab_nogaps(self, old_pos, new_pos): - """ - Move tabs without creating gaps - old_pos: old position of the tab - new_pos: desired position of the tab - """ - tab = self.tabs[old_pos] - if new_pos < old_pos: - self.tabs.pop(old_pos) - self.tabs.insert(new_pos, tab) - elif new_pos > old_pos: - self.tabs.insert(new_pos, tab) - self.tabs.remove(tab) - else: - return False - return True - - def insert_tab_gaps(self, old_pos, new_pos): - """ - Move tabs and create gaps in the eventual remaining space - old_pos: old position of the tab - new_pos: desired position of the tab - """ - tab = self.tabs[old_pos] - target = None if new_pos >= len(self.tabs) else self.tabs[new_pos] - if not target: - if new_pos < len(self.tabs): - old_tab = self.tabs[old_pos] - self.tabs[new_pos], self.tabs[old_pos] = old_tab, tabs.GapTab() - else: - self.tabs.append(self.tabs[old_pos]) - self.tabs[old_pos] = tabs.GapTab() - else: - if new_pos > old_pos: - self.tabs.insert(new_pos, tab) - self.tabs[old_pos] = tabs.GapTab() - elif new_pos < old_pos: - self.tabs[old_pos] = tabs.GapTab() - self.tabs.insert(new_pos, tab) - else: - return False - i = self.tabs.index(tab) - done = False - # Remove the first Gap on the right in the list - # in order to prevent global shifts when there is empty space - while not done: - i += 1 - if i >= len(self.tabs): - done = True - elif not self.tabs[i]: - self.tabs.pop(i) - done = True - # Remove the trailing gaps - i = len(self.tabs) - 1 - while isinstance(self.tabs[i], tabs.GapTab): - self.tabs.pop() - i -= 1 - return True - - def insert_tab(self, old_pos, new_pos=99999): - """ - Insert a tab at a position, changing the number of the following tabs - returns False if it could not move the tab, True otherwise - """ - if old_pos <= 0 or old_pos >= len(self.tabs): - return False - elif new_pos <= 0: - return False - elif new_pos == old_pos: - return False - elif not self.tabs[old_pos]: - return False - if config.get('create_gaps'): - return self.insert_tab_gaps(old_pos, new_pos) - return self.insert_tab_nogaps(old_pos, new_pos) - - ### Move actions (e.g. go to next room) ### - - def rotate_rooms_right(self, args=None): - """ - rotate the rooms list to the right - """ - self.current_tab().on_lose_focus() - self.current_tab_nb += 1 - while not self.tabs[self.current_tab_nb]: - self.current_tab_nb += 1 - self.current_tab().on_gain_focus() - self.refresh_window() - - def rotate_rooms_left(self, args=None): - """ - rotate the rooms list to the right - """ - self.current_tab().on_lose_focus() - self.current_tab_nb -= 1 - while not self.tabs[self.current_tab_nb]: - self.current_tab_nb -= 1 - self.current_tab().on_gain_focus() - self.refresh_window() - - def go_to_room_number(self): - """ - Read 2 more chars and go to the tab - with the given number - """ - def read_next_digit(digit): - try: - nb = int(digit) - except ValueError: - # If it is not a number, we do nothing. If it was the first - # one, we do not wait for a second one by re-setting the - # callback - self.room_number_jump.clear() - else: - self.room_number_jump.append(digit) - if len(self.room_number_jump) == 2: - arg = "".join(self.room_number_jump) - self.room_number_jump.clear() - self.command_win(arg) - else: - # We need to read more digits - keyboard.continuation_keys_callback = read_next_digit - keyboard.continuation_keys_callback = read_next_digit - - def go_to_roster(self): - "Select the roster as the current tab" - self.command_win('0') - - def go_to_previous_tab(self): - "Go to the previous tab" - self.command_win('%s' % (self.previous_tab_nb,)) - - def go_to_important_room(self): - """ - Go to the next room with activity, in the order defined in the - dict tabs.STATE_PRIORITY - """ - # shortcut - priority = tabs.STATE_PRIORITY - tab_refs = {} - # put all the active tabs in a dict of lists by state - for tab in self.tabs: - if not tab: - continue - if tab.state not in tab_refs: - tab_refs[tab.state] = [tab] - else: - tab_refs[tab.state].append(tab) - # sort the state by priority and remove those with negative priority - states = sorted(tab_refs.keys(), - key=(lambda x: priority.get(x, 0)), - reverse=True) - states = [state for state in states if priority.get(state, -1) >= 0] - - for state in states: - for tab in tab_refs[state]: - if (tab.nb < self.current_tab_nb and - tab_refs[state][-1].nb > self.current_tab_nb): - continue - self.command_win('%s' % tab.nb) - return - return - - def focus_tab_named(self, tab_name, type_=None): - """Returns True if it found a tab to focus on""" - for tab in self.tabs: - if tab.name == tab_name: - if (type_ and (isinstance(tab, type_))) or not type_: - self.command_win('%s' % (tab.nb,)) - return True - return False - - @property - def current_tab_nb(self): - """Wrapper for the current tab number""" - return self._current_tab_nb - - @current_tab_nb.setter - def current_tab_nb(self, value): - """ - Prevents the tab number from going over the total number of opened - tabs, or under 0 - """ - old = self._current_tab_nb - if value >= len(self.tabs): - self._current_tab_nb = 0 - elif value < 0: - self._current_tab_nb = len(self.tabs) - 1 - else: - self._current_tab_nb = value - if old != self._current_tab_nb and self.tabs[self._current_tab_nb]: - self.events.trigger('tab_change', old, self._current_tab_nb) - - ### Opening actions ### - - def open_conversation_window(self, jid, focus=True): - """ - Open a new conversation tab and focus it if needed. If a resource is - provided, we open a StaticConversationTab, else a - DynamicConversationTab - """ - if safeJID(jid).resource: - new_tab = tabs.StaticConversationTab(jid) - else: - new_tab = tabs.DynamicConversationTab(jid) - if not focus: - new_tab.state = "private" - self.add_tab(new_tab, focus) - self.refresh_window() - return new_tab - - def open_private_window(self, room_name, user_nick, focus=True): - """ - Open a Private conversation in a MUC and focus if needed. - """ - complete_jid = room_name+'/'+user_nick - # if the room exists, focus it and return - for tab in self.get_tabs(tabs.PrivateTab): - if tab.name == complete_jid: - self.command_win('%s' % tab.nb) - return tab - # create the new tab - tab = self.get_tab_by_name(room_name, tabs.MucTab) - if not tab: - return None - new_tab = tabs.PrivateTab(complete_jid, tab.own_nick) - if hasattr(tab, 'directed_presence'): - new_tab.directed_presence = tab.directed_presence - if not focus: - new_tab.state = "private" - # insert it in the tabs - self.add_tab(new_tab, focus) - self.refresh_window() - tab.privates.append(new_tab) - return new_tab - - def open_new_room(self, room, nick, *, password=None, focus=True): - """ - Open a new tab.MucTab containing a muc Room, using the specified nick - """ - new_tab = tabs.MucTab(room, nick, password=password) - self.add_tab(new_tab, focus) - self.refresh_window() - return new_tab - - def open_new_form(self, form, on_cancel, on_send, **kwargs): - """ - Open a new tab containing the form - The callback are called with the completed form as parameter in - addition with kwargs - """ - form_tab = tabs.DataFormsTab(form, on_cancel, on_send, kwargs) - self.add_tab(form_tab, True) - - ### Modifying actions ### - - def rename_private_tabs(self, room_name, old_nick, new_nick): - """ - Call this method when someone changes his/her nick in a MUC, - this updates the name of all the opened private conversations - with him/her - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, old_nick), - tabs.PrivateTab) - if tab: - tab.rename_user(old_nick, new_nick) - - def on_user_left_private_conversation(self, room_name, nick, status_message): - """ - The user left the MUC: add a message in the associated - private conversation - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) - if tab: - tab.user_left(status_message, nick) - - def on_user_rejoined_private_conversation(self, room_name, nick): - """ - The user joined a MUC: add a message in the associated - private conversation - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) - if tab: - tab.user_rejoined(nick) - - def disable_private_tabs(self, room_name, reason=None): - """ - Disable private tabs when leaving a room - """ - if reason is None: - reason = '\x195}You left the chatroom\x193}' - for tab in self.get_tabs(tabs.PrivateTab): - if tab.name.startswith(room_name): - tab.deactivate(reason=reason) - - def enable_private_tabs(self, room_name, reason=None): - """ - Enable private tabs when joining a room - """ - if reason is None: - reason = '\x195}You joined the chatroom\x193}' - for tab in self.get_tabs(tabs.PrivateTab): - if tab.name.startswith(room_name): - tab.activate(reason=reason) - - def on_user_changed_status_in_private(self, jid, msg): - tab = self.get_tab_by_name(jid, tabs.ChatTab) - if tab: # display the message in private - tab.add_message(msg, typ=2) - - def close_tab(self, tab=None): - """ - Close the given tab. If None, close the current one - """ - was_current = tab is None - tab = tab or self.current_tab() - if isinstance(tab, tabs.RosterInfoTab): - return # The tab 0 should NEVER be closed - del tab.key_func # Remove self references - del tab.commands # and make the object collectable - tab.on_close() - nb = tab.nb - if was_current: - if self.previous_tab_nb != nb: - self.current_tab_nb = self.previous_tab_nb - self.previous_tab_nb = 0 - if config.get('create_gaps'): - if nb >= len(self.tabs) - 1: - self.tabs.remove(tab) - nb -= 1 - while not self.tabs[nb]: # remove the trailing gaps - self.tabs.pop() - nb -= 1 - else: - self.tabs[nb] = tabs.GapTab() - else: - self.tabs.remove(tab) - if tab and tab.name in logger.fds: - logger.fds[tab.name].close() - log.debug("Log file for %s closed.", tab.name) - del logger.fds[tab.name] - if self.current_tab_nb >= len(self.tabs): - self.current_tab_nb = len(self.tabs) - 1 - while not self.tabs[self.current_tab_nb]: - self.current_tab_nb -= 1 - if was_current: - self.current_tab().on_gain_focus() - self.refresh_window() - import gc - gc.collect() - log.debug('___ Referrers of closing tab:\n%s\n______', - gc.get_referrers(tab)) - del tab - - def add_information_message_to_conversation_tab(self, jid, msg): - """ - Search for a ConversationTab with the given jid (full or bare), - if yes, add the given message to it - """ - tab = self.get_tab_by_name(jid, tabs.ConversationTab) - if tab: - tab.add_message(msg, typ=2) - if self.current_tab() is tab: - self.refresh_window() - - -####################### Curses and ui-related stuff ########################### - - def doupdate(self): - "Do a curses update" - if not self.running: - return - curses.doupdate() - - def information(self, msg, typ=''): - """ - Displays an informational message in the "Info" buffer - """ - filter_messages = config.get('filter_info_messages').split(':') - for words in filter_messages: - if words and words in msg: - log.debug('Did not show the message:\n\t%s> %s', typ, msg) - return False - colors = get_theme().INFO_COLORS - color = colors.get(typ.lower(), colors.get('default', None)) - nb_lines = self.information_buffer.add_message(msg, - nickname=typ, - nick_color=color) - popup_on = config.get('information_buffer_popup_on').split() - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - elif typ != '' and typ.lower() in popup_on: - popup_time = config.get('popup_time') + (nb_lines - 1) * 2 - self.pop_information_win_up(nb_lines, popup_time) - else: - if self.information_win_size != 0: - self.information_win.refresh() - self.current_tab().input.refresh() - return True - - def init_curses(self, stdscr): - """ - ncurses initialization - """ - curses.curs_set(1) - curses.noecho() - curses.nonl() - curses.raw() - stdscr.idlok(1) - stdscr.keypad(1) - curses.start_color() - curses.use_default_colors() - theming.reload_theme() - curses.ungetch(" ") # H4X: without this, the screen is - stdscr.getkey() # erased on the first "getkey()" - - def reset_curses(self): - """ - Reset terminal capabilities to what they were before ncurses - init - """ - curses.echo() - curses.nocbreak() - curses.curs_set(1) - curses.endwin() - - @property - def informations(self): - return self.information_buffer - - def refresh_window(self): - """ - Refresh everything - """ - nocursor = curses.curs_set(0) - self.current_tab().state = 'current' - self.current_tab().refresh() - self.doupdate() - curses.curs_set(nocursor) - - def refresh_tab_win(self): - """ - Refresh the window containing the tab list - """ - self.current_tab().refresh_tab_win() - self.refresh_input() - self.doupdate() - - def refresh_input(self): - """ - Refresh the input if it exists - """ - if self.current_tab().input: - self.current_tab().input.refresh() - self.doupdate() - - def scroll_page_down(self, args=None): - """ - Scroll a page down, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_scroll_down(): - self.refresh_window() - return True - - def scroll_page_up(self, args=None): - """ - Scroll a page up, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_scroll_up(): - self.refresh_window() - return True - - def scroll_line_up(self, args=None): - """ - Scroll a line up, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_line_up(): - self.refresh_window() - return True - - def scroll_line_down(self, args=None): - """ - Scroll a line down, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_line_down(): - self.refresh_window() - return True - - def scroll_half_up(self, args=None): - """ - Scroll half a screen down, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_half_scroll_up(): - self.refresh_window() - return True - - def scroll_half_down(self, args=None): - """ - Scroll half a screen down, if possible. - Returns True on success, None on failure. - """ - if self.current_tab().on_half_scroll_down(): - self.refresh_window() - return True - - def grow_information_win(self, nb=1): - """ - Expand the information win a number of lines - """ - if self.information_win_size >= self.current_tab().height -5 or \ - self.information_win_size+nb >= self.current_tab().height-4 or\ - self.size.core_degrade_y: - return 0 - self.information_win_size += nb - self.resize_global_information_win() - for tab in self.tabs: - tab.on_info_win_size_changed() - self.refresh_window() - return nb - - def shrink_information_win(self, nb=1): - """ - Reduce the size of the information win - """ - if self.information_win_size == 0 or self.size.core_degrade_y: - return - self.information_win_size -= nb - if self.information_win_size < 0: - self.information_win_size = 0 - self.resize_global_information_win() - for tab in self.tabs: - tab.on_info_win_size_changed() - self.refresh_window() - - def scroll_info_up(self): - """ - Scroll the information buffer up - """ - self.information_win.scroll_up(self.information_win.height) - if not isinstance(self.current_tab(), tabs.RosterInfoTab): - self.information_win.refresh() - else: - info = self.current_tab().information_win - info.scroll_up(info.height) - self.refresh_window() - - def scroll_info_down(self): - """ - Scroll the information buffer down - """ - self.information_win.scroll_down(self.information_win.height) - if not isinstance(self.current_tab(), tabs.RosterInfoTab): - self.information_win.refresh() - else: - info = self.current_tab().information_win - info.scroll_down(info.height) - self.refresh_window() - - def pop_information_win_up(self, size, time): - """ - Temporarly increase the size of the information win of size lines - during time seconds. - After that delay, the size will decrease from size lines. - """ - if time <= 0 or size <= 0: - return - result = self.grow_information_win(size) - timed_event = timed_events.DelayedEvent(time, - self.shrink_information_win, - result) - self.add_timed_event(timed_event) - self.refresh_window() - - def toggle_left_pane(self): - """ - Enable/disable the left panel. - """ - enabled = config.get('enable_vertical_tab_list') - if not config.silent_set('enable_vertical_tab_list', str(not enabled)): - self.information('Unable to write in the config file', 'Error') - self.call_for_resize() - - def resize_global_information_win(self): - """ - Resize the global_information_win only once at each resize. - """ - if self.information_win_size > tabs.Tab.height - 6: - self.information_win_size = tabs.Tab.height - 6 - if tabs.Tab.height < 6: - self.information_win_size = 0 - height = (tabs.Tab.height - 1 - self.information_win_size - - tabs.Tab.tab_win_height()) - self.information_win.resize(self.information_win_size, - tabs.Tab.width, - height, - 0) - - def resize_global_info_bar(self): - """ - Resize the GlobalInfoBar only once at each resize - """ - height, width = self.stdscr.getmaxyx() - if config.get('enable_vertical_tab_list'): - - if self.size.core_degrade_x: - return - try: - height, _ = self.stdscr.getmaxyx() - truncated_win = self.stdscr.subwin(height, - config.get('vertical_tab_list_size'), - 0, 0) - except: - log.error('Curses error on infobar resize', exc_info=True) - return - self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) - elif not self.size.core_degrade_y: - self.tab_win.resize(1, tabs.Tab.width, - tabs.Tab.height - 2, 0) - self.left_tab_win = None - - def add_message_to_text_buffer(self, buff, txt, - time=None, nickname=None, history=None): - """ - Add the message to the room if possible, else, add it to the Info window - (in the Info tab of the info window in the RosterTab) - """ - if not buff: - self.information('Trying to add a message in no room: %s' % txt, 'Error') - else: - buff.add_message(txt, time, nickname, history=history) - - def full_screen_redraw(self): - """ - Completely erase and redraw the screen - """ - self.stdscr.clear() - self.refresh_window() - - def call_for_resize(self): - """ - Called when we want to resize the screen - """ - # If we have the tabs list on the left, we just give a truncated - # window to each Tab class, so they draw themself in the portion of - # the screen that they can occupy, and we draw the tab list on the - # remaining space, on the left - height, width = self.stdscr.getmaxyx() - if (config.get('enable_vertical_tab_list') and - not self.size.core_degrade_x): - try: - scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size')) - except: - log.error('Curses error on resize', exc_info=True) - return - else: - scr = self.stdscr - tabs.Tab.resize(scr) - self.resize_global_info_bar() - self.resize_global_information_win() - for tab in self.tabs: - if config.get('lazy_resize'): - tab.need_resize = True - else: - tab.resize() - if self.tabs: - self.full_screen_redraw() - - def read_keyboard(self): - """ - Get the next keyboard key pressed and returns it. It blocks until - something can be read on stdin, this function must be called only if - there is something to read. No timeout ever occurs. - """ - return self.keyboard.get_user_input(self.stdscr) - - def escape_next_key(self): - """ - Tell the Keyboard object that the next key pressed by the user - should be escaped. See Keyboard.get_user_input - """ - self.keyboard.escape_next_key() - -####################### Commands and completions ############################## - - def register_command(self, name, func, **kwargs): - """ - Add a command - """ - desc = kwargs.get('desc', '') - shortdesc = kwargs.get('shortdesc', '') - completion = kwargs.get('completion') - usage = kwargs.get('usage', '') - if name in self.commands: - return - if not desc and shortdesc: - desc = shortdesc - self.commands[name] = Command(func, desc, completion, shortdesc, usage) - - def register_initial_commands(self): - """ - Register the commands when poezio starts - """ - self.register_command('help', self.command_help, - usage='[command]', - shortdesc='\\_o< KOIN KOIN KOIN', - completion=self.completion_help) - self.register_command('join', self.command_join, - usage="[room_name][@server][/nick] [password]", - desc="Join the specified room. You can specify a nickname " - "after a slash (/). If no nickname is specified, you will" - " use the default_nick in the configuration file. You can" - " omit the room name: you will then join the room you\'re" - " looking at (useful if you were kicked). You can also " - "provide a room_name without specifying a server, the " - "server of the room you're currently in will be used. You" - " can also provide a password to join the room.\nExamples" - ":\n/join room@server.tld\n/join room@server.tld/John\n" - "/join room2\n/join /me_again\n/join\n/join room@server" - ".tld/my_nick password\n/join / password", - shortdesc='Join a room', - completion=self.completion_join) - self.register_command('exit', self.command_quit, - desc='Just disconnect from the server and exit poezio.', - shortdesc='Exit poezio.') - self.register_command('quit', self.command_quit, - desc='Just disconnect from the server and exit poezio.', - shortdesc='Exit poezio.') - self.register_command('next', self.rotate_rooms_right, - shortdesc='Go to the next room.') - self.register_command('prev', self.rotate_rooms_left, - shortdesc='Go to the previous room.') - self.register_command('win', self.command_win, - usage='<number or name>', - shortdesc='Go to the specified room', - completion=self.completion_win) - self.commands['w'] = self.commands['win'] - self.register_command('move_tab', self.command_move_tab, - usage='<source> <destination>', - desc="Insert the <source> tab at the position of " - "<destination>. This will make the following tabs shift in" - " some cases (refer to the documentation). A tab can be " - "designated by its number or by the beginning of its " - "address. You can use \".\" as a shortcut for the current " - "tab.", - shortdesc='Move a tab.', - completion=self.completion_move_tab) - self.register_command('destroy_room', self.command_destroy_room, - usage='[room JID]', - desc='Try to destroy the room [room JID], or the current' - ' tab if it is a multi-user chat and [room JID] is ' - 'not given.', - shortdesc='Destroy a room.', - completion=None) - self.register_command('show', self.command_status, - usage='<availability> [status message]', - desc="Sets your availability and (optionally) your status " - "message. The <availability> argument is one of \"available" - ", chat, away, afk, dnd, busy, xa\" and the optional " - "[status message] argument will be your status message.", - shortdesc='Change your availability.', - completion=self.completion_status) - self.commands['status'] = self.commands['show'] - self.register_command('bookmark_local', self.command_bookmark_local, - usage="[roomname][/nick] [password]", - desc="Bookmark Local: Bookmark locally the specified room " - "(you will then auto-join it on each poezio start). This" - " commands uses almost the same syntaxe as /join. Type " - "/help join for syntax examples. Note that when typing " - "\"/bookmark\" on its own, the room will be bookmarked " - "with the nickname you\'re currently using in this room " - "(instead of default_nick)", - shortdesc='Bookmark a room locally.', - completion=self.completion_bookmark_local) - self.register_command('bookmark', self.command_bookmark, - usage="[roomname][/nick] [autojoin] [password]", - desc="Bookmark: Bookmark online the specified room (you " - "will then auto-join it on each poezio start if autojoin" - " is specified and is 'true'). This commands uses almost" - " the same syntax as /join. Type /help join for syntax " - "examples. Note that when typing \"/bookmark\" alone, the" - " room will be bookmarked with the nickname you\'re " - "currently using in this room (instead of default_nick).", - shortdesc="Bookmark a room online.", - completion=self.completion_bookmark) - self.register_command('set', self.command_set, - usage="[plugin|][section] <option> [value]", - desc="Set the value of an option in your configuration file." - " You can, for example, change your default nickname by " - "doing `/set default_nick toto` or your resource with `/set" - " resource blabla`. You can also set options in specific " - "sections with `/set bindings M-i ^i` or in specific plugin" - " with `/set mpd_client| host 127.0.0.1`. `toggle` can be " - "used as a special value to toggle a boolean option.", - shortdesc="Set the value of an option", - completion=self.completion_set) - self.register_command('set_default', self.command_set_default, - usage="[section] <option>", - desc="Set the default value of an option. For example, " - "`/set_default resource` will reset the resource " - "option. You can also reset options in specific " - "sections by doing `/set_default section option`.", - shortdesc="Set the default value of an option", - completion=self.completion_set_default) - self.register_command('toggle', self.command_toggle, - usage='<option>', - desc='Shortcut for /set <option> toggle', - shortdesc='Toggle an option', - completion=self.completion_toggle) - self.register_command('theme', self.command_theme, - usage='[theme name]', - desc="Reload the theme defined in the config file. If theme" - "_name is provided, set that theme before reloading it.", - shortdesc='Load a theme', - completion=self.completion_theme) - self.register_command('list', self.command_list, - usage='[server]', - desc="Get the list of public chatrooms" - " on the specified server.", - shortdesc='List the rooms.', - completion=self.completion_list) - self.register_command('message', self.command_message, - usage='<jid> [optional message]', - desc="Open a conversation with the specified JID (even if it" - " is not in our roster), and send a message to it, if the " - "message is specified.", - shortdesc='Send a message', - completion=self.completion_message) - self.register_command('version', self.command_version, - usage='<jid>', - desc="Get the software version of the given JID (usually its" - " XMPP client and Operating System).", - shortdesc='Get the software version of a JID.', - completion=self.completion_version) - self.register_command('server_cycle', self.command_server_cycle, - usage='[domain] [message]', - desc='Disconnect and reconnect in all the rooms in domain.', - shortdesc='Cycle a range of rooms', - completion=self.completion_server_cycle) - self.register_command('bind', self.command_bind, - usage='<key> <equ>', - desc="Bind a key to another key or to a “command”. For " - "example \"/bind ^H KEY_UP\" makes Control + h do the" - " same same as the Up key.", - completion=self.completion_bind, - shortdesc='Bind a key to another key.') - self.register_command('load', self.command_load, - usage='<plugin> [<otherplugin> …]', - shortdesc='Load the specified plugin(s)', - completion=self.plugin_manager.completion_load) - self.register_command('unload', self.command_unload, - usage='<plugin> [<otherplugin> …]', - shortdesc='Unload the specified plugin(s)', - completion=self.plugin_manager.completion_unload) - self.register_command('plugins', self.command_plugins, - shortdesc='Show the plugins in use.') - self.register_command('presence', self.command_presence, - usage='<JID> [type] [status]', - desc="Send a directed presence to <JID> and using" - " [type] and [status] if provided.", - shortdesc='Send a directed presence.', - completion=self.completion_presence) - self.register_command('rawxml', self.command_rawxml, - usage='<xml>', - shortdesc='Send a custom xml stanza.') - self.register_command('invite', self.command_invite, - usage='<jid> <room> [reason]', - desc='Invite jid in room with reason.', - shortdesc='Invite someone in a room.', - completion=self.completion_invite) - self.register_command('invitations', self.command_invitations, - shortdesc='Show the pending invitations.') - self.register_command('bookmarks', self.command_bookmarks, - shortdesc='Show the current bookmarks.') - self.register_command('remove_bookmark', self.command_remove_bookmark, - usage='[jid]', - desc="Remove the specified bookmark, or the " - "bookmark on the current tab, if any.", - shortdesc='Remove a bookmark', - completion=self.completion_remove_bookmark) - self.register_command('xml_tab', self.command_xml_tab, - shortdesc='Open an XML tab.') - self.register_command('runkey', self.command_runkey, - usage='<key>', - shortdesc='Execute the action defined for <key>.', - completion=self.completion_runkey) - self.register_command('self', self.command_self, - shortdesc='Remind you of who you are.') - self.register_command('last_activity', self.command_last_activity, - usage='<jid>', - desc='Informs you of the last activity of a JID.', - shortdesc='Get the activity of someone.', - completion=self.completion_last_activity) - self.register_command('ad-hoc', self.command_adhoc, - usage='<jid>', - shortdesc='List available ad-hoc commands on the given jid') - self.register_command('reload', self.command_reload, - shortdesc='Reload the config. You can achieve the same by ' - 'sending SIGUSR1 to poezio.') - - if config.get('enable_user_activity'): - self.register_command('activity', self.command_activity, - usage='[<general> [specific] [text]]', - desc='Send your current activity to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting an activity".', - shortdesc='Send your activity.', - completion=self.completion_activity) - if config.get('enable_user_mood'): - self.register_command('mood', self.command_mood, - usage='[<mood> [text]]', - desc='Send your current mood to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting a mood".', - shortdesc='Send your mood.', - completion=self.completion_mood) - if config.get('enable_user_gaming'): - self.register_command('gaming', self.command_gaming, - usage='[<game name> [server address]]', - desc='Send your current gaming activity to ' - 'your contacts. Nothing means "stop ' - 'broadcasting a gaming activity".', - shortdesc='Send your gaming activity.', - completion=None) - -####################### XMPP Event Handlers ################################## - on_session_start_features = handlers.on_session_start_features - on_carbon_received = handlers.on_carbon_received - on_carbon_sent = handlers.on_carbon_sent - on_groupchat_invitation = handlers.on_groupchat_invitation - on_groupchat_direct_invitation = handlers.on_groupchat_direct_invitation - on_groupchat_decline = handlers.on_groupchat_decline - on_message = handlers.on_message - on_error_message = handlers.on_error_message - on_normal_message = handlers.on_normal_message - on_nick_received = handlers.on_nick_received - on_gaming_event = handlers.on_gaming_event - on_mood_event = handlers.on_mood_event - on_activity_event = handlers.on_activity_event - on_tune_event = handlers.on_tune_event - on_groupchat_message = handlers.on_groupchat_message - on_muc_own_nickchange = handlers.on_muc_own_nickchange - on_groupchat_private_message = handlers.on_groupchat_private_message - on_chatstate_active = handlers.on_chatstate_active - on_chatstate_inactive = handlers.on_chatstate_inactive - on_chatstate_composing = handlers.on_chatstate_composing - on_chatstate_paused = handlers.on_chatstate_paused - on_chatstate_gone = handlers.on_chatstate_gone - on_chatstate = handlers.on_chatstate - on_chatstate_normal_conversation = handlers.on_chatstate_normal_conversation - on_chatstate_private_conversation = \ - handlers.on_chatstate_private_conversation - on_chatstate_groupchat_conversation = \ - handlers.on_chatstate_groupchat_conversation - on_roster_update = handlers.on_roster_update - on_subscription_request = handlers.on_subscription_request - on_subscription_authorized = handlers.on_subscription_authorized - on_subscription_remove = handlers.on_subscription_remove - on_subscription_removed = handlers.on_subscription_removed - on_presence = handlers.on_presence - on_presence_error = handlers.on_presence_error - on_got_offline = handlers.on_got_offline - on_got_online = handlers.on_got_online - on_groupchat_presence = handlers.on_groupchat_presence - on_failed_connection = handlers.on_failed_connection - on_disconnected = handlers.on_disconnected - on_stream_error = handlers.on_stream_error - on_failed_all_auth = handlers.on_failed_all_auth - on_no_auth = handlers.on_no_auth - on_connected = handlers.on_connected - on_connecting = handlers.on_connecting - on_session_start = handlers.on_session_start - on_status_codes = handlers.on_status_codes - on_groupchat_subject = handlers.on_groupchat_subject - on_data_form = handlers.on_data_form - on_receipt = handlers.on_receipt - on_attention = handlers.on_attention - room_error = handlers.room_error - check_bookmark_storage = handlers.check_bookmark_storage - outgoing_stanza = handlers.outgoing_stanza - incoming_stanza = handlers.incoming_stanza - validate_ssl = handlers.validate_ssl - ssl_invalid_chain = handlers.ssl_invalid_chain - on_next_adhoc_step = handlers.on_next_adhoc_step - on_adhoc_error = handlers.on_adhoc_error - cancel_adhoc_command = handlers.cancel_adhoc_command - validate_adhoc_step = handlers.validate_adhoc_step - terminate_adhoc_command = handlers.terminate_adhoc_command - command_help = commands.command_help - command_runkey = commands.command_runkey - command_status = commands.command_status - command_presence = commands.command_presence - command_theme = commands.command_theme - command_win = commands.command_win - command_move_tab = commands.command_move_tab - command_list = commands.command_list - command_version = commands.command_version - command_join = commands.command_join - command_bookmark_local = commands.command_bookmark_local - command_bookmark = commands.command_bookmark - command_bookmarks = commands.command_bookmarks - command_destroy_room = commands.command_destroy_room - command_remove_bookmark = commands.command_remove_bookmark - command_set = commands.command_set - command_set_default = commands.command_set_default - command_toggle = commands.command_toggle - command_server_cycle = commands.command_server_cycle - command_last_activity = commands.command_last_activity - command_mood = commands.command_mood - command_activity = commands.command_activity - command_gaming = commands.command_gaming - command_invite = commands.command_invite - command_decline = commands.command_decline - command_invitations = commands.command_invitations - command_quit = commands.command_quit - command_bind = commands.command_bind - command_rawxml = commands.command_rawxml - command_load = commands.command_load - command_unload = commands.command_unload - command_plugins = commands.command_plugins - command_message = commands.command_message - command_xml_tab = commands.command_xml_tab - command_adhoc = commands.command_adhoc - command_self = commands.command_self - command_reload = commands.command_reload - completion_help = completions.completion_help - completion_status = completions.completion_status - completion_presence = completions.completion_presence - completion_theme = completions.completion_theme - completion_win = completions.completion_win - completion_join = completions.completion_join - completion_version = completions.completion_version - completion_list = completions.completion_list - completion_move_tab = completions.completion_move_tab - completion_runkey = completions.completion_runkey - completion_bookmark = completions.completion_bookmark - completion_remove_bookmark = completions.completion_remove_bookmark - completion_decline = completions.completion_decline - completion_bind = completions.completion_bind - completion_message = completions.completion_message - completion_invite = completions.completion_invite - completion_activity = completions.completion_activity - completion_mood = completions.completion_mood - completion_last_activity = completions.completion_last_activity - completion_server_cycle = completions.completion_server_cycle - completion_set = completions.completion_set - completion_set_default = completions.completion_set_default - completion_toggle = completions.completion_toggle - completion_bookmark_local = completions.completion_bookmark_local - - - -class KeyDict(dict): - """ - A dict, with a wrapper for get() that will return a custom value - if the key starts with _exc_ - """ - def get(self, k, d=None): - if isinstance(k, str) and k.startswith('_exc_') and len(k) > 5: - return lambda: dict.get(self, '_exc_')(k[5:]) - return dict.get(self, k, d) - -def replace_key_with_bound(key): - """ - Replace an inputted key with the one defined as its replacement - in the config - """ - bind = config.get(key, default=key, section='bindings') - if not bind: - bind = key - return bind - - diff --git a/src/core/handlers.py b/src/core/handlers.py deleted file mode 100644 index 8cc08179..00000000 --- a/src/core/handlers.py +++ /dev/null @@ -1,1354 +0,0 @@ -""" -XMPP-related handlers for the Core class -""" - -import logging -log = logging.getLogger(__name__) - -import asyncio -import curses -import functools -import ssl -import sys -import time -from datetime import datetime -from hashlib import sha1, sha512 -from os import path - -from slixmpp import InvalidJID -from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase -from xml.etree import ElementTree as ET - -import common -import fixes -import pep -import tabs -import windows -import xhtml -import multiuserchat as muc -from common import safeJID -from config import config, CACHE_DIR -from contact import Resource -from logger import logger -from roster import roster -from text_buffer import CorrectionError, AckError -from theming import dump_tuple, get_theme - -from . commands import dumb_callback - -try: - from pygments import highlight - from pygments.lexers import get_lexer_by_name - from pygments.formatters import HtmlFormatter - LEXER = get_lexer_by_name('xml') - FORMATTER = HtmlFormatter(noclasses=True) - PYGMENTS = True -except ImportError: - PYGMENTS = False - -def _join_initial_rooms(self, bookmarks): - """Join all rooms given in the iterator `bookmarks`""" - for bm in bookmarks: - if not (bm.autojoin or config.get('open_all_bookmarks')): - continue - tab = self.get_tab_by_name(bm.jid, tabs.MucTab) - nick = bm.nick if bm.nick else self.own_nick - if not tab: - self.open_new_room(bm.jid, nick, focus=False, - password=bm.password) - self.initial_joins.append(bm.jid) - # do not join rooms that do not have autojoin - # but display them anyway - if bm.autojoin: - muc.join_groupchat(self, bm.jid, nick, - passwd=bm.password, - status=self.status.message, - show=self.status.show) - -def check_bookmark_storage(self, features): - private = 'jabber:iq:private' in features - pep_ = 'http://jabber.org/protocol/pubsub#publish' in features - self.bookmarks.available_storage['private'] = private - self.bookmarks.available_storage['pep'] = pep_ - def _join_remote_only(iq): - if iq['type'] == 'error': - type_ = iq['error']['type'] - condition = iq['error']['condition'] - if not (type_ == 'cancel' and condition == 'item-not-found'): - self.information('Unable to fetch the remote' - ' bookmarks; %s: %s' % (type_, condition), - 'Error') - return - remote_bookmarks = self.bookmarks.remote() - _join_initial_rooms(self, remote_bookmarks) - if not self.xmpp.anon and config.get('use_remote_bookmarks'): - self.bookmarks.get_remote(self.xmpp, self.information, _join_remote_only) - -def on_session_start_features(self, _): - """ - Enable carbons & blocking on session start if wanted and possible - """ - def callback(iq): - if not iq: - return - features = iq['disco_info']['features'] - rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab) - rostertab.check_blocking(features) - rostertab.check_saslexternal(features) - if (config.get('enable_carbons') and - 'urn:xmpp:carbons:2' in features): - self.xmpp.plugin['xep_0280'].enable() - self.check_bookmark_storage(features) - - self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, - callback=callback) - -def on_carbon_received(self, message): - """ - Carbon <received/> received - """ - def ignore_message(recv): - log.debug('%s has category conference, ignoring carbon', - recv['from'].server) - def receive_message(recv): - recv['to'] = self.xmpp.boundjid.full - if recv['receipt']: - return self.on_receipt(recv) - self.on_normal_message(recv) - - recv = message['carbon_received'] - if (recv['from'].bare not in roster or - roster[recv['from'].bare].subscription == 'none'): - fixes.has_identity(self.xmpp, recv['from'].server, - identity='conference', - on_true=functools.partial(ignore_message, recv), - on_false=functools.partial(receive_message, recv)) - return - else: - receive_message(recv) - -def on_carbon_sent(self, message): - """ - Carbon <sent/> received - """ - def ignore_message(sent): - log.debug('%s has category conference, ignoring carbon', - sent['to'].server) - def send_message(sent): - sent['from'] = self.xmpp.boundjid.full - self.on_normal_message(sent) - - sent = message['carbon_sent'] - if (sent['to'].bare not in roster or - roster[sent['to'].bare].subscription == 'none'): - fixes.has_identity(self.xmpp, sent['to'].server, - identity='conference', - on_true=functools.partial(ignore_message, sent), - on_false=functools.partial(send_message, sent)) - else: - send_message(sent) - -### Invites ### - -def on_groupchat_invitation(self, message): - """ - Mediated invitation received - """ - jid = message['from'] - if jid.bare in self.pending_invites: - return - # there are 2 'x' tags in the messages, making message['x'] useless - invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite')) - inviter = invite['from'] - reason = invite['reason'] - password = invite['password'] - msg = "You are invited to the room %s by %s" % (jid.full, inviter.full) - if reason: - msg += "because: %s" % reason - if password: - msg += ". The password is \"%s\"." % password - self.information(msg, 'Info') - if 'invite' in config.get('beep_on').split(): - curses.beep() - logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full) - self.pending_invites[jid.bare] = inviter.full - -def on_groupchat_decline(self, decline): - "Mediated invitation declined; skip for now" - pass - -def on_groupchat_direct_invitation(self, message): - """ - Direct invitation received - """ - room = safeJID(message['groupchat_invite']['jid']) - if room.bare in self.pending_invites: - return - - inviter = message['from'] - reason = message['groupchat_invite']['reason'] - password = message['groupchat_invite']['password'] - continue_ = message['groupchat_invite']['continue'] - msg = "You are invited to the room %s by %s" % (room, inviter.full) - - if password: - msg += ' (password: "%s")' % password - if continue_: - msg += '\nto continue the discussion' - if reason: - msg += "\nreason: %s" % reason - - self.information(msg, 'Info') - if 'invite' in config.get('beep_on').split(): - curses.beep() - - self.pending_invites[room.bare] = inviter.full - logger.log_roster_change(inviter.full, 'invited you to %s' % room.bare) - -### "classic" messages ### - -def on_message(self, message): - """ - When receiving private message from a muc OR a normal message - (from one of our contacts) - """ - if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None: - return - if message['type'] == 'groupchat': - return - # Differentiate both type of messages, and call the appropriate handler. - jid_from = message['from'] - for tab in self.get_tabs(tabs.MucTab): - if tab.name == jid_from.bare: - if message['type'] == 'chat': - return self.on_groupchat_private_message(message) - return self.on_normal_message(message) - -def on_error_message(self, message): - """ - When receiving any message with type="error" - """ - jid_from = message['from'] - for tab in self.get_tabs(tabs.MucTab): - if tab.name == jid_from.bare: - if message['type'] == 'error': - return self.room_error(message, jid_from.bare) - else: - return self.on_groupchat_private_message(message) - tab = self.get_conversation_by_jid(message['from'], create=False) - error_msg = self.get_error_message(message, deprecated=True) - if not tab: - return self.information(error_msg, 'Error') - error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), - error_msg) - if not tab.nack_message('\n' + error, message['id'], message['to']): - tab.add_message(error, typ=0) - self.refresh_window() - - -def on_normal_message(self, message): - """ - When receiving "normal" messages (not a private message from a - muc participant) - """ - if message['type'] == 'error': - return - elif message['type'] == 'headline' and message['body']: - return self.information('%s says: %s' % (message['from'], message['body']), 'Headline') - - use_xhtml = config.get('enable_xhtml_im') - tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') - extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - if not body: - return - - remote_nick = '' - # normal message, we are the recipient - if message['to'].bare == self.xmpp.boundjid.bare: - conv_jid = message['from'] - jid = conv_jid - color = get_theme().COLOR_REMOTE_USER - # check for a name - if conv_jid.bare in roster: - remote_nick = roster[conv_jid.bare].name - # check for a received nick - if not remote_nick and config.get('enable_user_nick'): - if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None: - remote_nick = message['nick']['nick'] - if not remote_nick: - remote_nick = conv_jid.user - if not remote_nick: - remote_nick = conv_jid.full - own = False - # we wrote the message (happens with carbons) - elif message['from'].bare == self.xmpp.boundjid.bare: - conv_jid = message['to'] - jid = self.xmpp.boundjid - color = get_theme().COLOR_OWN_NICK - remote_nick = self.own_nick - own = True - # we are not part of that message, drop it - else: - return - - conversation = self.get_conversation_by_jid(conv_jid, create=True) - if isinstance(conversation, tabs.DynamicConversationTab) and conv_jid.resource: - conversation.lock(conv_jid.resource) - - if not own and not conversation.nick: - conversation.nick = remote_nick - elif not own: # keep a fixed nick during the whole conversation - remote_nick = conversation.nick - - self.events.trigger('conversation_msg', message, conversation) - if not message['body']: - return - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - delayed, date = common.find_delayed_tag(message) - - def try_modify(): - replaced_id = message['replace']['id'] - if replaced_id and config.get_by_tabname('group_corrections', - conv_jid.bare): - try: - conversation.modify_message(body, replaced_id, message['id'], jid=jid, - nickname=remote_nick) - return True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - return False - - if not try_modify(): - conversation.add_message(body, date, - nickname=remote_nick, - nick_color=color, - history=delayed, - identifier=message['id'], - jid=jid, - typ=1) - - if conversation.remote_wants_chatstates is None and not delayed: - if message['chat_state']: - conversation.remote_wants_chatstates = True - else: - conversation.remote_wants_chatstates = False - if not own and 'private' in config.get('beep_on').split(): - if not config.get_by_tabname('disable_beep', conv_jid.bare): - curses.beep() - if self.current_tab() is not conversation: - if not own: - conversation.state = 'private' - self.refresh_tab_win() - else: - conversation.set_state('normal') - self.refresh_tab_win() - else: - self.refresh_window() - -def on_nick_received(self, message): - """ - Called when a pep notification for an user nickname - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - item = message['pubsub_event']['items']['item'] - if item.xml.find('{http://jabber.org/protocol/nick}nick'): - contact.name = item['nick']['nick'] - else: - contact.name = '' - -def on_gaming_event(self, message): - """ - Called when a pep notification for user gaming - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - item = message['pubsub_event']['items']['item'] - old_gaming = contact.gaming - if item.xml.find('{urn:xmpp:gaming:0}gaming'): - item = item['gaming'] - # only name and server_address are used for now - contact.gaming = { - 'character_name': item['character_name'], - 'character_profile': item['character_profile'], - 'name': item['name'], - 'level': item['level'], - 'uri': item['uri'], - 'server_name': item['server_name'], - 'server_address': item['server_address'], - } - else: - contact.gaming = {} - - if contact.gaming: - logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming))) - - if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid): - if contact.gaming: - self.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming') - else: - self.information(contact.bare_jid + ' stopped playing.', 'Gaming') - -def on_mood_event(self, message): - """ - Called when a pep notification for an user mood - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_mood = contact.mood - if item.xml.find('{http://jabber.org/protocol/mood}mood'): - mood = item['mood']['value'] - if mood: - mood = pep.MOODS.get(mood, mood) - text = item['mood']['text'] - if text: - mood = '%s (%s)' % (mood, text) - contact.mood = mood - else: - contact.mood = '' - else: - contact.mood = '' - - if contact.mood: - logger.log_roster_change(contact.bare_jid, 'has now the mood: %s' % contact.mood) - - if old_mood != contact.mood and config.get_by_tabname('display_mood_notifications', contact.bare_jid): - if contact.mood: - self.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood') - else: - self.information(contact.bare_jid + ' stopped having his/her mood.', 'Mood') - -def on_activity_event(self, message): - """ - Called when a pep notification for an user activity - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_activity = contact.activity - if item.xml.find('{http://jabber.org/protocol/activity}activity'): - try: - activity = item['activity']['value'] - except ValueError: - return - if activity[0]: - general = pep.ACTIVITIES.get(activity[0]) - s = general['category'] - if activity[1]: - s = s + '/' + general.get(activity[1], 'other') - text = item['activity']['text'] - if text: - s = '%s (%s)' % (s, text) - contact.activity = s - else: - contact.activity = '' - else: - contact.activity = '' - - if contact.activity: - logger.log_roster_change(contact.bare_jid, 'has now the activity %s' % contact.activity) - - if old_activity != contact.activity and config.get_by_tabname('display_activity_notifications', contact.bare_jid): - if contact.activity: - self.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity') - else: - self.information(contact.bare_jid + ' stopped doing his/her activity.', 'Activity') - -def on_tune_event(self, message): - """ - Called when a pep notification for an user tune - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_tune = contact.tune - if item.xml.find('{http://jabber.org/protocol/tune}tune'): - item = item['tune'] - contact.tune = { - 'artist': item['artist'], - 'length': item['length'], - 'rating': item['rating'], - 'source': item['source'], - 'title': item['title'], - 'track': item['track'], - 'uri': item['uri'] - } - else: - contact.tune = {} - - if contact.tune: - logger.log_roster_change(message['from'].bare, 'is now listening to %s' % common.format_tune_string(contact.tune)) - - if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid): - if contact.tune: - self.information( - 'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune), - 'Tune') - else: - self.information(contact.bare_jid + ' stopped listening to music.', 'Tune') - -def on_groupchat_message(self, message): - """ - Triggered whenever a message is received from a multi-user chat room. - """ - if message['subject']: - return - room_from = message['from'].bare - - if message['type'] == 'error': # Check if it's an error - return self.room_error(message, room_from) - - tab = self.get_tab_by_name(room_from, tabs.MucTab) - if not tab: - self.information("message received for a non-existing room: %s" % (room_from)) - muc.leave_groupchat(self.xmpp, room_from, self.own_nick, msg='') - return - - nick_from = message['mucnick'] - user = tab.get_user_by_name(nick_from) - if user and user in tab.ignores: - return - - self.events.trigger('muc_msg', message, tab) - use_xhtml = config.get('enable_xhtml_im') - tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') - extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - if not body: - return - - old_state = tab.state - delayed, date = common.find_delayed_tag(message) - replaced_id = message['replace']['id'] - replaced = False - if replaced_id is not '' and config.get_by_tabname('group_corrections', - message['from'].bare): - try: - delayed_date = date or datetime.now() - if tab.modify_message(body, replaced_id, message['id'], - time=delayed_date, - nickname=nick_from, user=user): - self.events.trigger('highlight', message, tab) - replaced = True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - if not replaced and tab.add_message(body, date, nick_from, history=delayed, identifier=message['id'], jid=message['from'], typ=1): - self.events.trigger('highlight', message, tab) - - if message['from'].resource == tab.own_nick: - tab.last_sent_message = message - - if tab is self.current_tab(): - tab.text_win.refresh() - tab.info_header.refresh(tab, tab.text_win) - tab.input.refresh() - self.doupdate() - elif tab.state != old_state: - self.refresh_tab_win() - current = self.current_tab() - if hasattr(current, 'input') and current.input: - current.input.refresh() - self.doupdate() - - if 'message' in config.get('beep_on').split(): - if (not config.get_by_tabname('disable_beep', room_from) - and self.own_nick != message['from'].resource): - curses.beep() - -def on_muc_own_nickchange(self, muc): - "We changed our nick in a MUC" - for tab in self.get_tabs(tabs.PrivateTab): - if tab.parent_muc == muc: - tab.own_nick = muc.own_nick - -def on_groupchat_private_message(self, message): - """ - We received a Private Message (from someone in a Muc) - """ - jid = message['from'] - nick_from = jid.resource - if not nick_from: - return self.on_groupchat_message(message) - - room_from = jid.bare - use_xhtml = config.get('enable_xhtml_im') - tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images') - extract_images = config.get('extract_inline_images') - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation - ignore = config.get_by_tabname('ignore_private', room_from) - if not tab: # It's the first message we receive: create the tab - if body and not ignore: - tab = self.open_private_window(room_from, nick_from, False) - if ignore: - self.events.trigger('ignored_private', message, tab) - msg = config.get_by_tabname('private_auto_response', room_from) - if msg and body: - self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') - return - self.events.trigger('private_msg', message, tab) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml, - tmp_dir=tmp_dir, - extract_images=extract_images) - if not body or not tab: - return - replaced_id = message['replace']['id'] - replaced = False - user = tab.parent_muc.get_user_by_name(nick_from) - if replaced_id is not '' and config.get_by_tabname('group_corrections', - room_from): - try: - tab.modify_message(body, replaced_id, message['id'], user=user, jid=message['from'], - nickname=nick_from) - replaced = True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - if not replaced: - tab.add_message(body, time=None, nickname=nick_from, - forced_user=user, - identifier=message['id'], - jid=message['from'], - typ=1) - - if tab.remote_wants_chatstates is None: - if message['chat_state']: - tab.remote_wants_chatstates = True - else: - tab.remote_wants_chatstates = False - if 'private' in config.get('beep_on').split(): - if not config.get_by_tabname('disable_beep', jid.full): - curses.beep() - if tab is self.current_tab(): - self.refresh_window() - else: - tab.state = 'private' - self.refresh_tab_win() - -### Chatstates ### - -def on_chatstate_active(self, message): - self.on_chatstate(message, "active") - -def on_chatstate_inactive(self, message): - self.on_chatstate(message, "inactive") - -def on_chatstate_composing(self, message): - self.on_chatstate(message, "composing") - -def on_chatstate_paused(self, message): - self.on_chatstate(message, "paused") - -def on_chatstate_gone(self, message): - self.on_chatstate(message, "gone") - -def on_chatstate(self, message, state): - if message['type'] == 'chat': - if not self.on_chatstate_normal_conversation(message, state): - tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) - if not tab: - return - self.on_chatstate_private_conversation(message, state) - elif message['type'] == 'groupchat': - self.on_chatstate_groupchat_conversation(message, state) - -def on_chatstate_normal_conversation(self, message, state): - tab = self.get_conversation_by_jid(message['from'], False) - if not tab: - return False - tab.remote_wants_chatstates = True - self.events.trigger('normal_chatstate', message, tab) - tab.chatstate = state - if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab): - tab.unlock() - if tab == self.current_tab(): - tab.refresh_info_header() - self.doupdate() - else: - _composing_tab_state(tab, state) - self.refresh_tab_win() - return True - -def on_chatstate_private_conversation(self, message, state): - """ - Chatstate received in a private conversation from a MUC - """ - tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) - if not tab: - return - tab.remote_wants_chatstates = True - self.events.trigger('private_chatstate', message, tab) - tab.chatstate = state - if tab == self.current_tab(): - tab.refresh_info_header() - self.doupdate() - else: - _composing_tab_state(tab, state) - self.refresh_tab_win() - return True - -def on_chatstate_groupchat_conversation(self, message, state): - """ - Chatstate received in a MUC - """ - nick = message['mucnick'] - room_from = message.get_mucroom() - tab = self.get_tab_by_name(room_from, tabs.MucTab) - if tab and tab.get_user_by_name(nick): - self.events.trigger('muc_chatstate', message, tab) - tab.get_user_by_name(nick).chatstate = state - if tab == self.current_tab(): - if not self.size.tab_degrade_x: - tab.user_win.refresh(tab.users) - tab.input.refresh() - self.doupdate() - else: - _composing_tab_state(tab, state) - self.refresh_tab_win() - -### subscription-related handlers ### - -def on_roster_update(self, iq): - """ - The roster was received. - """ - for item in iq['roster']: - try: - jid = item['jid'] - except InvalidJID: - jid = item._get_attr('jid', '') - log.error('Invalid JID: "%s"', jid, exc_info=True) - else: - if item['subscription'] == 'remove': - del roster[jid] - else: - roster.update_contact_groups(jid) - roster.update_size() - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_subscription_request(self, presence): - """subscribe received""" - jid = presence['from'].bare - contact = roster[jid] - if contact and contact.subscription in ('from', 'both'): - return - elif contact and contact.subscription == 'to': - self.xmpp.sendPresence(pto=jid, ptype='subscribed') - self.xmpp.sendPresence(pto=jid) - else: - if not contact: - contact = roster.get_and_set(jid) - roster.update_contact_groups(contact) - contact.pending_in = True - self.information('%s wants to subscribe to your presence, use ' - '/accept <jid> or /deny <jid> in the roster ' - 'tab to accept or reject the query.' % jid, - 'Roster') - self.get_tab_by_number(0).state = 'highlight' - roster.modified() - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_subscription_authorized(self, presence): - """subscribed received""" - jid = presence['from'].bare - contact = roster[jid] - if contact.subscription not in ('both', 'from'): - self.information('%s accepted your contact proposal' % jid, 'Roster') - if contact.pending_out: - contact.pending_out = False - - roster.modified() - - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_subscription_remove(self, presence): - """unsubscribe received""" - jid = presence['from'].bare - contact = roster[jid] - if not contact: - return - roster.modified() - self.information('%s does not want to receive your status anymore.' % jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_subscription_removed(self, presence): - """unsubscribed received""" - jid = presence['from'].bare - contact = roster[jid] - if not contact: - return - roster.modified() - if contact.pending_out: - self.information('%s rejected your contact proposal' % jid, 'Roster') - contact.pending_out = False - else: - self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -### Presence-related handlers ### - -def on_presence(self, presence): - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'): - return - jid = presence['from'] - contact = roster[jid.bare] - tab = self.get_conversation_by_jid(jid, create=False) - if isinstance(tab, tabs.DynamicConversationTab): - if tab.get_dest_jid() != jid.full: - tab.unlock(from_=jid.full) - elif presence['type'] == 'unavailable': - tab.unlock() - if contact is None: - return - roster.modified() - contact.error = None - self.events.trigger('normal_presence', presence, contact[jid.full]) - tab = self.get_conversation_by_jid(jid, create=False) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - elif self.current_tab() == tab: - tab.refresh() - self.doupdate() - -def on_presence_error(self, presence): - jid = presence['from'] - contact = roster[jid.bare] - if not contact: - return - roster.modified() - contact.error = presence['error']['type'] + ': ' + presence['error']['condition'] - # reset chat states status on presence error - tab = self.get_tab_by_name(jid.full, tabs.ConversationTab) - if tab: - tab.remote_wants_chatstates = None - -def on_got_offline(self, presence): - """ - A JID got offline - """ - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'): - return - jid = presence['from'] - if not logger.log_roster_change(jid.bare, 'got offline'): - self.information('Unable to write in the log file', 'Error') - # If a resource got offline, display the message in the conversation with this - # precise resource. - contact = roster[jid.bare] - name = jid.bare - if contact: - roster.connected -= 1 - if contact.name: - name = contact.name - if jid.resource: - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % name) - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % name) - self.information('\x193}%s \x195}is \x191}offline' % name, 'Roster') - roster.modified() - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_got_online(self, presence): - """ - A JID got online - """ - if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'): - return - jid = presence['from'] - contact = roster[jid.bare] - if contact is None: - # Todo, handle presence coming from contacts not in roster - return - roster.connected += 1 - roster.modified() - if not logger.log_roster_change(jid.bare, 'got online'): - self.information('Unable to write in the log file', 'Error') - resource = Resource(jid.full, { - 'priority': presence.get_priority() or 0, - 'status': presence['status'], - 'show': presence['show'], - }) - self.events.trigger('normal_presence', presence, resource) - name = contact.name if contact.name else jid.bare - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % name) - if time.time() - self.connection_time > 10: - # We do not display messages if we recently logged in - if presence['status']: - self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (name, presence['status']), "Roster") - else: - self.information("\x193}%s \x195}is \x194}online\x195}" % name, "Roster") - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % name) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - -def on_groupchat_presence(self, presence): - """ - Triggered whenever a presence stanza is received from a user in a multi-user chat room. - Display the presence on the room window and update the - presence information of the concerned user - """ - from_room = presence['from'].bare - tab = self.get_tab_by_name(from_room, tabs.MucTab) - if tab: - self.events.trigger('muc_presence', presence, tab) - tab.handle_presence(presence) - - -### Connection-related handlers ### - -def on_failed_connection(self, error): - """ - We cannot contact the remote server - """ - self.information("Connection to remote server failed: %s" % (error,), 'Error') - -def on_disconnected(self, event): - """ - When we are disconnected from remote server - """ - roster.connected = 0 - # Stop the ping plugin. It would try to send stanza on regular basis - self.xmpp.plugin['xep_0199'].disable_keepalive() - roster.modified() - for tab in self.get_tabs(tabs.MucTab): - tab.disconnect() - msg_typ = 'Error' if not self.legitimate_disconnect else 'Info' - self.information("Disconnected from server.", msg_typ) - if not self.legitimate_disconnect and config.get('auto_reconnect', True): - self.information("Auto-reconnecting.", 'Info') - self.xmpp.start() - -def on_stream_error(self, event): - """ - When we receive a stream error - """ - if event and event['text']: - self.information('Stream error: %s' % event['text'], 'Error') - -def on_failed_all_auth(self, event): - """ - Authentication failed - """ - self.information("Authentication failed (bad credentials?).", - 'Error') - self.legitimate_disconnect = True - -def on_no_auth(self, event): - """ - Authentication failed (no mech) - """ - self.information("Authentication failed, no login method available.", - 'Error') - self.legitimate_disconnect = True - -def on_connected(self, event): - """ - Remote host responded, but we are not yet authenticated - """ - self.information("Connected to server.", 'Info') - -def on_connecting(self, event): - """ - Just before we try to connect to the server - """ - self.legitimate_disconnect = False - -def on_session_start(self, event): - """ - Called when we are connected and authenticated - """ - self.connection_time = time.time() - if not self.plugins_autoloaded: # Do not reload plugins on reconnection - self.autoload_plugins() - self.information("Authentication success.", 'Info') - self.information("Your JID is %s" % self.xmpp.boundjid.full, 'Info') - if not self.xmpp.anon: - # request the roster - self.xmpp.get_roster() - roster.update_contact_groups(self.xmpp.boundjid.bare) - # send initial presence - if config.get('send_initial_presence'): - pres = self.xmpp.make_presence() - pres['show'] = self.status.show - pres['status'] = self.status.message - self.events.trigger('send_normal_presence', pres) - pres.send() - self.bookmarks.get_local() - # join all the available bookmarks. As of yet, this is just the local ones - _join_initial_rooms(self, self.bookmarks) - - if config.get('enable_user_nick'): - self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback) - asyncio.async(self.xmpp.plugin['xep_0115'].update_caps()) - # Start the ping's plugin regular event - self.xmpp.set_keepalive_values() - -### Other handlers ### - -def on_status_codes(self, message): - """ - Handle groupchat messages with status codes. - Those are received when a room configuration change occurs. - """ - room_from = message['from'] - tab = self.get_tab_by_name(room_from, tabs.MucTab) - status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))]) - if '101' in status_codes: - self.information('Your affiliation in the room %s changed' % room_from, 'Info') - elif tab and status_codes: - show_unavailable = '102' in status_codes - hide_unavailable = '103' in status_codes - non_priv = '104' in status_codes - logging_on = '170' in status_codes - logging_off = '171' in status_codes - non_anon = '172' in status_codes - semi_anon = '173' in status_codes - full_anon = '174' in status_codes - modif = False - if show_unavailable or hide_unavailable or non_priv or logging_off\ - or non_anon or semi_anon or full_anon: - tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - modif = True - if show_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - elif hide_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - if non_anon: - tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - elif semi_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - elif full_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - if logging_on: - tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - elif logging_off: - tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - if modif: - self.refresh_window() - -def on_groupchat_subject(self, message): - """ - Triggered when the topic is changed. - """ - nick_from = message['mucnick'] - room_from = message.get_mucroom() - tab = self.get_tab_by_name(room_from, tabs.MucTab) - subject = message['subject'] - if subject is None or not tab: - return - if subject != tab.topic: - # Do not display the message if the subject did not change or if we - # receive an empty topic when joining the room. - if nick_from: - tab.add_message("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s" % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject}, - time=None, - typ=2) - else: - tab.add_message("\x19%(info_col)s}The subject is: %(subject)s" % - {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - time=None, - typ=2) - tab.topic = subject - tab.topic_from = nick_from - if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): - self.refresh_window() - -def on_receipt(self, message): - """ - When a delivery receipt is received (XEP-0184) - """ - jid = message['from'] - msg_id = message['receipt'] - if not msg_id: - return - - conversation = self.get_tab_by_name(jid, tabs.ChatTab) - conversation = conversation or self.get_tab_by_name(jid.bare, tabs.ChatTab) - if not conversation: - return - - try: - conversation.ack_message(msg_id, self.xmpp.boundjid) - except AckError: - log.debug('Error while receiving an ack', exc_info=True) - -def on_data_form(self, message): - """ - When a data form is received - """ - self.information('%s' % message) - -def on_attention(self, message): - """ - Attention probe received. - """ - jid_from = message['from'] - self.information('%s requests your attention!' % jid_from, 'Info') - for tab in self.tabs: - if tab.name == jid_from: - tab.state = 'attention' - self.refresh_tab_win() - return - for tab in self.tabs: - if tab.name == jid_from.bare: - tab.state = 'attention' - self.refresh_tab_win() - return - self.information('%s tab not found.' % jid_from, 'Error') - -def room_error(self, error, room_name): - """ - Display the error in the tab - """ - tab = self.get_tab_by_name(room_name, tabs.MucTab) - if not tab: - return - error_message = self.get_error_message(error) - tab.add_message(error_message, highlight=True, nickname='Error', - nick_color=get_theme().COLOR_ERROR_MSG, typ=2) - code = error['error']['code'] - if code == '401': - msg = 'To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)' - tab.add_message(msg, typ=2) - if code == '409': - if config.get('alternative_nickname') != '': - self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname'))) - else: - if not tab.joined: - tab.add_message('You can join the room with an other nick, by typing "/join /other_nick"', typ=2) - self.refresh_window() - -def outgoing_stanza(self, stanza): - """ - We are sending a new stanza, write it in the xml buffer if needed. - """ - if self.xml_tab: - if PYGMENTS: - xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER) - poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip() - else: - poezio_colored = '%s' % stanza - self.add_message_to_text_buffer(self.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) - try: - if self.xml_tab.match_stanza(ElementBase(ET.fromstring(stanza))): - self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_OUT) - except: - log.debug('', exc_info=True) - - if isinstance(self.current_tab(), tabs.XMLTab): - self.current_tab().refresh() - self.doupdate() - -def incoming_stanza(self, stanza): - """ - We are receiving a new stanza, write it in the xml buffer if needed. - """ - if self.xml_tab: - if PYGMENTS: - xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER) - poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip() - else: - poezio_colored = '%s' % stanza - self.add_message_to_text_buffer(self.xml_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) - try: - if self.xml_tab.match_stanza(stanza): - self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored, - nickname=get_theme().CHAR_XML_IN) - except: - log.debug('', exc_info=True) - if isinstance(self.current_tab(), tabs.XMLTab): - self.current_tab().refresh() - self.doupdate() - -def ssl_invalid_chain(self, tb): - self.information('The certificate sent by the server is invalid.', 'Error') - self.disconnect() - -def validate_ssl(self, pem): - """ - Check the server certificate using the slixmpp ssl_cert event - """ - if config.get('ignore_certificate'): - return - cert = config.get('certificate') - # update the cert representation when it uses the old one - if cert and not ':' in cert: - cert = ':'.join(i + j for i, j in zip(cert[::2], cert[1::2])).upper() - config.set_and_save('certificate', cert) - - der = ssl.PEM_cert_to_DER_cert(pem) - sha1_digest = sha1(der).hexdigest().upper() - sha1_found_cert = ':'.join(i + j for i, j in zip(sha1_digest[::2], sha1_digest[1::2])) - sha2_digest = sha512(der).hexdigest().upper() - sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2])) - if cert: - if sha1_found_cert == cert: - log.debug('Cert %s OK', sha1_found_cert) - log.debug('Current hash is SHA-1, moving to SHA-2 (%s)', - sha2_found_cert) - config.set_and_save('certificate', sha2_found_cert) - return - elif sha2_found_cert == cert: - log.debug('Cert %s OK', sha2_found_cert) - return - else: - saved_input = self.current_tab().input - log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, sha2_found_cert) - self.information('New certificate found (sha-2 hash:' - ' %s)\nPlease validate or abort' % sha2_found_cert, - 'Warning') - def check_input(): - self.current_tab().input = saved_input - if input.value: - self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info') - log.debug('Setting certificate to %s', sha2_found_cert) - if not config.silent_set('certificate', sha2_found_cert): - self.information('Unable to write in the config file', 'Error') - else: - self.information('You refused to validate the certificate. You are now disconnected', 'Info') - self.disconnect() - new_loop.stop() - asyncio.set_event_loop(old_loop) - input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n)", callback=check_input) - self.current_tab().input = input - input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) - input.refresh() - self.doupdate() - old_loop = asyncio.get_event_loop() - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - new_loop.add_reader(sys.stdin, self.on_input_readable) - curses.beep() - new_loop.run_forever() - else: - log.debug('First time. Setting certificate to %s', sha2_found_cert) - if not config.silent_set('certificate', sha2_found_cert): - self.information('Unable to write in the config file', 'Error') - -def _composing_tab_state(tab, state): - """ - Set a tab state to or from the "composing" state - according to the config and the current tab state - """ - if isinstance(tab, tabs.MucTab): - values = ('true', 'muc') - elif isinstance(tab, tabs.PrivateTab): - values = ('true', 'direct', 'private') - elif isinstance(tab, tabs.ConversationTab): - values = ('true', 'direct', 'conversation') - else: - return # should not happen - - show = config.get('show_composing_tabs') - show = show in values - - if tab.state != 'composing' and state == 'composing': - if show: - if tabs.STATE_PRIORITY[tab.state] > tabs.STATE_PRIORITY[state]: - return - tab.save_state() - tab.state = 'composing' - elif tab.state == 'composing' and state != 'composing': - tab.restore_state() - -### Ad-hoc commands - -def on_next_adhoc_step(self, iq, adhoc_session): - status = iq['command']['status'] - xform = iq.xml.find('{http://jabber.org/protocol/commands}command/{jabber:x:data}x') - if xform is not None: - form = self.xmpp.plugin['xep_0004'].buildForm(xform) - else: - form = None - - if status == 'error': - return self.information("An error occured while executing the command") - - if status == 'executing': - if not form: - self.information("Adhoc command step does not contain a data-form. Aborting the execution.", "Error") - return self.xmpp.plugin['xep_0050'].cancel_command(adhoc_session) - on_validate = self.validate_adhoc_step - on_cancel = self.cancel_adhoc_command - if status == 'completed': - on_validate = lambda form, session: self.close_tab() - on_cancel = lambda form, session: self.close_tab() - - # If a form is available, use it, and add the Notes from the - # response to it, if any - if form: - for note in iq['command']['notes']: - form.add_field(type='fixed', label=note[1]) - self.open_new_form(form, on_cancel, on_validate, - session=adhoc_session) - else: # otherwise, just display an information - # message - notes = '\n'.join([note[1] for note in iq['command']['notes']]) - self.information("Adhoc command %s: %s" % (status, notes), "Info") - -def on_adhoc_error(self, iq, adhoc_session): - self.xmpp.plugin['xep_0050'].terminate_command(adhoc_session) - error_message = self.get_error_message(iq) - self.information("An error occured while executing the command: %s" % (error_message), - 'Error') - -def cancel_adhoc_command(self, form, session): - self.xmpp.plugin['xep_0050'].cancel_command(session) - self.close_tab() - -def validate_adhoc_step(self, form, session): - session['payload'] = form - self.xmpp.plugin['xep_0050'].continue_command(session) - self.close_tab() - -def terminate_adhoc_command(self, form, session): - self.xmpp.plugin['xep_0050'].terminate_command(session) - self.close_tab() diff --git a/src/core/structs.py b/src/core/structs.py deleted file mode 100644 index 4ce0ef43..00000000 --- a/src/core/structs.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Module defining structures useful to the core class and related methods -""" -import collections - -# http://xmpp.org/extensions/xep-0045.html#errorstatus -ERROR_AND_STATUS_CODES = { - '401': 'A password is required', - '403': 'Permission denied', - '404': 'The room doesn’t exist', - '405': 'Your are not allowed to create a new room', - '406': 'A reserved nick must be used', - '407': 'You are not in the member list', - '409': 'This nickname is already in use or has been reserved', - '503': 'The maximum number of users has been reached', - } - -# http://xmpp.org/extensions/xep-0086.html -DEPRECATED_ERRORS = { - '302': 'Redirect', - '400': 'Bad request', - '401': 'Not authorized', - '402': 'Payment required', - '403': 'Forbidden', - '404': 'Not found', - '405': 'Not allowed', - '406': 'Not acceptable', - '407': 'Registration required', - '408': 'Request timeout', - '409': 'Conflict', - '500': 'Internal server error', - '501': 'Feature not implemented', - '502': 'Remote server error', - '503': 'Service unavailable', - '504': 'Remote server timeout', - '510': 'Disconnected', -} - -possible_show = {'available':None, - 'chat':'chat', - 'away':'away', - 'afk':'away', - 'dnd':'dnd', - 'busy':'dnd', - 'xa':'xa' - } - -Status = collections.namedtuple('Status', 'show message') -Command = collections.namedtuple('Command', 'func desc comp short usage') diff --git a/src/daemon.py b/src/daemon.py deleted file mode 100755 index 6325d8df..00000000 --- a/src/daemon.py +++ /dev/null @@ -1,82 +0,0 @@ -#/usr/bin/env python3 -# Copyright 2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -This file is a standalone program that reads commands on -stdin and executes them (each line should be a command). - -Usage: cat some_fifo | ./daemon.py - -Poezio writes commands in the fifo, and this daemon executes them on the -local machine. -Note that you should not start this daemon if you do not trust the remote -machine that is running poezio, since this could make it run any (dangerous) -command on your local machine. -""" - -import sys -import threading -import subprocess -import shlex -import logging - -from subprocess import DEVNULL - -log = logging.getLogger(__name__) - -class Executor(threading.Thread): - """ - Just a class to execute commands in a thread. This way, the execution - can totally fail, we don’t care, and we can start commands without - having to wait for them to return. - WARNING: Be careful to properly escape what is untrusted by using - pipes.quote (or shlex.quote with python 3.3) for example. - """ - def __init__(self, command, remote=False): - threading.Thread.__init__(self) - self.command = command - self.remote = remote - # check for > or >> special case - self.filename = None - self.redirection_mode = 'w' - if len(command) >= 3: - if command[-2] in ('>', '>>'): - self.filename = command.pop(-1) - if command[-1] == '>>': - self.redirection_mode = 'a' - command.pop(-1) - - def run(self): - log.debug('executing %s', self.command) - stdout = DEVNULL - if self.filename: - try: - stdout = open(self.filename, self.redirection_mode) - except (OSError, IOError): - log.error('Could not open redirection file: %s', self.filename, exc_info=True) - return - try: - subprocess.call(self.command, stdout=stdout, stderr=DEVNULL) - except: - if self.remote: - import traceback - print(traceback.format_exc()) - else: - log.error('Could not execute %s:', self.command, exc_info=True) - -def main(): - while True: - line = sys.stdin.readline() - if line == '': - break - command = shlex.split(line) - e = Executor(command, remote=True) - e.start() - -if __name__ == '__main__': - main() diff --git a/src/decorators.py b/src/decorators.py deleted file mode 100644 index c4ea6563..00000000 --- a/src/decorators.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Module containing various decorators -""" - -import common - -class RefreshWrapper(object): - def __init__(self): - self.core = None - - def conditional(self, func): - """ - Decorator to refresh the UI if the wrapped function - returns True - """ - def wrap(*args, **kwargs): - ret = func(*args, **kwargs) - if self.core and ret: - self.core.refresh_window() - return ret - return wrap - - def always(self, func): - """ - Decorator that refreshs the UI no matter what after the function - """ - def wrap(*args, **kwargs): - ret = func(*args, **kwargs) - if self.core: - self.core.refresh_window() - return ret - return wrap - - def update(self, func): - """ - Decorator that only updates the screen - """ - def wrap(*args, **kwargs): - ret = func(*args, **kwargs) - if self.core: - self.core.doupdate() - return ret - return wrap - -refresh_wrapper = RefreshWrapper() - -class CommandArgParser(object): - """Modify the string argument of the function into a list of strings - containing the right number of extracted arguments, or None if we don’t - have enough. - """ - @staticmethod - def raw(func): - """Just call the function with a single string, which is the original string - untouched - """ - def wrap(self, args, *a, **kw): - return func(self, args, *a, **kw) - return wrap - - @staticmethod - def ignored(func): - """ - Call the function without any argument - """ - def wrap(self, args, *a, **kw): - return func(self, *a, **kw) - return wrap - - @staticmethod - def quoted(mandatory, optional=0, defaults=[], - ignore_trailing_arguments=False): - - """The function receives a list with a number of arguments that is between - the numbers `mandatory` and `optional`. - - If the string doesn’t contain at least `mandatory` arguments, we return - None because the given arguments are invalid. - - If there are any remaining arguments after `mandatory` and `optional` - arguments have been found (and “ignore_trailing_arguments" is not True), - we happen them to the last argument of the list. - - An argument is a string (with or without whitespaces) between to quotes - ("), or a whitespace separated word (if not inside quotes). - - The argument `defaults` is a list of strings that are used when an - optional argument is missing. For example if we accept one optional - argument, zero is available but we have one value in the `defaults` - list, we use that string inplace. The `defaults` list can only - replace missing optional arguments, not mandatory ones. And it - should not contain more than `mandatory` values. Also you cannot - - Example: - This method needs at least one argument, and accepts up to 3 - arguments - - >> @command_args_parser.quoted(1, 2, ['default for first arg'], False) - >> def f(args): - >> print(args) - - >> f('coucou les amis') # We have one mandatory and two optional - ['coucou', 'les', 'amis'] - >> f('"coucou les amis" "PROUT PROUT"') # One mandator and only one optional, - # no default for the second - ['coucou les amis', 'PROUT PROUT'] - >> f('') # Not enough args for mandatory number - None - >> f('"coucou les potes"') # One mandatory, and use the default value - # for the first optional - ['coucou les potes, 'default for first arg'] - >> f('"un et demi" deux trois quatre cinq six') # We have three trailing arguments - ['un et demi', 'deux', 'trois quatre cinq six'] - - """ - def first(func): - def second(self, args, *a, **kw): - default_args = defaults - args = common.shell_split(args) - if len(args) < mandatory: - return func(self, None, *a, **kw) - res, args = args[:mandatory], args[mandatory:] - if optional == -1: - opt_args = args[:] - else: - opt_args = args[:optional] - - if opt_args: - res += opt_args - args = args[len(opt_args):] - default_args = default_args[len(opt_args):] - res += default_args - if args and res and not ignore_trailing_arguments: - res[-1] += " " + " ".join(args) - return func(self, res, *a, **kw) - return second - return first - -command_args_parser = CommandArgParser() diff --git a/src/events.py b/src/events.py deleted file mode 100644 index 15ef3e35..00000000 --- a/src/events.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# 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 EventHandler class. -The list of available events is here: -http://poezio.eu/doc/en/plugins.html#_poezio_events -""" - -class EventHandler(object): - """ - A class keeping a list of possible events that are triggered - by poezio. You (a plugin for example) can add an event handler - associated with an event name, and whenever that event is triggered, - the callback is called. - """ - def __init__(self): - self.events = { - 'highlight': [], - 'muc_say': [], - 'muc_say_after': [], - 'conversation_say': [], - 'conversation_say_after': [], - 'private_say': [], - 'private_say_after': [], - 'conversation_msg': [], - 'private_msg': [], - 'muc_msg': [], - 'conversation_chatstate': [], - 'muc_chatstate': [], - 'private_chatstate': [], - 'normal_presence': [], - 'muc_presence': [], - 'muc_join': [], - 'joining_muc': [], - 'changing_nick': [], - 'muc_kick': [], - 'muc_nickchange': [], - 'muc_ban': [], - 'send_normal_presence': [], - 'ignored_private': [], - 'tab_change': [], - } - - def add_event_handler(self, name, callback, position=0): - """ - Add a callback to a given event. - Note that if that event name doesn’t exist, it just returns False. - If it was successfully added, it returns True - position: 0 means insert at the beginning, -1 means end - """ - if name not in self.events: - return False - - if position >= 0: - self.events[name].insert(position, callback) - else: - self.events[name].append(callback) - - return True - - def trigger(self, name, *args, **kwargs): - """ - Call all the callbacks associated to the given event name. - """ - callbacks = self.events.get(name, None) - if callbacks is None: - return - for callback in callbacks: - callback(*args, **kwargs) - - def del_event_handler(self, name, callback): - """ - Remove the callback from the list of callbacks of the given event - """ - if not name: - for event in self.events: - while callback in self.events[event]: - self.events[event].remove(callback) - return True - else: - if callback in self.events[name]: - self.events[name].remove(callback) - diff --git a/src/fifo.py b/src/fifo.py deleted file mode 100644 index 863ef228..00000000 --- a/src/fifo.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Defines the Fifo class - -This fifo allows simple communication between a remote poezio -and a local computer, with ssh+cat. -""" - -import logging -log = logging.getLogger(__name__) - -import os -import threading - -class OpenTrick(threading.Thread): - """ - A threaded trick to make the open for writing succeed. - A fifo cannot be opened for writing if it has not been - yet opened by the other hand for reading. - So, we just open the fifo for reading and we do not close - it afterwards, because if the other reader disconnects, - we will receive a SIGPIPE. And we do not want that. - - (we never read anything from it, obviously) - """ - def __init__(self, path): - threading.Thread.__init__(self) - self.path = path - self.fd = None - - def run(self): - self.fd = open(self.path, 'r', encoding='utf-8') - - -class Fifo(object): - """ - Just a simple file handler, writing and reading in a fifo. - Mode is either 'r' or 'w', just like the mode for the open() - function. - """ - def __init__(self, path, mode): - self.trick = None - if not os.path.exists(path): - os.mkfifo(path) - if mode == 'w': - self.trick = OpenTrick(path) - # that thread will wait until we open it for writing - self.trick.start() - self.fd = open(path, mode, encoding='utf-8') - - def write(self, data): - """ - Try to write on the fifo. If that fails, this means - that nothing has that fifo opened, so the writing is useless, - so we just return (and display an error telling that, somewhere). - """ - self.fd.write(data) - self.fd.flush() - - def readline(self): - "Read a line from the fifo" - return self.fd.readline() - - def __del__(self): - "Close opened fds" - try: - self.fd.close() - if self.trick: - self.trick.fd.close() - except: - log.error('Unable to close descriptors for the fifo', - exc_info=True) diff --git a/src/fixes.py b/src/fixes.py deleted file mode 100644 index 3840a093..00000000 --- a/src/fixes.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Module used to provide fixes for slixmpp functions not yet fixed -upstream. - -TODO: Check that they are fixed and remove those hacks -""" - -from slixmpp.stanza import Message -from slixmpp.xmlstream import ET - -import logging - -log = logging.getLogger(__name__) - -def has_identity(xmpp, jid, identity, on_true=None, on_false=None): - def _cb(iq): - ident = lambda x: x[0] - res = identity in map(ident, iq['disco_info']['identities']) - if res and on_true is not None: - on_true() - if not res and on_false is not None: - on_false() - xmpp.plugin['xep_0030'].get_info(jid=jid, callback=_cb) - -def get_version(xmpp, jid, callback=None, **kwargs): - def handle_result(res): - if res and res['type'] != 'error': - ret = res['software_version'].values - else: - ret = False - if callback: - callback(ret) - return ret - iq = xmpp.make_iq_get(ito=jid) - iq['query'] = 'jabber:iq:version' - result = iq.send(callback=handle_result if callback else None) - if not callback: - return handle_result(result) - - -def get_room_form(xmpp, room, callback): - def _cb(result): - if result["type"] == "error": - return callback(None) - xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') - if xform is None: - return callback(None) - form = xmpp.plugin['xep_0004'].buildForm(xform) - return callback(form) - - iq = xmpp.make_iq_get(ito=room) - query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - iq.append(query) - iq.send(callback=_cb) - -def _filter_add_receipt_request(self, stanza): - """ - Auto add receipt requests to outgoing messages, if: - - - ``self.auto_request`` is set to ``True`` - - The message is not for groupchat - - The message does not contain a receipt acknowledgment - - The recipient is a bare JID or, if a full JID, one - that has the ``urn:xmpp:receipts`` feature enabled - - The message has a body - - The disco cache is checked if a full JID is specified in - the outgoing message, which may mean a round-trip disco#info - delay for the first message sent to the JID if entity caps - are not used. - """ - - if not self.auto_request: - return stanza - - if not isinstance(stanza, Message): - return stanza - - if stanza['request_receipt']: - return stanza - - if not stanza['type'] in self.ack_types: - return stanza - - if stanza['receipt']: - return stanza - - if not stanza['body']: - return stanza - - # hack - if stanza['to'].resource and not hasattr(stanza, '_add_receipt'): - return stanza - - stanza['request_receipt'] = True - return stanza - diff --git a/src/keyboard.py b/src/keyboard.py deleted file mode 100755 index ccf9e752..00000000 --- a/src/keyboard.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -Functions to interact with the keyboard -Mainly, read keys entered and return a string (most -of the time ONE char, but may be longer if it's a keyboard -shortcut, like ^A, M-a or KEY_RESIZE) -""" - -import curses -import curses.ascii -import logging -log = logging.getLogger(__name__) - -# A callback that will handle the next key entered by the user. For -# example if the user presses Ctrl+j, we set a callbacks, and the -# next key pressed by the user will be passed to this callback -# instead of the normal process of executing global keybard -# shortcuts or inserting text in the current output. The callback -# is always reset to None afterwards (to resume the normal -# processing of keys) -continuation_keys_callback = None - -def get_next_byte(s): - """ - Read the next byte of the utf-8 char - ncurses seems to return a string of the byte - encoded in latin-1. So what we get is NOT what we typed - unless we do the conversion… - """ - try: - c = s.getkey() - except: - return (None, None) - if len(c) >= 4: - return (None, c) - return (ord(c), c.encode('latin-1')) # returns a number and a bytes object - -def get_char_list(s): - ret_list = [] - while True: - try: - key = s.get_wch() - except curses.error: - # No input, this means a timeout occurs. - return ret_list - except ValueError: # invalid input - log.debug('Invalid character entered.') - return ret_list - # Set to non-blocking. We try to read more bytes. If there are no - # more data to read, it will immediately timeout and return with the - # data we have so far - s.timeout(0) - if isinstance(key, int): - ret_list.append(curses.keyname(key).decode()) - else: - if curses.ascii.isctrl(key): - key = curses.unctrl(key).decode() - # Here special cases for alt keys, where we get a ^[ and then a second char - if key == '^[': - try: - part = s.get_wch() - if part == '[': - # CTRL+arrow and meta+arrow keys have a long format - part += s.get_wch() + s.get_wch() + s.get_wch() + s.get_wch() - except curses.error: - pass - except ValueError: # invalid input - log.debug('Invalid character entered.') - else: - key = 'M-%s' % part - # and an even more special case for keys like - # ctrl+arrows, where we get ^[, then [, then a third - # char. - if key == 'M-[': - try: - part = s.get_wch() - except curses.error: - pass - except ValueError: - log.debug('Invalid character entered.') - else: - key = '%s-%s' % (key, part) - if key == '\x7f' or key == '\x08': - key = '^?' - elif key == '\r': - key = '^M' - ret_list.append(key) - -class Keyboard(object): - def __init__(self): - self.escape = False - - def escape_next_key(self): - """ - The next key pressed by the user should be escaped. e.g. if the user - presses ^N, keyboard.get_user_input() will return ["^", "N"] instead - of ["^N"]. This will display ^N in the input, instead of - interpreting the key binding. - """ - self.escape = True - - def get_user_input(self, s): - """ - Returns a list of all the available characters to read (for example it - may contain a whole text if there’s some lag, or the user pasted text, - or the user types really really fast). Also it can return None, meaning - that it’s time to do some other checks (because this function is - blocking, we need to get out of it every now and then even if nothing - was entered). - """ - # Disable the timeout - s.timeout(-1) - ret_list = get_char_list(s) - if not ret_list: - return ret_list - if len(ret_list) != 1: - if ret_list[-1] == '^M': - ret_list.pop(-1) - ret_list = [char if char != '^M' else '^J' for char in ret_list] - if self.escape: - # Modify the first char of the list into its escaped version (i.e one or more char) - key = ret_list.pop(0) - for char in key[::-1]: - ret_list.insert(0, char) - self.escape = False - return ret_list - -if __name__ == '__main__': - import sys - keyboard = Keyboard() - s = curses.initscr() - curses.noecho() - curses.cbreak() - s.keypad(1) - curses.start_color() - curses.use_default_colors() - curses.init_pair(1, 2, -1) - s.attron(curses.A_BOLD | curses.color_pair(1)) - s.addstr('Type Ctrl-c to close\n') - s.attroff(curses.A_BOLD | curses.color_pair(1)) - - pressed_chars = [] - while True: - - try: - chars = keyboard.get_user_input(s) - for char in chars if chars else '': - s.addstr('%s ' % (char)) - pressed_chars.append(chars) - - except KeyboardInterrupt: - break - curses.echo() - curses.cbreak() - curses.curs_set(1) - curses.endwin() - for char_list in pressed_chars: - if char_list: - print(' '.join((char for char in char_list)), end=' ') - print() - sys.exit(0) diff --git a/src/logger.py b/src/logger.py deleted file mode 100644 index 7efa8f61..00000000 --- a/src/logger.py +++ /dev/null @@ -1,284 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -The logger module that handles logging of the poezio -conversations and roster changes -""" - -import mmap -import os -import re -from os import makedirs -from datetime import datetime - -import common -from config import config -from xhtml import clean_text -from theming import dump_tuple, get_theme - -import logging - -log = logging.getLogger(__name__) - -from config import LOG_DIR as log_dir - -message_log_re = re.compile(r'MR (\d{4})(\d{2})(\d{2})T' - r'(\d{2}):(\d{2}):(\d{2})Z ' - r'(\d+) <([^ ]+)> (.*)') -info_log_re = re.compile(r'MI (\d{4})(\d{2})(\d{2})T' - r'(\d{2}):(\d{2}):(\d{2})Z ' - r'(\d+) (.*)') - -def parse_message_line(msg): - if re.match(message_log_re, msg): - return [i for i in re.split(message_log_re, msg) if i] - elif re.match(info_log_re, msg): - return [i for i in re.split(info_log_re, msg) if i] - return False - - -class Logger(object): - """ - Appends things to files. Error/information/warning logs - and also log the conversations to logfiles - """ - def __init__(self): - self.logfile = config.get('logfile') - self.roster_logfile = None - # a dict of 'groupchatname': file-object (opened) - self.fds = dict() - - def __del__(self): - for opened_file in self.fds.values(): - if opened_file: - try: - opened_file.close() - except: # Can't close? too bad - pass - - def reload_all(self): - """Close and reload all the file handles (on SIGHUP)""" - for opened_file in self.fds.values(): - if opened_file: - opened_file.close() - log.debug('All log file handles closed') - for room in self.fds: - self.fds[room] = self.check_and_create_log_dir(room) - log.debug('Log handle for %s re-created', room) - - def check_and_create_log_dir(self, room, open_fd=True): - """ - Check that the directory where we want to log the messages - exists. if not, create it - """ - if not config.get_by_tabname('use_log', room): - return - try: - makedirs(log_dir) - except OSError as e: - if e.errno != 17: # file exists - log.error('Unable to create the log dir', exc_info=True) - except: - log.error('Unable to create the log dir', exc_info=True) - return - if not open_fd: - return - try: - fd = open(os.path.join(log_dir, room), 'a') - self.fds[room] = fd - return fd - except IOError: - log.error('Unable to open the log file (%s)', - os.path.join(log_dir, room), - exc_info=True) - - def get_logs(self, jid, nb=10): - """ - Get the nb last messages from the log history for the given jid. - Note that a message may be more than one line in these files, so - this function is a little bit more complicated than “read the last - nb lines”. - """ - if config.get_by_tabname('load_log', jid) <= 0: - return - - if not config.get_by_tabname('use_log', jid): - return - - if nb <= 0: - return - - self.check_and_create_log_dir(jid, open_fd=False) - - try: - fd = open(os.path.join(log_dir, jid), 'rb') - except FileNotFoundError: - log.info('Non-existing log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) - return - except OSError: - log.error('Unable to open the log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) - return - if not fd: - return - - # read the needed data from the file, we just search nb messages by - # searching "\nM" nb times from the end of the file. We use mmap to - # do that efficiently, instead of seek()s and read()s which are costly. - with fd: - try: - m = mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ) - except Exception: # file probably empty - log.error('Unable to mmap the log file for (%s)', - os.path.join(log_dir, jid), - exc_info=True) - return - pos = m.rfind(b"\nM") # start of messages begin with MI or MR, - # after a \n - # number of message found so far - count = 0 - while pos != -1 and count < nb-1: - count += 1 - pos = m.rfind(b"\nM", 0, pos) - if pos == -1: # If we don't have enough lines in the file - pos = 1 # 1, because we do -1 just on the next line - # to get 0 (start of the file) - lines = m[pos-1:].decode(errors='replace').splitlines() - - messages = [] - color = '\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG) - - # now convert that data into actual Message objects - idx = 0 - while idx < len(lines): - if lines[idx].startswith(' '): # should not happen ; skip - idx += 1 - log.debug('fail?') - continue - tup = parse_message_line(lines[idx]) - idx += 1 - if not tup or 7 > len(tup) > 10: # skip - log.debug('format? %s', tup) - continue - time = [int(i) for index, i in enumerate(tup) if index < 6] - message = {'lines': [], - 'history': True, - 'time': common.get_local_time(datetime(*time))} - size = int(tup[6]) - if len(tup) == 8: #info line - message['lines'].append(color+tup[7]) - else: # message line - message['nickname'] = tup[7] - message['lines'].append(color+tup[8]) - while size != 0 and idx < len(lines): - message['lines'].append(lines[idx][1:]) - size -= 1 - idx += 1 - message['txt'] = '\n'.join(message['lines']) - del message['lines'] - messages.append(message) - - return messages - - def log_message(self, jid, nick, msg, date=None, typ=1): - """ - log the message in the appropriate jid's file - type: - 0 = Don’t log - 1 = Message - 2 = Status/whatever - """ - if not typ: - return True - - jid = str(jid).replace('/', '\\') - if not config.get_by_tabname('use_log', jid): - return True - if jid in self.fds.keys(): - fd = self.fds[jid] - else: - fd = self.check_and_create_log_dir(jid) - if not fd: - return True - try: - msg = clean_text(msg) - if date is None: - str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ') - else: - str_time = common.get_utc_time(date).strftime('%Y%m%dT%H:%M:%SZ') - if typ == 1: - prefix = 'MR' - else: - prefix = 'MI' - lines = msg.split('\n') - first_line = lines.pop(0) - nb_lines = str(len(lines)).zfill(3) - - if nick: - nick = '<' + nick + '>' - fd.write(' '.join((prefix, str_time, nb_lines, nick, ' '+first_line, '\n'))) - else: - fd.write(' '.join((prefix, str_time, nb_lines, first_line, '\n'))) - for line in lines: - fd.write(' %s\n' % line) - except: - log.error('Unable to write in the log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) - return False - else: - try: - fd.flush() # TODO do something better here? - except: - log.error('Unable to flush the log file (%s)', - os.path.join(log_dir, jid), - exc_info=True) - return False - return True - - def log_roster_change(self, jid, message): - """ - Log a roster change - """ - if not config.get_by_tabname('use_log', jid): - return True - self.check_and_create_log_dir('', open_fd=False) - if not self.roster_logfile: - try: - self.roster_logfile = open(os.path.join(log_dir, 'roster.log'), 'a') - except IOError: - log.error('Unable to create the log file (%s)', - os.path.join(log_dir, 'roster.log'), - exc_info=True) - return False - try: - str_time = common.get_utc_time().strftime('%Y%m%dT%H:%M:%SZ') - message = clean_text(message) - lines = message.split('\n') - first_line = lines.pop(0) - nb_lines = str(len(lines)).zfill(3) - self.roster_logfile.write('MI %s %s %s %s\n' % (str_time, nb_lines, jid, first_line)) - for line in lines: - self.roster_logfile.write(' %s\n' % line) - self.roster_logfile.flush() - except: - log.error('Unable to write in the log file (%s)', - os.path.join(log_dir, 'roster.log'), - exc_info=True) - return False - return True - -def create_logger(): - "Create the global logger object" - global logger - logger = Logger() - -logger = None diff --git a/src/multiuserchat.py b/src/multiuserchat.py deleted file mode 100644 index b7b12305..00000000 --- a/src/multiuserchat.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -Implementation of the XEP-0045: Multi-User Chat. -Add some facilities that are not available on the XEP_0045 -slix plugin -""" - -from xml.etree import cElementTree as ET - -from common import safeJID -import logging -log = logging.getLogger(__name__) - -NS_MUC_ADMIN = 'http://jabber.org/protocol/muc#admin' -NS_MUC_OWNER = 'http://jabber.org/protocol/muc#owner' - - -def destroy_room(xmpp, room, reason='', altroom=''): - """ - destroy a room - """ - room = safeJID(room) - if not room: - return False - iq = xmpp.make_iq_set() - iq['to'] = room - query = ET.Element('{%s}query' % NS_MUC_OWNER) - destroy = ET.Element('{%s}destroy' % NS_MUC_OWNER) - if altroom: - destroy.attrib['jid'] = altroom - if reason: - xreason = ET.Element('{%s}reason' % NS_MUC_OWNER) - xreason.text = reason - destroy.append(xreason) - query.append(destroy) - iq.append(query) - def callback(iq): - if not iq or iq['type'] == 'error': - xmpp.core.information('Unable to destroy room %s' % room, - 'Info') - else: - xmpp.core.information('Room %s destroyed' % room, 'Info') - iq.send(callback=callback) - return True - -def send_private_message(xmpp, jid, line): - """ - Send a private message - """ - jid = safeJID(jid) - xmpp.send_message(mto=jid, mbody=line, mtype='chat') - -def send_groupchat_message(xmpp, jid, line): - """ - Send a message to the groupchat - """ - jid = safeJID(jid) - xmpp.send_message(mto=jid, mbody=line, mtype='groupchat') - -def change_show(xmpp, jid, own_nick, show, status): - """ - Change our 'Show' - """ - jid = safeJID(jid) - pres = xmpp.make_presence(pto='%s/%s' % (jid, own_nick)) - if show: # if show is None, don't put a <show /> tag. It means "available" - pres['type'] = show - if status: - pres['status'] = status - pres.send() - -def change_subject(xmpp, jid, subject): - """ - Change the room subject - """ - jid = safeJID(jid) - msg = xmpp.make_message(jid) - msg['type'] = 'groupchat' - msg['subject'] = subject - msg.send() - -def change_nick(core, jid, nick, status=None, show=None): - """ - Change our own nick in a room - """ - xmpp = core.xmpp - presence = xmpp.make_presence(pshow=show, pstatus=status, pto=safeJID('%s/%s' % (jid, nick))) - core.events.trigger('changing_nick', presence) - presence.send() - -def join_groupchat(core, jid, nick, passwd='', status=None, show=None, seconds=None): - xmpp = core.xmpp - stanza = xmpp.make_presence(pto='%s/%s' % (jid, nick), pstatus=status, pshow=show) - x = ET.Element('{http://jabber.org/protocol/muc}x') - if passwd: - passelement = ET.Element('password') - passelement.text = passwd - x.append(passelement) - if seconds is not None: - history = ET.Element('{http://jabber.org/protocol/muc}history') - history.attrib['seconds'] = str(seconds) - x.append(history) - stanza.append(x) - core.events.trigger('joining_muc', stanza) - to = stanza["to"] - stanza.send() - xmpp.plugin['xep_0045'].rooms[jid] = {} - xmpp.plugin['xep_0045'].ourNicks[jid] = to.resource - -def leave_groupchat(xmpp, jid, own_nick, msg): - """ - Leave the groupchat - """ - jid = safeJID(jid) - try: - xmpp.plugin['xep_0045'].leaveMUC(jid, own_nick, msg) - except KeyError: - log.debug("muc.leave_groupchat: could not leave the room %s", - jid, exc_info=True) - -def set_user_role(xmpp, jid, nick, reason, role, callback=None): - """ - (try to) Set the role of a MUC user - (role = 'none': eject user) - """ - jid = safeJID(jid) - iq = xmpp.make_iq_set() - query = ET.Element('{%s}query' % NS_MUC_ADMIN) - item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'role':role}) - if reason: - reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN) - reason_el.text = reason - item.append(reason_el) - query.append(item) - iq.append(query) - iq['to'] = jid - if callback: - return iq.send(callback=callback) - try: - return iq.send() - except Exception as e: - return e.iq - -def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason=None, callback=None): - """ - (try to) Set the affiliation of a MUC user - """ - muc_jid = safeJID(muc_jid) - query = ET.Element('{http://jabber.org/protocol/muc#admin}query') - if nick: - item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) - else: - item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':str(jid)}) - - if reason: - reason_item = ET.Element('{http://jabber.org/protocol/muc#admin}reason') - reason_item.text = reason - item.append(reason_item) - - query.append(item) - iq = xmpp.make_iq_set(query) - iq['to'] = muc_jid - if callback: - return iq.send(callback=callback) - try: - return xmpp.plugin['xep_0045'].setAffiliation(str(muc_jid), str(jid) if jid else None, nick, affiliation) - except: - import traceback - log.debug('Error setting the affiliation: %s', traceback.format_exc()) - return False - -def cancel_config(xmpp, room): - query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - x = ET.Element('{jabber:x:data}x', type='cancel') - query.append(x) - iq = xmpp.make_iq_set(query) - iq['to'] = room - iq.send() - -def configure_room(xmpp, room, form): - if form is None: - return - iq = xmpp.make_iq_set() - iq['to'] = room - query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - form = form.getXML('submit') - query.append(form) - iq.append(query) - iq.send() - diff --git a/src/pep.py b/src/pep.py deleted file mode 100644 index 0f7a1ced..00000000 --- a/src/pep.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -Collection of mappings for PEP moods/activities -extracted directly from the XEP -""" - -MOODS = { - 'afraid': 'Afraid', - 'amazed': 'Amazed', - 'angry': 'Angry', - 'amorous': 'Amorous', - 'annoyed': 'Annoyed', - 'anxious': 'Anxious', - 'aroused': 'Aroused', - 'ashamed': 'Ashamed', - 'bored': 'Bored', - 'brave': 'Brave', - 'calm': 'Calm', - 'cautious': 'Cautious', - 'cold': 'Cold', - 'confident': 'Confident', - 'confused': 'Confused', - 'contemplative': 'Contemplative', - 'contented': 'Contented', - 'cranky': 'Cranky', - 'crazy': 'Crazy', - 'creative': 'Creative', - 'curious': 'Curious', - 'dejected': 'Dejected', - 'depressed': 'Depressed', - 'disappointed': 'Disappointed', - 'disgusted': 'Disgusted', - 'dismayed': 'Dismayed', - 'distracted': 'Distracted', - 'embarrassed': 'Embarrassed', - 'envious': 'Envious', - 'excited': 'Excited', - 'flirtatious': 'Flirtatious', - 'frustrated': 'Frustrated', - 'grumpy': 'Grumpy', - 'guilty': 'Guilty', - 'happy': 'Happy', - 'hopeful': 'Hopeful', - 'hot': 'Hot', - 'humbled': 'Humbled', - 'humiliated': 'Humiliated', - 'hungry': 'Hungry', - 'hurt': 'Hurt', - 'impressed': 'Impressed', - 'in_awe': 'In awe', - 'in_love': 'In love', - 'indignant': 'Indignant', - 'interested': 'Interested', - 'intoxicated': 'Intoxicated', - 'invincible': 'Invincible', - 'jealous': 'Jealous', - 'lonely': 'Lonely', - 'lucky': 'Lucky', - 'mean': 'Mean', - 'moody': 'Moody', - 'nervous': 'Nervous', - 'neutral': 'Neutral', - 'offended': 'Offended', - 'outraged': 'Outraged', - 'playful': 'Playful', - 'proud': 'Proud', - 'relaxed': 'Relaxed', - 'relieved': 'Relieved', - 'remorseful': 'Remorseful', - 'restless': 'Restless', - 'sad': 'Sad', - 'sarcastic': 'Sarcastic', - 'serious': 'Serious', - 'shocked': 'Shocked', - 'shy': 'Shy', - 'sick': 'Sick', - 'sleepy': 'Sleepy', - 'spontaneous': 'Spontaneous', - 'stressed': 'Stressed', - 'strong': 'Strong', - 'surprised': 'Surprised', - 'thankful': 'Thankful', - 'thirsty': 'Thirsty', - 'tired': 'Tired', - 'undefined': 'Undefined', - 'weak': 'Weak', - 'worried': 'Worried' -} - - - - -ACTIVITIES = { - 'doing_chores': { - 'category': 'Doing_chores', - - 'buying_groceries': 'Buying groceries', - 'cleaning': 'Cleaning', - 'cooking': 'Cooking', - 'doing_maintenance': 'Doing maintenance', - 'doing_the_dishes': 'Doing the dishes', - 'doing_the_laundry': 'Doing the laundry', - 'gardening': 'Gardening', - 'running_an_errand': 'Running an errand', - 'walking_the_dog': 'Walking the dog', - 'other': 'Other', - }, - 'drinking': { - 'category': 'Drinking', - - 'having_a_beer': 'Having a beer', - 'having_coffee': 'Having coffee', - 'having_tea': 'Having tea', - 'other': 'Other', - }, - 'eating': { - 'category':'Eating', - - 'having_breakfast': 'Having breakfast', - 'having_a_snack': 'Having a snack', - 'having_dinner': 'Having dinner', - 'having_lunch': 'Having lunch', - 'other': 'Other', - }, - 'exercising': { - 'category': 'Exercising', - - 'cycling': 'Cycling', - 'dancing': 'Dancing', - 'hiking': 'Hiking', - 'jogging': 'Jogging', - 'playing_sports': 'Playing sports', - 'running': 'Running', - 'skiing': 'Skiing', - 'swimming': 'Swimming', - 'working_out': 'Working out', - 'other': 'Other', - }, - 'grooming': { - 'category': 'Grooming', - - 'at_the_spa': 'At the spa', - 'brushing_teeth': 'Brushing teeth', - 'getting_a_haircut': 'Getting a haircut', - 'shaving': 'Shaving', - 'taking_a_bath': 'Taking a bath', - 'taking_a_shower': 'Taking a shower', - 'other': 'Other', - }, - 'having_appointment': { - 'category': 'Having appointment', - - 'other': 'Other', - }, - 'inactive': { - 'category': 'Inactive', - - 'day_off': 'Day_off', - 'hanging_out': 'Hanging out', - 'hiding': 'Hiding', - 'on_vacation': 'On vacation', - 'praying': 'Praying', - 'scheduled_holiday': 'Scheduled holiday', - 'sleeping': 'Sleeping', - 'thinking': 'Thinking', - 'other': 'Other', - }, - 'relaxing': { - 'category': 'Relaxing', - - 'fishing': 'Fishing', - 'gaming': 'Gaming', - 'going_out': 'Going out', - 'partying': 'Partying', - 'reading': 'Reading', - 'rehearsing': 'Rehearsing', - 'shopping': 'Shopping', - 'smoking': 'Smoking', - 'socializing': 'Socializing', - 'sunbathing': 'Sunbathing', - 'watching_a_movie': 'Watching a movie', - 'watching_tv': 'Watching tv', - 'other': 'Other', - }, - 'talking': { - 'category': 'Talking', - - 'in_real_life': 'In real life', - 'on_the_phone': 'On the phone', - 'on_video_phone': 'On video phone', - 'other': 'Other', - }, - 'traveling': { - 'category': 'Traveling', - - 'commuting': 'Commuting', - 'driving': 'Driving', - 'in_a_car': 'In a car', - 'on_a_bus': 'On a bus', - 'on_a_plane': 'On a plane', - 'on_a_train': 'On a train', - 'on_a_trip': 'On a trip', - 'walking': 'Walking', - 'cycling': 'Cycling', - 'other': 'Other', - }, - 'undefined': { - 'category': 'Undefined', - - 'other': 'Other', - }, - 'working': { - 'category': 'Working', - - 'coding': 'Coding', - 'in_a_meeting': 'In a meeting', - 'writing': 'Writing', - 'studying': 'Studying', - 'other': 'Other', - } - } - diff --git a/src/plugin.py b/src/plugin.py deleted file mode 100644 index bf30c981..00000000 --- a/src/plugin.py +++ /dev/null @@ -1,485 +0,0 @@ -""" -Define the PluginConfig and Plugin classes, plus the SafetyMetaclass. -These are used in the plugin system added in poezio 0.7.5 -(see plugin_manager.py) -""" -import os -from functools import partial -from configparser import RawConfigParser -from timed_events import TimedEvent, DelayedEvent -import config -import inspect -import traceback -import logging -log = logging.getLogger(__name__) - -class PluginConfig(config.Config): - """ - Plugin configuration object. - They are accessible inside the plugin with self.config - and behave like the core Config object. - """ - def __init__(self, filename, module_name, default=None): - config.Config.__init__(self, filename, default=default) - self.module_name = module_name - self.read() - - def get(self, option, default=None, section=None): - if not section: - section = self.module_name - return config.Config.get(self, option, default, section) - - def set(self, option, default, section=None): - if not section: - section = self.module_name - return config.Config.set_and_save(self, option, default, section) - - def remove(self, option, section=None): - if not section: - section = self.module_name - return config.Config.remove_and_save(self, option, section) - - def read(self): - """Read the config file""" - RawConfigParser.read(self, self.file_name) - if not self.has_section(self.module_name): - self.add_section(self.module_name) - - def options(self, section=None): - """ - Return the options of the section - If no section is given, it defaults to the plugin name. - """ - if not section: - section = self.module_name - if not self.has_section(section): - self.add_section(section) - return config.Config.options(self, section) - - def write(self): - """Write the config to the disk""" - try: - fp = open(self.file_name, 'w') - RawConfigParser.write(self, fp) - fp.close() - return True - except IOError: - return False - - -class SafetyMetaclass(type): - # A hack - core = None - - @staticmethod - def safe_func(f): - def helper(*args, **kwargs): - try: - return f(*args, **kwargs) - except: - if inspect.stack()[1][1] == inspect.getfile(f): - raise - elif SafetyMetaclass.core: - log.error('Error in a plugin', exc_info=True) - SafetyMetaclass.core.information(traceback.format_exc()) - return None - return helper - - def __new__(meta, name, bases, class_dict): - for k, v in class_dict.items(): - if inspect.isfunction(v): - if k != '__init__' and k != 'init': - class_dict[k] = SafetyMetaclass.safe_func(v) - return type.__new__(meta, name, bases, class_dict) - -class PluginWrap(object): - """ - A wrapper to implicitly pass the module name to PluginAPI - """ - def __init__(self, api, module): - self.api = api - self.module = module - - def __getattribute__(self, name): - api = object.__getattribute__(self, 'api') - module = object.__getattribute__(self, 'module') - return partial(getattr(api, name), module) - -class PluginAPI(object): - """ - The public API exposed to the plugins. - Its goal is to limit the use of the raw Core object - as much as possible. - """ - - def __init__(self, core, plugin_manager): - self.core = core - self.plugin_manager = plugin_manager - - def __getitem__(self, value): - return PluginWrap(self, value) - - def send_message(self, _, *args, **kwargs): - """ - Send a message to the current tab. - - :param str msg: The message to send. - """ - return self.core.send_message(*args, **kwargs) - - def get_conversation_messages(self, _, *args, **kwargs): - """ - Get all the Messages of the current Tab. - - :returns: The list of :py:class:`text_buffer.Message` objects. - :returns: None if the Tab does not inherit from ChatTab. - :rtype: :py:class:`list` - """ - return self.core.get_conversation_messages() - - def add_timed_event(self, _, *args, **kwargs): - """ - Schedule a timed event. - - :param timed_events.TimedEvent event: The timed event to schedule. - """ - return self.core.add_timed_event(*args, **kwargs) - - def remove_timed_event(self, _, *args, **kwargs): - """ - Unschedule a timed event. - - :param timed_events.TimedEvent event: The event to unschedule. - """ - return self.core.remove_timed_event(*args, **kwargs) - - def create_timed_event(self, _, *args, **kwargs): - """ - Create a timed event, but do not schedule it; - :py:func:`~PluginAPI.add_timed_event` must be used for that. - - :param datetime.datetime date: The time at which the handler must be executed - :param function callback: The handler that will be executed - :param \*args: Optional arguments passed to the handler. - :return: The created event. - :rtype: :py:class:`timed_events.TimedEvent` - """ - return TimedEvent(*args, **kwargs) - - def create_delayed_event(self, _, *args, **kwargs): - """ - Create a delayed event, but do not schedule it; - :py:func:`~PluginAPI.add_timed_event` must be used for that. - - A delayed event is a timed event with a delay from the time - this function is called (instead of a datetime). - - :param int delay: The number of seconds to schedule the execution - :param function callback: The handler that will be executed - :param \*args: Optional arguments passed to the handler. - :return: The created event. - :rtype: :py:class:`timed_events.DelayedEvent` - """ - return DelayedEvent(*args, **kwargs) - - def information(self, _, *args, **kwargs): - """ - Display a new message in the information buffer. - - :param str msg: The message to display. - :param str typ: The message type (e.g. Info, Error…) - """ - return self.core.information(*args, **kwargs) - - def current_tab(self, _): - """ - Get the current Tab. - - :returns: The current tab. - """ - return self.core.current_tab() - - def get_status(self, _): - """ - Get the current user global status. - - :returns Status: The current status. - """ - return self.core.get_status() - - def run_command(self, _, *args, **kwargs): - """ - Run a command from the current tab. - (a command starts with a /, if not, it’s a message) - - :param str line: The command to run. - """ - return self.core.current_tab().execute_command(*args, **kwargs) - - def all_tabs(self, _): - """ - Return a list of all opened tabs - - :returns list: The list of tabs. - """ - return self.core.tabs - - def add_command(self, module, *args, **kwargs): - """ - Add a global command. - - :param str name: The name of the command (/name) - :param function handler: The function called when the command is run. - :param str help: The complete help for that command. - :param str short: A short description of the command. - :param function completion: The completion function for that command - (optional) - :param str usage: A string showing the required and optional args - of the command. Optional args should be surrounded by [] - and mandatory args should be surrounded by <>. - - Example string: "<server> [port]" - - :raises Exception: If the command already exists. - """ - return self.plugin_manager.add_command(module, *args, **kwargs) - - def del_command(self, module, *args, **kwargs): - """ - Remove a global command. - - :param str name: The name of the command to remove. - That command _must_ have been added by the same plugin - """ - return self.plugin_manager.del_command(module, *args, **kwargs) - - def add_key(self, module, *args, **kwargs): - """ - Associate a global binding to a handler. - - :param str key: The curses representation of the binding. - :param function handler: The function called when the binding is pressed. - - :raise Exception: If the binding is already present. - """ - return self.plugin_manager.add_key(module, *args, **kwargs) - - def del_key(self, module, *args, **kwargs): - """ - Remove a global binding. - - :param str key: The binding to remove. - """ - return self.plugin_manager.del_key(module, *args, **kwargs) - - def add_tab_key(self, module, *args, **kwargs): - """ - Associate a binding to a handler, but only for a certain tab type. - - :param Tab tab_type: The type of tab to target. - :param str key: The binding to add. - :param function handler: The function called when the binding is pressed - """ - return self.plugin_manager.add_tab_key(module, *args, **kwargs) - - def del_tab_key(self, module, *args, **kwargs): - """ - Remove a binding added with add_tab_key - - :param tabs.Tab tab_type: The type of tab to target. - :param str key: The binding to remove. - """ - return self.plugin_manager.del_tab_key(module, *args, **kwargs) - - def add_tab_command(self, module, *args, **kwargs): - """ - Add a command to only one type of tab. - - :param tabs.Tab tab_type: The type of Tab to target. - :param str name: The name of the command (/name) - :param function handler: The function called when the command is run. - :param str help: The complete help for that command. - :param str short: A short description of the command. - :param function completion: The completion function for that command - (optional) - :param str usage: A string showing the required and optional args - of the command. Optional args should be surrounded by [] - and mandatory args should be surrounded by <>. - - Example string: "<server> [port]" - - :raise Exception: If the command already exists. - """ - return self.plugin_manager.add_tab_command(module, *args, **kwargs) - - def del_tab_command(self, module, *args, **kwargs): - """ - Remove a tab-specific command. - - :param tabs.Tab tab_type: The type of tab to target. - :param str name: The name of the command to remove. - That command _must_ have been added by the same plugin - """ - return self.plugin_manager.del_tab_command(module, *args, **kwargs) - - def add_event_handler(self, module, *args, **kwargs): - """ - Add an event handler for a poezio event. - - :param str event_name: The event name. - :param function handler: The handler function. - :param int position: The position of that handler in the handler list. - This is useful for plugins like GPG or OTR, which must be the last - function called on the text. - Defaults to 0. - - A complete list of those events can be found at - https://doc.poez.io/dev/events.html - """ - return self.plugin_manager.add_event_handler(module, *args, **kwargs) - - def del_event_handler(self, module, *args, **kwargs): - """ - Remove a handler for a poezio event. - - :param str event_name: The name of the targeted event. - :param function handler: The function to remove from the handlers. - """ - return self.plugin_manager.del_event_handler(module, *args, **kwargs) - - def add_slix_event_handler(self, module, event_name, handler): - """ - Add an event handler for a slixmpp event. - - :param str event_name: The event name. - :param function handler: The handler function. - - A list of the slixmpp events can be found here - http://sleekxmpp.com/event_index.html - """ - self.core.xmpp.add_event_handler(event_name, handler) - - def del_slix_event_handler(self, module, event_name, handler): - """ - Remove a handler for a slixmpp event - - :param str event_name: The name of the targeted event. - :param function handler: The function to remove from the handlers. - """ - self.core.xmpp.del_event_handler(event_name, handler) - -class BasePlugin(object, metaclass=SafetyMetaclass): - """ - Class that all plugins derive from. - """ - - default_config = None - - def __init__(self, plugin_api, core, plugins_conf_dir): - self.core = core - # More hack; luckily we'll never have more than one core object - SafetyMetaclass.core = core - conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg') - try: - self.config = PluginConfig(conf, self.__module__, - default=self.default_config) - except Exception: - log.debug('Error while creating the plugin config', exc_info=True) - self.config = PluginConfig(conf, self.__module__) - self._api = plugin_api[self.name] - self.init() - - @property - def name(self): - """ - Get the name (module name) of the plugin. - """ - return self.__module__ - - @property - def api(self): - return self._api - - def init(self): - """ - Method called at the creation of the plugin. - - Do not overwrite __init__ and use this instead. - """ - pass - - def cleanup(self): - """ - Called when the plugin is unloaded. - - Overwrite this if you want to erase or save things before the plugin is disabled. - """ - pass - - def unload(self): - self.cleanup() - - def add_command(self, name, handler, help, completion=None, short='', usage=''): - """ - Add a global command. - You cannot overwrite the existing commands. - """ - return self.api.add_command(name, handler, help, - completion=completion, short=short, usage=usage) - - def del_command(self, name): - """ - Remove a global command. - This only works if the command was added by the plugin - """ - return self.api.del_command(name) - - def add_key(self, key, handler): - """ - Add a global keybind - """ - return self.api.add_key(key, handler) - - def del_key(self, key): - """ - Remove a global keybind - """ - return self.api.del_key(key) - - def add_tab_key(self, tab_type, key, handler): - """ - Add a keybind only for a type of tab. - """ - return self.api.add_tab_key(tab_type, key, handler) - - def del_tab_key(self, tab_type, key): - """ - Remove a keybind added through add_tab_key. - """ - return self.api.del_tab_key(tab_type, key) - - def add_tab_command(self, tab_type, name, handler, help, completion=None, short='', usage=''): - """ - Add a command only for a type of tab. - """ - return self.api.add_tab_command(tab_type, name, handler, help, - completion=completion, short=short, usage=usage) - - def del_tab_command(self, tab_type, name): - """ - Delete a command added through add_tab_command. - """ - return self.api.del_tab_command(tab_type, name) - - def add_event_handler(self, event_name, handler, position=0): - """ - Add an event handler to the event event_name. - An optional position in the event handler list can be provided. - """ - return self.api.add_event_handler(event_name, handler, position) - - def del_event_handler(self, event_name, handler): - """ - Remove 'handler' from the event list for 'event_name'. - """ - return self.api.del_event_handler(event_name, handler) diff --git a/src/plugin_manager.py b/src/plugin_manager.py deleted file mode 100644 index 549753a9..00000000 --- a/src/plugin_manager.py +++ /dev/null @@ -1,384 +0,0 @@ -""" -Plugin manager module. -Define the PluginManager class, the one that glues all the plugins and -the API together. Defines also a bunch of variables related to the -plugin env. -""" - -import os -from os import path -import logging - -import core -import tabs -from plugin import PluginAPI -from config import config - -log = logging.getLogger(__name__) - -class PluginManager(object): - """ - Plugin Manager - Contains all the references to the plugins - And keeps track of everything the plugin has done through the API. - """ - def __init__(self, core): - self.core = core - # module name -> module object - self.modules = {} - # module name -> plugin object - self.plugins = {} - # module name -> dict of commands loaded for the module - self.commands = {} - # module name -> list of event_name/handler pairs loaded for the module - self.event_handlers = {} - # module name -> dict of tab types; tab type -> commands - # loaded by the module - self.tab_commands = {} - # module name → dict of keys/handlers loaded for the module - self.keys = {} - # module name → dict of tab types; tab type → list of keybinds (tuples) - self.tab_keys = {} - self.roster_elements = {} - - from importlib import machinery - self.finder = machinery.PathFinder() - - self.initial_set_plugins_dir() - self.initial_set_plugins_conf_dir() - self.fill_load_path() - - self.plugin_api = PluginAPI(core, self) - - def disable_plugins(self): - for plugin in set(self.plugins.keys()): - try: - self.unload(plugin, notify=False) - except: - pass - - def load(self, name, notify=True): - """ - Load a plugin. - """ - if name in self.plugins: - self.unload(name) - - try: - module = None - loader = self.finder.find_module(name, self.load_path) - if not loader: - self.core.information('Could not find plugin: %s' % name) - return - module = loader.load_module() - except Exception as e: - log.debug("Could not load plugin %s", name, exc_info=True) - self.core.information("Could not load plugin %s: %s" % (name, e), - 'Error') - finally: - if not module: - return - - self.modules[name] = module - self.commands[name] = {} - self.keys[name] = {} - self.tab_keys[name] = {} - self.tab_commands[name] = {} - self.event_handlers[name] = [] - try: - self.plugins[name] = None - self.plugins[name] = module.Plugin(self.plugin_api, self.core, - self.plugins_conf_dir) - except Exception as e: - log.error('Error while loading the plugin %s', name, exc_info=True) - if notify: - self.core.information('Unable to load the plugin %s: %s' % - (name, e), - 'Error') - self.unload(name, notify=False) - else: - if notify: - self.core.information('Plugin %s loaded' % name, 'Info') - - def unload(self, name, notify=True): - if name in self.plugins: - try: - for command in self.commands[name].keys(): - del self.core.commands[command] - for key in self.keys[name].keys(): - del self.core.key_func[key] - for tab in list(self.tab_commands[name].keys()): - for command in self.tab_commands[name][tab][:]: - self.del_tab_command(name, getattr(tabs, tab), - command[0]) - del self.tab_commands[name][tab] - for tab in list(self.tab_keys[name].keys()): - for key in self.tab_keys[name][tab][:]: - self.del_tab_key(name, getattr(tabs, tab), key[0]) - del self.tab_keys[name][tab] - for event_name, handler in self.event_handlers[name][:]: - self.del_event_handler(name, event_name, handler) - - if self.plugins[name] is not None: - self.plugins[name].unload() - del self.plugins[name] - del self.commands[name] - del self.keys[name] - del self.tab_commands[name] - del self.event_handlers[name] - if notify: - self.core.information('Plugin %s unloaded' % name, 'Info') - except Exception as e: - log.debug("Could not unload plugin %s", name, exc_info=True) - self.core.information("Could not unload plugin %s: %s" % - (name, e), - 'Error') - - def add_command(self, module_name, name, handler, help, - completion=None, short='', usage=''): - """ - Add a global command. - """ - if name in self.core.commands: - raise Exception("Command '%s' already exists" % (name,)) - - commands = self.commands[module_name] - commands[name] = core.Command(handler, help, completion, short, usage) - self.core.commands[name] = commands[name] - - def del_command(self, module_name, name): - """ - Remove a global command added through add_command. - """ - if name in self.commands[module_name]: - del self.commands[module_name][name] - if name in self.core.commands: - del self.core.commands[name] - - def add_tab_command(self, module_name, tab_type, name, handler, help, - completion=None, short='', usage=''): - """ - Add a command only for a type of Tab. - """ - commands = self.tab_commands[module_name] - t = tab_type.__name__ - if name in tab_type.plugin_commands: - return - if not t in commands: - commands[t] = [] - commands[t].append((name, handler, help, completion)) - tab_type.plugin_commands[name] = core.Command(handler, help, - completion, short, usage) - for tab in self.core.tabs: - if isinstance(tab, tab_type): - tab.update_commands() - - def del_tab_command(self, module_name, tab_type, name): - """ - Remove a command added through add_tab_command. - """ - commands = self.tab_commands[module_name] - t = tab_type.__name__ - if not t in commands: - return - for command in commands[t]: - if command[0] == name: - commands[t].remove(command) - del tab_type.plugin_commands[name] - for tab in self.core.tabs: - if isinstance(tab, tab_type) and name in tab.commands: - del tab.commands[name] - - def add_tab_key(self, module_name, tab_type, key, handler): - """ - Associate a key binding to a handler only for a type of Tab. - """ - keys = self.tab_keys[module_name] - t = tab_type.__name__ - if key in tab_type.plugin_keys: - return - if not t in keys: - keys[t] = [] - keys[t].append((key, handler)) - tab_type.plugin_keys[key] = handler - for tab in self.core.tabs: - if isinstance(tab, tab_type): - tab.update_keys() - - def del_tab_key(self, module_name, tab_type, key): - """ - Remove a key binding added through add_tab_key. - """ - keys = self.tab_keys[module_name] - t = tab_type.__name__ - if not t in keys: - return - for _key in keys[t]: - if _key[0] == key: - keys[t].remove(_key) - del tab_type.plugin_keys[key] - for tab in self.core.tabs: - if isinstance(tab, tab_type) and key in tab.key_func: - del tab.key_func[key] - - def add_key(self, module_name, key, handler): - """ - Associate a global key binding to a handler, except if it - already exists. - """ - if key in self.core.key_func: - raise Exception("Key '%s' already exists" % (key,)) - keys = self.keys[module_name] - keys[key] = handler - self.core.key_func[key] = handler - - def del_key(self, module_name, key): - """ - Remove a global key binding added by a plugin. - """ - if key in self.keys[module_name]: - del self.keys[module_name][key] - if key in self.core.key_func: - del self.core.commands[key] - - def add_event_handler(self, module_name, event_name, handler, position=0): - """ - Add an event handler. If event_name isn’t in the event list, assume - it is a slixmpp event. - """ - eh = self.event_handlers[module_name] - eh.append((event_name, handler)) - if event_name in self.core.events.events: - self.core.events.add_event_handler(event_name, handler, position) - else: - self.core.xmpp.add_event_handler(event_name, handler) - - def del_event_handler(self, module_name, event_name, handler): - """ - Remove an event handler if it exists. - """ - if event_name in self.core.events.events: - self.core.events.del_event_handler(None, handler) - else: - self.core.xmpp.del_event_handler(event_name, handler) - eh = self.event_handlers[module_name] - eh = list(filter(lambda e: e != (event_name, handler), eh)) - - def completion_load(self, the_input): - """ - completion function that completes the name of the plugins, from - all .py files in plugins_dir - """ - try: - names = set() - for path in self.load_path: - try: - add = set(os.listdir(path)) - names |= add - except: - pass - except OSError as e: - self.core.information('Completion failed: %s' % e, 'Error') - return - plugins_files = [name[:-3] for name in names if name.endswith('.py') - and name != '__init__.py' and not name.startswith('.')] - plugins_files.sort() - position = the_input.get_argument_position(quoted=False) - return the_input.new_completion(plugins_files, position, '', - quotify=False) - - def completion_unload(self, the_input): - """ - completion function that completes the name of loaded plugins - """ - position = the_input.get_argument_position(quoted=False) - return the_input.new_completion(sorted(self.plugins.keys()), position, - '', quotify=False) - - def on_plugins_dir_change(self, new_value): - self.plugins_dir = new_value - self.check_create_plugins_dir() - self.fill_load_path() - - def on_plugins_conf_dir_change(self, new_value): - self.plugins_conf_dir = new_value - self.check_create_plugins_conf_dir() - - def initial_set_plugins_conf_dir(self): - """ - Create the plugins_conf_dir - """ - plugins_conf_dir = config.get('plugins_conf_dir') - if not plugins_conf_dir: - config_home = os.environ.get('XDG_CONFIG_HOME') - if not config_home: - config_home = os.path.join(os.environ.get('HOME'), '.config') - plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins') - self.plugins_conf_dir = os.path.expanduser(plugins_conf_dir) - self.check_create_plugins_conf_dir() - - def check_create_plugins_conf_dir(self): - """ - Create the plugins config directory if it does not exist. - Returns True on success, False on failure. - """ - if not os.access(self.plugins_conf_dir, os.R_OK | os.X_OK): - try: - os.makedirs(self.plugins_conf_dir) - except OSError: - log.error('Unable to create the plugin conf dir: %s', - self.plugins_conf_dir, exc_info=True) - return False - return True - - def initial_set_plugins_dir(self): - """ - Set the plugins_dir on start - """ - plugins_dir = config.get('plugins_dir') - plugins_dir = plugins_dir or\ - os.path.join(os.environ.get('XDG_DATA_HOME') or\ - os.path.join(os.environ.get('HOME'), - '.local', 'share'), - 'poezio', 'plugins') - self.plugins_dir = os.path.expanduser(plugins_dir) - self.check_create_plugins_dir() - - def check_create_plugins_dir(self): - """ - Create the plugins directory if it does not exist. - Returns True on success, False on failure. - """ - if not os.access(self.plugins_dir, os.R_OK | os.X_OK): - try: - os.makedirs(self.plugins_dir, exist_ok=True) - except OSError: - log.error('Unable to create the plugins dir: %s', - self.plugins_dir, exc_info=True) - return False - return True - - def fill_load_path(self): - """ - Append the global packages and the source directory if available - """ - - self.load_path = [] - - default_plugin_path = path.join(path.dirname(path.dirname(__file__)), - 'plugins') - - if os.access(default_plugin_path, os.R_OK | os.X_OK): - self.load_path.insert(0, default_plugin_path) - - if os.access(self.plugins_dir, os.R_OK | os.X_OK): - self.load_path.append(self.plugins_dir) - - try: - import poezio_plugins - except: - pass - else: - if poezio_plugins.__path__: - self.load_path.append(list(poezio_plugins.__path__)[0]) - diff --git a/src/poezio.py b/src/poezio.py deleted file mode 100644 index 9fb6fb73..00000000 --- a/src/poezio.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - - -""" -Starting point of poezio. Launches both the Connection and Gui -""" - -import sys -import os -import signal -import logging - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -import singleton - -def test_curses(): - """ - Check if the system ncurses linked with python has unicode capabilities. - """ - import curses - if hasattr(curses, 'unget_wch'): - return True - print("""\ -ERROR: The current python executable is linked with a ncurses version that \ -has no unicode capabilities. - -This could mean that: - - python was built on a system where readline is linked against \ -libncurses and not libncursesw - - python was built without ncursesw headers available - -Please file a bug for your distribution or fix that on your system and then \ -recompile python. -Poezio is currently unable to read your input or draw its interface properly,\ - so it will now exit.""") - return False - - -def main(): - """ - Enter point - """ - sys.stdout.write("\x1b]0;poezio\x07") - sys.stdout.flush() - import config - config_path = config.check_create_config_dir() - config.run_cmdline_args(config_path) - config.create_global_config() - config.check_create_log_dir() - config.check_create_cache_dir() - config.setup_logging() - config.post_logging_setup() - - from config import options - - if options.check_config: - config.check_config() - sys.exit(0) - - import theming - theming.update_themes_dir() - - import logger - logger.create_logger() - - import roster - roster.create_roster() - - import core - - log = logging.getLogger('') - - signal.signal(signal.SIGINT, signal.SIG_IGN) # ignore ctrl-c - cocore = singleton.Singleton(core.Core) - signal.signal(signal.SIGUSR1, cocore.sigusr_handler) # reload the config - signal.signal(signal.SIGHUP, cocore.exit_from_signal) - signal.signal(signal.SIGTERM, cocore.exit_from_signal) - if options.debug: - cocore.debug = True - cocore.start() - - from slixmpp.exceptions import IqError, IqTimeout - def swallow_iqerrors(loop, context): - """Do not log unhandled iq errors and timeouts""" - if not isinstance(context['exception'], (IqError, IqTimeout)): - loop.default_exception_handler(context) - - # Warning: asyncio must always be imported after the config. Otherwise - # the asyncio logger will not follow our configuration and won't write - # the tracebacks in the correct file, etc - import asyncio - loop = asyncio.get_event_loop() - loop.set_exception_handler(swallow_iqerrors) - - loop.add_reader(sys.stdin, cocore.on_input_readable) - loop.add_signal_handler(signal.SIGWINCH, cocore.sigwinch_handler) - cocore.xmpp.start() - loop.run_forever() - # We reach this point only when loop.stop() is called - try: - cocore.reset_curses() - except: - pass - -if __name__ == '__main__': - if test_curses(): - main() - else: - sys.exit(1) diff --git a/src/poezio_shlex.py b/src/poezio_shlex.py deleted file mode 100644 index 032baeee..00000000 --- a/src/poezio_shlex.py +++ /dev/null @@ -1,276 +0,0 @@ -""" -A lexical analyzer class for simple shell-like syntaxes. - -Tweaked for the specific needs of parsing poezio input. - -""" - -# Module and documentation by Eric S. Raymond, 21 Dec 1998 -# Input stacking and error message cleanup added by ESR, March 2000 -# push_source() and pop_source() made explicit by ESR, January 2001. -# Posix compliance, split(), string arguments, and -# iterator interface by Gustavo Niemeyer, April 2003. - -import os -import re -import sys -from collections import deque - -from io import StringIO - -__all__ = ["shlex", "split", "quote"] - -class shlex(object): - """ - A custom version of the shlex in the stdlib to yield more information - """ - def __init__(self, instream=None, infile=None, posix=True): - if isinstance(instream, str): - instream = StringIO(instream) - if instream is not None: - self.instream = instream - self.infile = infile - else: - self.instream = sys.stdin - self.infile = None - self.posix = posix - self.eof = '' - self.commenters = '' - self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') - if self.posix: - self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' - 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') - self.whitespace = ' \t\r\n' - self.whitespace_split = True - self.quotes = '"' - self.escape = '\\' - self.escapedquotes = '"' - self.state = ' ' - self.pushback = deque() - self.lineno = 1 - self.debug = 0 - self.token = '' - self.filestack = deque() - self.source = None - if self.debug: - print('shlex: reading from %s, line %d' \ - % (self.instream, self.lineno)) - - def push_token(self, tok): - "Push a token onto the stack popped by the get_token method" - if self.debug >= 1: - print("shlex: pushing token " + repr(tok)) - self.pushback.appendleft(tok) - - def push_source(self, newstream, newfile=None): - "Push an input source onto the lexer's input source stack." - if isinstance(newstream, str): - newstream = StringIO(newstream) - self.filestack.appendleft((self.infile, self.instream, self.lineno)) - self.infile = newfile - self.instream = newstream - self.lineno = 1 - if self.debug: - if newfile is not None: - print('shlex: pushing to file %s' % (self.infile,)) - else: - print('shlex: pushing to stream %s' % (self.instream,)) - - def pop_source(self): - "Pop the input source stack." - self.instream.close() - (self.infile, self.instream, self.lineno) = self.filestack.popleft() - if self.debug: - print('shlex: popping to %s, line %d' \ - % (self.instream, self.lineno)) - self.state = ' ' - - def get_token(self): - "Get a token from the input stream (or from stack if it's nonempty)" - if self.pushback: - tok = self.pushback.popleft() - if self.debug >= 1: - print("shlex: popping token " + repr(tok)) - return tok - # No pushback. Get a token. - start, end, raw = self.read_token() - return start, end, raw - - def read_token(self): - quoted = False - escapedstate = ' ' - token_start = 0 - token_end = -1 - # read one char from the stream at once - while True: - nextchar = self.instream.read(1) - if nextchar == '\n': - self.lineno = self.lineno + 1 - if self.debug >= 3: - print("shlex: in state", repr(self.state), \ - "I see character:", repr(nextchar)) - if self.state is None: - self.token = '' # past end of file - token_end = self.instream.tell() - break - elif self.state == ' ': - if not nextchar: - self.state = None # end of file - token_end = self.instream.tell() - break - elif nextchar in self.whitespace: - if self.debug >= 2: - print("shlex: I see whitespace in whitespace state") - if self.token or (self.posix and quoted): - token_end = self.instream.tell() - 1 - break # emit current token - else: - continue - elif nextchar in self.wordchars: - token_start = self.instream.tell() - 1 - self.token = nextchar - self.state = 'a' - elif nextchar in self.quotes: - token_start = self.instream.tell() - 1 - self.state = nextchar - elif self.whitespace_split: - token_start = self.instream.tell() - 1 - self.token = nextchar - self.state = 'a' - else: - token_start = self.instream.tell() - 1 - self.token = nextchar - if self.token or (self.posix and quoted): - token_end = self.instream.tell() - 1 - break # emit current token - else: - continue - elif self.state in self.quotes: - quoted = True - if not nextchar: # end of file - if self.debug >= 2: - print("shlex: I see EOF in quotes state") - # XXX what error should be raised here? - token_end = self.instream.tell() - break - if nextchar == self.state: - if not self.posix: - self.token = self.token + nextchar - self.state = ' ' - token_end = self.instream.tell() - break - else: - self.state = 'a' - elif self.posix and nextchar in self.escape and \ - self.state in self.escapedquotes: - escapedstate = self.state - self.state = nextchar - else: - self.token = self.token + nextchar - elif self.state in self.escape: - if not nextchar: # end of file - if self.debug >= 2: - print("shlex: I see EOF in escape state") - # XXX what error should be raised here? - token_end = self.instream.tell() - break - # only the quote may be escaped - if escapedstate in self.quotes and nextchar != escapedstate: - self.token = self.token + self.state - self.token = self.token + nextchar - self.state = escapedstate - elif self.state == 'a': - if not nextchar: - self.state = None # end of file - token_end = self.instream.tell() - break - elif nextchar in self.whitespace: - if self.debug >= 2: - print("shlex: I see whitespace in word state") - self.state = ' ' - if self.token or (self.posix and quoted): - token_end = self.instream.tell() - 1 - break # emit current token - else: - continue - elif nextchar in self.wordchars or nextchar in self.quotes \ - or self.whitespace_split: - self.token = self.token + nextchar - else: - self.pushback.appendleft(nextchar) - if self.debug >= 2: - print("shlex: I see punctuation in word state") - self.state = ' ' - if self.token: - token_end = self.instream.tell() - break # emit current token - else: - continue - result = self.token - self.token = '' - if self.posix and not quoted and result == '': - result = None - if self.debug > 1: - if result: - print("shlex: raw token=" + repr(result)) - else: - print("shlex: raw token=EOF") - return (token_start, token_end, result) - - def sourcehook(self, newfile): - "Hook called on a filename to be sourced." - if newfile[0] == '"': - newfile = newfile[1:-1] - # This implements cpp-like semantics for relative-path inclusion. - if isinstance(self.infile, str) and not os.path.isabs(newfile): - newfile = os.path.join(os.path.dirname(self.infile), newfile) - return (newfile, open(newfile, "r")) - - def error_leader(self, infile=None, lineno=None): - "Emit a C-compiler-like, Emacs-friendly error-message leader." - if infile is None: - infile = self.infile - if lineno is None: - lineno = self.lineno - return "\"%s\", line %d: " % (infile, lineno) - - def __iter__(self): - return self - - def __next__(self): - token = self.get_token() - if token and token[0] == self.eof: - raise StopIteration - return token - -def split(s, comments=False, posix=True): - lex = shlex(s, posix=posix) - lex.whitespace_split = True - if not comments: - lex.commenters = '' - return list(lex) - - -_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search - -def quote(s): - """Return a shell-escaped version of the string *s*.""" - if not s: - return "''" - if _find_unsafe(s) is None: - return s - - # use single quotes, and put single quotes into double quotes - # the string $'b is then quoted as '$'"'"'b' - return "'" + s.replace("'", "'\"'\"'") + "'" - - -if __name__ == '__main__': - lexer = shlex(instream=sys.argv[1]) - while 1: - tt = lexer.get_token() - if tt: - print("Token: " + repr(tt)) - else: - break diff --git a/src/pooptmodule.c b/src/pooptmodule.c deleted file mode 100644 index 69fb7f6f..00000000 --- a/src/pooptmodule.c +++ /dev/null @@ -1,486 +0,0 @@ -/* Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> */ - -/* 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. */ - -/** The poopt python3 module -**/ - -/* This file is a python3 module for poezio, used to replace some time-critical -python functions that are too slow. */ - -#define PY_SSIZE_T_CLEAN - -#include "Python.h" - -PyObject *ErrorObject; - -/*** - Internal functions - ***/ - -/** - Just checking if the return value is -1. In some (all?) implementations, - wcwidth("😆") returns -1 while it should return 1. In these cases, we - return 1 instead because this is by far the most probable real value. - Since the string is received from python, and the unicode character is - extracted with mbrtowc(), and supposing these two compononents are not - bugged, and since poezio’s code should never pass '\t', '\n' or their - friends, a return value of -1 from wcwidth() is considered to be a bug in - wcwidth() (until proven otherwise). xwcwidth() is here to work around - this bug. */ -static int xwcwidth(wchar_t c) -{ - const int res = wcwidth(c); - if (res == -1 && c != '\x19') - return 1; - return res; -} - -/*** - The module functions - ***/ - -/** - cut_text: takes a string and returns a tuple of int. - - Each two int tuple is a line, represented by the ending position it - (where it should be cut). Not that this position is calculed using the - position of the python string characters, not just the individual bytes. - - For example, - poopt_cut_text("vivent les réfrigérateurs", 6); - will return [(0, 6), (7, 10), (11, 17), (17, 22), (22, 24)], meaning that - the lines are - "vivent", "les", "réfrig", "érateu" and "rs" - -*/ -PyDoc_STRVAR(poopt_cut_text_doc, "cut_text(text, width)\n\n\nReturn a list of two-tuple, the first int is the starting position of the line and the second is its end."); - -static PyObject* poopt_cut_text(PyObject* self, PyObject* args) -{ - /* The list of tuples that we return */ - PyObject* retlist = PyList_New(0); - /* The temporary name for the tuples */ - PyObject* tmp; - - /* Get the python arguments */ - const size_t width; - const char* buffer; - const Py_ssize_t buffer_len; - - if (PyArg_ParseTuple(args, "s#k", &buffer, &buffer_len, &width) == 0) - return NULL; - - /* Pointer to the end of the string */ - const char* const end = buffer + buffer_len; - - /* The position, considering unicode chars (aka, the position in the - * python string). This is used to determine the position in the python - * string at which we should cut */ - unsigned int spos = 0; - - /* The start position (in the python-string) of the next line */ - unsigned int start_pos = 0; - - /* The position of the last space seen in the current line. This is used - * to cut on spaces instead of cutting inside words, if possible (aka if - * there is a space) */ - int last_space = -1; - /* The number of columns taken by chars between start_pos and last_space */ - size_t cols_until_space = 0; - - /* The number of bytes consumed by mbrtowc. We advance the buffer ptr by this value */ - size_t consumed; - - /* Number of columns taken to display the current line so far */ - size_t columns = 0; - - /* The unicode character found by mbrtowc */ - wchar_t wc; - - while (buffer < end) - { - /* Special case to jump poezio special characters that are contained - * in the python string, but should not be counted as chars, because - * they will not be displayed. Those are the formatting chars (to - * insert colors or things like that in the string) */ - if (*buffer == 25) /* \x19 */ - { - /* Jump everything until the end of this format marker, but - * without increasing the number of columns of the current - * line. Because these chars are not printed. */ - while (buffer < end && *buffer != 'u' && - *buffer != 'a' && *buffer != 'i' && - *buffer != 'b' && *buffer != 'o' && - *buffer != '}') - { - buffer++; - spos++; - } - buffer++; - spos++; - continue; - } - /* Find the next unicode character (a wchar_t) in the string. This - * may consume from one to 4 bytes. */ - consumed = mbrtowc(&wc, buffer, end-buffer, NULL); - if (consumed == 0) - break ; - else if ((size_t)-1 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -1: Invalid multibyte sequence."); - return NULL; - } - else if ((size_t)-2 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -2: Could not parse a complete multibyte character."); - return NULL; - } - - buffer += consumed; - - /* This is one condition to end the line: an explicit \n is found */ - if (wc == (wchar_t)'\n') - { - spos++; - tmp = Py_BuildValue("II", start_pos, spos); - if (PyList_Append(retlist, tmp) == -1) - { - Py_XDECREF(tmp); - return NULL; - } - Py_XDECREF(tmp); - /* And then initiate a new line */ - start_pos = spos; - last_space = -1; - columns = 0; - continue ; - } - - /* Get the number of columns needed to display this character. May be 0, 1 or 2 */ - const size_t cols = xwcwidth(wc); - - /* This is the second condition to end the line: we have consumed - * enough columns to fill a whole line */ - if (columns + cols > width) - { /* If possible, cut on a space */ - if (last_space != -1) - { - tmp = Py_BuildValue("II", start_pos, last_space); - if (PyList_Append(retlist, tmp) == -1) - { - Py_XDECREF(tmp); - return NULL; - } - Py_XDECREF(tmp); - start_pos = last_space + 1; - last_space = -1; - columns -= (cols_until_space + 1); - } - else - { - /* Otherwise, cut in the middle of a word */ - tmp = Py_BuildValue("II", start_pos, spos); - if (PyList_Append(retlist, tmp) == -1) - { - Py_XDECREF(tmp); - return NULL; - } - Py_XDECREF(tmp); - start_pos = spos; - columns = 0; - } - } - /* We save the position of the last space seen in this line, and the - number of columns we have until now. This helps us keep track of - the columns to count when we will use that space as a cutting - point, later */ - if (wc == (wchar_t)' ') - { - last_space = spos; - cols_until_space = columns; - } - /* We advanced from one char, increment spos by one and add the - * char's columns to the line's columns */ - columns += cols; - spos++; - } - /* We are at the end of the string, append the last line, not finished */ - tmp = Py_BuildValue("II", start_pos, spos); - if (PyList_Append(retlist, tmp) == -1) - { - Py_XDECREF(tmp); - return NULL; - } - Py_XDECREF(tmp); - return retlist; -} - -/** - wcswidth: An emulation of the POSIX wcswidth(3) function using wcwidth - and mbrtowc. -*/ -PyDoc_STRVAR(poopt_wcswidth_doc, "wcswidth(s)\n\n\nThe wcswidth() function returns the number of columns needed to represent the wide-character string pointed to by s. Raise UnicodeError if an invalid unicode value is passed"); -static PyObject* poopt_wcswidth(PyObject* self, PyObject* args) -{ - const char* string; - const Py_ssize_t len; - if (PyArg_ParseTuple(args, "s#", &string, &len) == 0) - return NULL; - const char* const end = string + len; - wchar_t wc; - int res = 0; - while (string < end) - { - const size_t consumed = mbrtowc(&wc, string, end-string, NULL); - if (consumed == 0) - break ; - else if ((size_t)-1 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -1: Invalid multibyte sequence."); - return NULL; - } - else if ((size_t)-2 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -2: Could not parse a complete multibyte character."); - return NULL; - } - string += consumed; - res += xwcwidth(wc); - } - return Py_BuildValue("i", res); -} - -/** - cut_by_columns: takes a python string and a number of columns, returns a - python string truncated to take at most that many columns - For example cut_by_columns(n, "エメルカ") will return: - - n == 5 -> "エメ" (which takes only 4 columns since we can't cut the - next character in half) - - n == 2 -> "エ" - - n == 1 -> "" - - n == 42 -> "エメルカ" - - etc -*/ -PyDoc_STRVAR(poopt_cut_by_columns_doc, "cut_by_columns(n, string)\n\n\nreturns a string truncated to take at most n columns"); -static PyObject* poopt_cut_by_columns(PyObject* self, PyObject* args) -{ - const char* start; - const Py_ssize_t len; - const size_t limit; - if (PyArg_ParseTuple(args, "s#k", &start, &len, &limit) == 0) - return NULL; - - const char* const end = start + len; - const char* ptr = start; - wchar_t wc; - - /* The number of columns that the string would take so far */ - size_t columns = 0; - - while (ptr < end) - { - if (columns == limit) - break ; - const size_t consumed = mbrtowc(&wc, ptr, end-ptr, NULL); - if (consumed == 0) - break ; - else if ((size_t)-1 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -1: Invalid multibyte sequence."); - return NULL; - } - else if ((size_t)-2 == consumed) - { - PyErr_SetString(PyExc_UnicodeError, - "mbrtowc returned -2: Could not parse a complete multibyte character."); - return NULL; - } - const size_t cols = wcwidth(wc); - if (columns + cols > limit) - /* Adding the next character would exceed the column limit */ - break ; - ptr += consumed; - columns += cols; - } - return Py_BuildValue("s#", start, ptr - start); -} - -/*** - Module initialization. Just taken from the xxmodule.c template from the - python sources. - ***/ -static PyTypeObject Str_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "pooptmodule.Str", /*tp_name*/ - 0, /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - 0, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_reserved*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - 0, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ - 0, /* see PyInit_xx */ /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - 0, /*tp_init*/ - 0, /*tp_alloc*/ - 0, /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ -}; - -static PyObject * -null_richcompare(PyObject *self, PyObject *other, int op) -{ - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; -} - -static PyTypeObject Null_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "pooptmodule.Null", /*tp_name*/ - 0, /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - 0, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_reserved*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - null_richcompare, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - 0, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ - 0, /* see PyInit_xx */ /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - 0, /*tp_init*/ - 0, /*tp_alloc*/ - 0, /* see PyInit_xx */ /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ -}; - - -/* List of functions defined in the module */ -static PyMethodDef poopt_methods[] = { - {"cut_text", poopt_cut_text, METH_VARARGS, poopt_cut_text_doc}, - {"wcswidth", poopt_wcswidth, METH_VARARGS, poopt_wcswidth_doc}, - {"cut_by_columns", poopt_cut_by_columns, METH_VARARGS, poopt_cut_by_columns_doc}, - {} /* sentinel */ -}; - -PyDoc_STRVAR(module_doc, - "This is a template module just for instruction. And poopt."); - -/* Initialization function for the module (*must* be called PyInit_xx) */ - -static struct PyModuleDef pooptmodule = { - PyModuleDef_HEAD_INIT, - "poopt", - module_doc, - -1, - poopt_methods, - NULL, - NULL, - NULL, - NULL -}; - -PyMODINIT_FUNC -PyInit_poopt(void) -{ - PyObject *m = NULL; - - /* Due to cross platform compiler issues the slots must be filled - * here. It's required for portability to Windows without requiring - * C++. */ - Null_Type.tp_base = &PyBaseObject_Type; - Null_Type.tp_new = PyType_GenericNew; - Str_Type.tp_base = &PyUnicode_Type; - - /* Finalize the type object including setting type of the new type - * object; doing it here is required for portability, too. */ - /* if (PyType_Ready(&Xxo_Type) < 0) */ - /* goto fail; */ - - /* Create the module and add the functions */ - m = PyModule_Create(&pooptmodule); - if (m == NULL) - goto fail; - - /* Add some symbolic constants to the module */ - if (ErrorObject == NULL) { - ErrorObject = PyErr_NewException("poopt.error", NULL, NULL); - if (ErrorObject == NULL) - goto fail; - } - Py_INCREF(ErrorObject); - PyModule_AddObject(m, "error", ErrorObject); - - /* Add Str */ - if (PyType_Ready(&Str_Type) < 0) - goto fail; - PyModule_AddObject(m, "Str", (PyObject *)&Str_Type); - - /* Add Null */ - if (PyType_Ready(&Null_Type) < 0) - goto fail; - PyModule_AddObject(m, "Null", (PyObject *)&Null_Type); - return m; - fail: - Py_XDECREF(m); - return NULL; -} diff --git a/src/roster.py b/src/roster.py deleted file mode 100644 index ba7da63e..00000000 --- a/src/roster.py +++ /dev/null @@ -1,334 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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 Roster and RosterGroup classes -""" -import logging -log = logging.getLogger(__name__) - -from config import config -from contact import Contact -from roster_sorting import SORTING_METHODS, GROUP_SORTING_METHODS - -from os import path as p -from datetime import datetime -from common import safeJID -from slixmpp.exceptions import IqError, IqTimeout - - -class Roster(object): - """ - The proxy class to get the roster from slixmpp. - Caches Contact and RosterGroup objects. - """ - - def __init__(self): - """ - node: the RosterSingle from slixmpp - """ - self.__node = None - self.contact_filter = None # A tuple(function, *args) - # function to filter contacts, - # on search, for example - self.folded_groups = set(config.get('folded_roster_groups', - section='var').split(':')) - self.groups = {} - self.contacts = {} - self.length = 0 - self.connected = 0 - - # Used for caching roster infos - self.last_built = datetime.now() - self.last_modified = datetime.now() - - def modified(self): - self.last_modified = datetime.now() - - @property - def needs_rebuild(self): - return self.last_modified >= self.last_built - - def __getitem__(self, key): - """Get a Contact from his bare JID""" - key = safeJID(key).bare - if key in self.contacts and self.contacts[key] is not None: - return self.contacts[key] - if key in self.jids(): - contact = Contact(self.__node[key]) - self.contacts[key] = contact - return contact - - def __setitem__(self, key, value): - """Set the a Contact value for the bare jid key""" - self.contacts[key] = value - - def remove(self, jid): - """Send a removal iq to the server""" - jid = safeJID(jid).bare - if self.__node[jid]: - try: - self.__node[jid].send_presence(ptype='unavailable') - self.__node.remove(jid) - except (IqError, IqTimeout): - log.debug('IqError when removing %s:', jid, exc_info=True) - - def __delitem__(self, jid): - """Remove a contact from the roster view""" - jid = safeJID(jid).bare - contact = self[jid] - if not contact: - return - del self.contacts[contact.bare_jid] - - for group in list(self.groups.values()): - group.remove(contact) - if not group: - del self.groups[group.name] - self.modified() - - def __iter__(self): - """Iterate over the jids of the contacts""" - return iter(self.contacts.values()) - - def __contains__(self, key): - """True if the bare jid is in the roster, false otherwise""" - return safeJID(key).bare in self.jids() - - @property - def jid(self): - """Our JID""" - return self.__node.jid - - def get_and_set(self, jid): - contact = self.contacts.get(jid) - if contact is None: - contact = Contact(self.__node[jid]) - self.contacts[jid] = contact - return contact - return contact - - def set_node(self, value): - """Set the slixmpp RosterSingle for our roster""" - self.__node = value - - def get_groups(self, sort=''): - """Return a list of the RosterGroups""" - group_list = sorted( - (group for group in self.groups.values() if group), - key=lambda x: x.name.lower() if x.name else '' - ) - - for sorting in sort.split(':'): - if sorting == 'reverse': - group_list = list(reversed(group_list)) - else: - method = GROUP_SORTING_METHODS.get(sorting, lambda x: 0) - group_list = sorted(group_list, key=method) - return group_list - - def get_group(self, name): - """Return a group or create it if not present""" - if name in self.groups: - return self.groups[name] - self.groups[name] = RosterGroup(name, folded=name in self.folded_groups) - - def add(self, jid): - """Subscribe to a jid""" - self.__node.subscribe(jid) - - def jids(self): - """List of the contact JIDS""" - l = [] - for key in self.__node.keys(): - contact = self.get_and_set(key) - if key != self.jid and (contact and self.exists(contact)): - l.append(key) - self.update_size(l) - return l - - def update_size(self, jids=None): - if jids is None: - jids = self.jids() - self.length = len(jids) - - def get_contacts(self): - """ - Return a list of all the contacts - """ - return [self[jid] for jid in self.jids()] - - def get_contacts_sorted_filtered(self, sort=''): - """ - Return a list of all the contacts sorted with a criteria - """ - contact_list = [] - for contact in self.get_contacts(): - if contact.bare_jid != self.jid: - if self.contact_filter: - if self.contact_filter[0](contact, self.contact_filter[1]): - contact_list.append(contact) - else: - contact_list.append(contact) - contact_list = sorted(contact_list, key=SORTING_METHODS['name']) - - for sorting in sort.split(':'): - if sorting == 'reverse': - contact_list = list(reversed(contact_list)) - else: - method = SORTING_METHODS.get(sorting, lambda x: 0) - contact_list = sorted(contact_list, key=method) - return contact_list - - def save_to_config_file(self): - """ - Save various information to the config file - e.g. the folded groups - """ - folded_groups = ':'.join([group.name for group in self.groups.values()\ - if group.folded]) - log.debug('folded:%s\n', folded_groups) - return config.silent_set('folded_roster_groups', folded_groups, 'var') - - def get_nb_connected_contacts(self): - """ - Get the number of connected contacts - """ - return self.connected - - def update_contact_groups(self, contact): - """Regenerate the RosterGroups when receiving a contact update""" - if not isinstance(contact, Contact): - contact = self.get_and_set(contact) - if not contact: - return - for name, group in self.groups.items(): - if name in contact.groups and contact not in group: - group.add(contact) - elif contact in group and name not in contact.groups: - group.remove(contact) - - for group in contact.groups: - if not group in self.groups: - self.groups[group] = RosterGroup(group, folded=group in self.folded_groups) - self.groups[group].add(contact) - - def __len__(self): - """ - Return the number of contacts - (used to return the display size, but now we have - the display cache in RosterWin for that) - """ - return self.length - - def __repr__(self): - ret = '== Roster:\nContacts:\n' - for contact in self.contacts.values(): - ret += '%s\n' % (contact,) - ret += 'Groups\n' - for group in self.groups: - ret += '%s\n' % (group,) - return ret + '\n' - - def export(self, path): - """Export a list of bare jids to a given file""" - if p.isfile(path): - return - try: - f = open(path, 'w+', encoding='utf-8') - f.writelines([str(i) + "\n" for i in self.contacts if self[i] and (self[i].subscription == "both" or self[i].ask)]) - f.close() - return True - except IOError: - return - except OSError: - return - - def exists(self, contact): - if not contact: - return False - for group in contact.groups: - if contact not in self.groups.get(group, tuple()): - return False - return True - - -class RosterGroup(object): - """ - A RosterGroup is a group containing contacts - It can be Friends/Family etc, but also can be - Online/Offline or whatever - """ - def __init__(self, name, contacts=None, folded=False): - if not contacts: - contacts = [] - self.contacts = set(contacts) - self.name = name if name is not None else '' - self.folded = folded # if the group content is to be shown - - def __iter__(self): - """Iterate over the contacts""" - return iter(self.contacts) - - def __repr__(self): - return '<Roster_group: %s; %s>' % (self.name, self.contacts) - - def __len__(self): - """Number of contacts in the group""" - return len(self.contacts) - - def __contains__(self, contact): - """ - Return a bool, telling if the contact is in the group - """ - return contact in self.contacts - - def add(self, contact): - """Add a contact to the group""" - self.contacts.add(contact) - - def remove(self, contact): - """Remove a contact from the group if present""" - if contact in self.contacts: - self.contacts.remove(contact) - - def get_contacts(self, contact_filter=None, sort=''): - """Return the group contacts, filtered and sorted""" - contact_list = self.contacts.copy() if not contact_filter\ - else [contact for contact in self.contacts.copy() if contact_filter[0](contact, contact_filter[1])] - contact_list = sorted(contact_list, key=SORTING_METHODS['name']) - - for sorting in sort.split(':'): - if sorting == 'reverse': - contact_list = list(reversed(contact_list)) - else: - method = SORTING_METHODS.get(sorting, lambda x: 0) - contact_list = sorted(contact_list, key=method) - return contact_list - - def toggle_folded(self): - """Fold/unfold the group in the roster""" - self.folded = not self.folded - if self.folded: - if self.name not in roster.folded_groups: - roster.folded_groups.add(self.name) - else: - if self.name in roster.folded_groups: - roster.folded_groups.remove(self.name) - - def get_nb_connected_contacts(self): - """Return the number of connected contacts""" - return len([1 for contact in self.contacts if len(contact)]) - -def create_roster(): - "Create the global roster object" - global roster - roster = Roster() - -# Shared roster object -roster = None diff --git a/src/roster_sorting.py b/src/roster_sorting.py deleted file mode 100644 index c57f0dce..00000000 --- a/src/roster_sorting.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Defines the roster sorting methods used in roster.py -(for contacts/groups) -""" - -########################### Contacts sorting ############################ - -PRESENCE_PRIORITY = {'unavailable': 5, - 'xa': 4, - 'away': 3, - 'dnd': 2, - '': 1, - 'available': 1} - -def sort_jid(contact): - """Sort by contact JID""" - return contact.bare_jid - -def sort_show(contact): - """Sort by show (from high availability to low)""" - res = contact.get_highest_priority_resource() - if not res: - return 5 - show = res.presence - if show not in PRESENCE_PRIORITY: - return 0 - return PRESENCE_PRIORITY[show] - -def sort_resource_nb(contact): - """Sort by number of connected resources""" - return - len(contact) - -def sort_name(contact): - """Sort by name (case insensitive)""" - return contact.name.lower() or contact.bare_jid - -def sort_sname(contact): - """Sort by name (case sensitive)""" - return contact.name or contact.bare_jid - -def sort_online(contact): - """Sort by connected/disconnected""" - result = sort_show(contact) - return 0 if result < 5 else 1 - -SORTING_METHODS = { - 'jid': sort_jid, - 'sname': sort_sname, - 'show': sort_show, - 'resource': sort_resource_nb, - 'name': sort_name, - 'online': sort_online, -} - - -######################## Roster Groups sorting ########################## - -def sort_group_name(group): - """Sort by name (case insensitive)""" - return group.name.lower() - -def sort_group_sname(group): - """Sort by name (case-sensitive)""" - return group.name - -def sort_group_folded(group): - """Sort by folded/unfolded""" - return group.folded - -def sort_group_connected(group): - """Sort by number of connected contacts""" - return - group.get_nb_connected_contacts() - -def sort_group_size(group): - """Sort by group size""" - return - len(group) - -def sort_group_none(group): - """Put the none group at the end, if any""" - return 0 if group.name != 'none' else 1 - -GROUP_SORTING_METHODS = { - 'name': sort_group_name, - 'fold': sort_group_folded, - 'connected': sort_group_connected, - 'size': sort_group_size, - 'none': sort_group_none, - 'sname': sort_group_sname, -} - diff --git a/src/singleton.py b/src/singleton.py deleted file mode 100644 index 9133012b..00000000 --- a/src/singleton.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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 a Singleton function that initialize an object -of the given class if it was never instantiated yet. Else, returns -the previously instantiated object. -This method is the only one that I can come up with that do not call -__init__() each time. -""" - -instances = {} -def Singleton(cls, *args, **kwargs): - if not cls in instances: - instances[cls] = cls(*args, **kwargs) - return instances[cls] diff --git a/src/size_manager.py b/src/size_manager.py deleted file mode 100644 index 1cad83fd..00000000 --- a/src/size_manager.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Size Manager: - used to check size boundaries of the whole window and - specific tabs -""" -THRESHOLD_WIDTH_DEGRADE = 45 -THRESHOLD_HEIGHT_DEGRADE = 10 - -FULL_WIDTH_DEGRADE = 66 -FULL_HEIGHT_DEGRADE = 10 - -class SizeManager(object): - - def __init__(self, core, win_cls): - self._win_class = win_cls - self._core = core - - @property - def tab_scr(self): - return self._win_class._tab_win - - @property - def core_scr(self): - return self._core.stdscr - - @property - def tab_degrade_x(self): - _, x = self.tab_scr.getmaxyx() - return x < THRESHOLD_WIDTH_DEGRADE - - @property - def tab_degrade_y(self): - y, x = self.tab_scr.getmaxyx() - return y < THRESHOLD_HEIGHT_DEGRADE - - @property - def core_degrade_x(self): - y, x = self.core_scr.getmaxyx() - return x < FULL_WIDTH_DEGRADE - - @property - def core_degrade_y(self): - y, x = self.core_scr.getmaxyx() - return y < FULL_HEIGHT_DEGRADE - - diff --git a/src/tabs/__init__.py b/src/tabs/__init__.py deleted file mode 100644 index d0a881a6..00000000 --- a/src/tabs/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from . basetabs import Tab, ChatTab, GapTab, OneToOneTab -from . basetabs import STATE_PRIORITY -from . rostertab import RosterInfoTab -from . muctab import MucTab, NS_MUC_USER -from . privatetab import PrivateTab -from . conversationtab import ConversationTab, StaticConversationTab,\ - DynamicConversationTab -from . xmltab import XMLTab -from . listtab import ListTab -from . muclisttab import MucListTab -from . adhoc_commands_list import AdhocCommandsListTab -from . data_forms import DataFormsTab -from . bookmarkstab import BookmarksTab diff --git a/src/tabs/adhoc_commands_list.py b/src/tabs/adhoc_commands_list.py deleted file mode 100644 index 10ebf22b..00000000 --- a/src/tabs/adhoc_commands_list.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -A tab listing the ad-hoc commands on a specific JID. The user can -select one of them and start executing it, or just close the tab and do -nothing. -""" - -import logging -log = logging.getLogger(__name__) - -from . import ListTab - -from slixmpp.plugins.xep_0030.stanza.items import DiscoItem - -class AdhocCommandsListTab(ListTab): - plugin_commands = {} - plugin_keys = {} - - def __init__(self, jid): - ListTab.__init__(self, jid.full, - "“Enter”: execute selected command.", - 'Ad-hoc commands of JID %s (Loading)' % jid, - (('Node', 0), ('Description', 1))) - self.key_func['^M'] = self.execute_selected_command - - def execute_selected_command(self): - if not self.listview or not self.listview.get_selected_row(): - return - node, name, jid = self.listview.get_selected_row() - session = {'next': self.core.on_next_adhoc_step, - 'error': self.core.on_adhoc_error} - self.core.xmpp.plugin['xep_0050'].start_command(jid, node, session) - - def get_columns_sizes(self): - return {'Node': int(self.width * 3 / 8), - 'Description': int(self.width * 5 / 8)} - - def on_list_received(self, iq): - """ - Fill the listview with the value from the received iq - """ - if iq['type'] == 'error': - self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text']) - return - def get_items(): - substanza = iq['disco_items'] - for item in substanza['substanzas']: - if isinstance(item, DiscoItem): - yield item - items = [(item['node'], item['name'] or '', item['jid']) for item in get_items()] - self.listview.set_lines(items) - self.info_header.message = 'Ad-hoc commands of JID %s' % self.name - if self.core.current_tab() is self: - self.refresh() - else: - self.state = 'highlight' - self.refresh_tab_win() - self.core.doupdate() diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py deleted file mode 100644 index bb0c0ea4..00000000 --- a/src/tabs/basetabs.py +++ /dev/null @@ -1,881 +0,0 @@ -""" -Module for the base Tabs - -The root class Tab defines the generic interface and attributes of a -tab. A tab organizes various Windows around the screen depending -of the tab specificity. If the tab shows messages, it will also -reference a buffer containing the messages. - -Each subclass should redefine its own refresh() and resize() method -according to its windows. - -This module also defines ChatTabs, the parent class for all tabs -revolving around chats. -""" - -import logging -log = logging.getLogger(__name__) - -import singleton -import string -import time -import weakref -from datetime import datetime, timedelta -from xml.etree import cElementTree as ET - -import core -import timed_events -import windows -import xhtml -from common import safeJID -from config import config -from decorators import refresh_wrapper -from logger import logger -from text_buffer import TextBuffer -from theming import get_theme, dump_tuple -from decorators import command_args_parser - -# getters for tab colors (lambdas, so that they are dynamic) -STATE_COLORS = { - 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED, - 'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED, - 'nonempty': lambda: get_theme().COLOR_TAB_NONEMPTY, - 'joined': lambda: get_theme().COLOR_TAB_JOINED, - 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE, - 'composing': lambda: get_theme().COLOR_TAB_COMPOSING, - 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT, - 'private': lambda: get_theme().COLOR_TAB_PRIVATE, - 'normal': lambda: get_theme().COLOR_TAB_NORMAL, - 'current': lambda: get_theme().COLOR_TAB_CURRENT, - 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, - } -VERTICAL_STATE_COLORS = { - 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED, - 'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED, - 'nonempty': lambda: get_theme().COLOR_VERTICAL_TAB_NONEMPTY, - 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED, - 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE, - 'composing': lambda: get_theme().COLOR_VERTICAL_TAB_COMPOSING, - 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT, - 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE, - 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL, - 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT, - 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, - } - - -# priority of the different tab states when using Alt+e -# higher means more priority, < 0 means not selectable -STATE_PRIORITY = { - 'normal': -1, - 'current': -1, - 'disconnected': 0, - 'nonempty': 0.1, - 'scrolled': 0.5, - 'joined': 0.8, - 'composing': 0.9, - 'message': 1, - 'highlight': 2, - 'private': 2, - 'attention': 3 - } - -class Tab(object): - tab_core = None - size_manager = None - - plugin_commands = {} - plugin_keys = {} - def __init__(self): - if not hasattr(self, 'name'): - self.name = self.__class__.__name__ - self.input = None - self.closed = False - self._state = 'normal' - self._prev_state = None - - self.need_resize = False - self.key_func = {} # each tab should add their keys in there - # and use them in on_input - self.commands = {} # and their own commands - - - @property - def size(self): - if not Tab.size_manager: - Tab.size_manager = self.core.size - return Tab.size_manager - - @property - def core(self): - if not Tab.tab_core: - Tab.tab_core = singleton.Singleton(core.Core) - return Tab.tab_core - - @property - def nb(self): - for index, tab in enumerate(self.core.tabs): - if tab == self: - return index - return len(self.core.tabs) - - @property - def tab_win(self): - if not Tab.tab_core: - Tab.tab_core = singleton.Singleton(core.Core) - return Tab.tab_core.tab_win - - @property - def left_tab_win(self): - if not Tab.tab_core: - Tab.tab_core = singleton.Singleton(core.Core) - return Tab.tab_core.left_tab_win - - @staticmethod - def tab_win_height(): - """ - Returns 1 or 0, depending on if we are using the vertical tab list - or not. - """ - if config.get('enable_vertical_tab_list'): - return 0 - return 1 - - @property - def info_win(self): - return self.core.information_win - - @property - def color(self): - return STATE_COLORS[self._state]() - - @property - def vertical_color(self): - return VERTICAL_STATE_COLORS[self._state]() - - @property - def state(self): - return self._state - - @state.setter - def state(self, value): - if not value in STATE_COLORS: - log.debug("Invalid value for tab state: %s", value) - elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \ - value not in ('current', 'disconnected') and \ - not (self._state == 'scrolled' and value == 'disconnected'): - log.debug("Did not set state because of lower priority, asked: %s, kept: %s", value, self._state) - elif self._state == 'disconnected' and value not in ('joined', 'current'): - log.debug('Did not set state because disconnected tabs remain visible') - else: - self._state = value - if self._state == 'current': - self._prev_state = None - - def set_state(self, value): - self._state = value - - def save_state(self): - if self._state != 'composing': - self._prev_state = self._state - - def restore_state(self): - if self.state == 'composing' and self._prev_state: - self._state = self._prev_state - self._prev_state = None - elif not self._prev_state: - self._state = 'normal' - - @staticmethod - def resize(scr): - Tab.height, Tab.width = scr.getmaxyx() - windows.Win._tab_win = scr - - def missing_command_callback(self, command_name): - """ - Callback executed when a command is not found. - Returns True if the callback took care of displaying - the error message, False otherwise. - """ - return False - - def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''): - """ - Add a command - """ - if name in self.commands: - return - if not desc and shortdesc: - desc = shortdesc - self.commands[name] = core.Command(func, desc, completion, shortdesc, usage) - - def complete_commands(self, the_input): - """ - Does command completion on the specified input for both global and tab-specific - commands. - This should be called from the completion method (on tab, for example), passing - the input where completion is to be made. - It can completion the command name itself or an argument of the command. - Returns True if a completion was made, False else. - """ - txt = the_input.get_text() - # check if this is a command - if txt.startswith('/') and not txt.startswith('//'): - position = the_input.get_argument_position(quoted=False) - if position == 0: - words = ['/%s'% (name) for name in sorted(self.core.commands)] +\ - ['/%s' % (name) for name in sorted(self.commands)] - the_input.new_completion(words, 0) - # Do not try to cycle command completion if there was only - # one possibily. The next tab will complete the argument. - # Otherwise we would need to add a useless space before being - # able to complete the arguments. - hit_copy = set(the_input.hit_list) - while not hit_copy: - whitespace = the_input.text.find(' ') - if whitespace == -1: - whitespace = len(the_input.text) - the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:] - the_input.new_completion(words, 0) - hit_copy = set(the_input.hit_list) - if len(hit_copy) == 1: - the_input.do_command(' ') - the_input.reset_completion() - return True - # check if we are in the middle of the command name - elif len(txt.split()) > 1 or\ - (txt.endswith(' ') and not the_input.last_completion): - command_name = txt.split()[0][1:] - if command_name in self.commands: - command = self.commands[command_name] - elif command_name in self.core.commands: - command = self.core.commands[command_name] - else: # Unknown command, cannot complete - return False - if command[2] is None: - return False # There's no completion function - else: - return command[2](the_input) - return False - - def execute_command(self, provided_text): - """ - Execute the command in the input and return False if - the input didn't contain a command - """ - txt = provided_text or self.input.key_enter() - if txt.startswith('/') and not txt.startswith('//') and\ - not txt.startswith('/me '): - command = txt.strip().split()[0][1:] - arg = txt[2+len(command):] # jump the '/' and the ' ' - func = None - if command in self.commands: # check tab-specific commands - func = self.commands[command][0] - elif command in self.core.commands: # check global commands - func = self.core.commands[command][0] - else: - low = command.lower() - if low in self.commands: - func = self.commands[low][0] - elif low in self.core.commands: - func = self.core.commands[low][0] - else: - if self.missing_command_callback is not None: - error_handled = self.missing_command_callback(low) - if not error_handled: - self.core.information("Unknown command (%s)" % - (command), - 'Error') - if command in ('correct', 'say'): # hack - arg = xhtml.convert_simple_to_full_colors(arg) - else: - arg = xhtml.clean_text_simple(arg) - if func: - if hasattr(self.input, "reset_completion"): - self.input.reset_completion() - func(arg) - return True - else: - return False - - def refresh_tab_win(self): - if config.get('enable_vertical_tab_list'): - if self.left_tab_win and not self.size.core_degrade_x: - self.left_tab_win.refresh() - elif not self.size.core_degrade_y: - self.tab_win.refresh() - - def refresh(self): - """ - Called on each screen refresh (when something has changed) - """ - pass - - def get_name(self): - """ - get the name of the tab - """ - return self.name - - def get_nick(self): - """ - Get the nick of the tab (defaults to its name) - """ - return self.name - - def get_text_window(self): - """ - Returns the principal TextWin window, if there's one - """ - return None - - def on_input(self, key, raw): - """ - raw indicates if the key should activate the associated command or not. - """ - pass - - def update_commands(self): - for c in self.plugin_commands: - if not c in self.commands: - self.commands[c] = self.plugin_commands[c] - - def update_keys(self): - for k in self.plugin_keys: - if not k in self.key_func: - self.key_func[k] = self.plugin_keys[k] - - def on_lose_focus(self): - """ - called when this tab loses the focus. - """ - self.state = 'normal' - - def on_gain_focus(self): - """ - called when this tab gains the focus. - """ - self.state = 'current' - - def on_scroll_down(self): - """ - Defines what happens when we scroll down - """ - pass - - def on_scroll_up(self): - """ - Defines what happens when we scroll up - """ - pass - - def on_line_up(self): - """ - Defines what happens when we scroll one line up - """ - pass - - def on_line_down(self): - """ - Defines what happens when we scroll one line up - """ - pass - - def on_half_scroll_down(self): - """ - Defines what happens when we scroll half a screen down - """ - pass - - def on_half_scroll_up(self): - """ - Defines what happens when we scroll half a screen up - """ - pass - - def on_info_win_size_changed(self): - """ - Called when the window with the informations is resized - """ - pass - - def on_close(self): - """ - Called when the tab is to be closed - """ - if self.input: - self.input.on_delete() - self.closed = True - - def matching_names(self): - """ - Returns a list of strings that are used to name a tab with the /win - command. For example you could switch to a tab that returns - ['hello', 'coucou'] using /win hel, or /win coucou - If not implemented in the tab, it just doesn’t match with anything. - """ - return [] - - def __del__(self): - log.debug('------ Closing tab %s', self.__class__.__name__) - -class GapTab(Tab): - - def __bool__(self): - return False - - def __len__(self): - return 0 - - @property - def name(self): - return '' - - def refresh(self): - log.debug('WARNING: refresh() called on a gap tab, this should not happen') - -class ChatTab(Tab): - """ - A tab containing a chat of any type. - Just use this class instead of Tab if the tab needs a recent-words completion - Also, ^M is already bound to on_enter - And also, add the /say command - """ - plugin_commands = {} - plugin_keys = {} - def __init__(self, jid=''): - Tab.__init__(self) - self.name = jid - self.text_win = None - self._text_buffer = TextBuffer() - self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" - # We keep a reference of the event that will set our chatstate to "paused", so that - # we can delete it or change it if we need to - self.timed_event_paused = None - # Keeps the last sent message to complete it easily in completion_correct, and to replace it. - self.last_sent_message = None - self.key_func['M-v'] = self.move_separator - self.key_func['M-h'] = self.scroll_separator - self.key_func['M-/'] = self.last_words_completion - self.key_func['^M'] = self.on_enter - self.register_command('say', self.command_say, - usage='<message>', - shortdesc='Send the message.') - self.register_command('xhtml', self.command_xhtml, - usage='<custom xhtml>', - shortdesc='Send custom XHTML.') - self.register_command('clear', self.command_clear, - shortdesc='Clear the current buffer.') - self.register_command('correct', self.command_correct, - desc='Fix the last message with whatever you want.', - shortdesc='Correct the last message.', - completion=self.completion_correct) - self.chat_state = None - self.update_commands() - self.update_keys() - - # Get the logs - log_nb = config.get('load_log') - logs = self.load_logs(log_nb) - - if logs: - for message in logs: - self._text_buffer.add_message(**message) - - @property - def is_muc(self): - return False - - def load_logs(self, log_nb): - logs = logger.get_logs(safeJID(self.name).bare, log_nb) - return logs - - def log_message(self, txt, nickname, time=None, typ=1): - """ - Log the messages in the archives. - """ - name = safeJID(self.name).bare - if not logger.log_message(name, nickname, txt, date=time, typ=typ): - self.core.information('Unable to write in the log file', 'Error') - - def add_message(self, txt, time=None, nickname=None, forced_user=None, - nick_color=None, identifier=None, jid=None, history=None, - typ=1, highlight=False): - self.log_message(txt, nickname, time=time, typ=typ) - self._text_buffer.add_message(txt, time=time, - nickname=nickname, - highlight=highlight, - nick_color=nick_color, - history=history, - user=forced_user, - identifier=identifier, - jid=jid) - - def modify_message(self, txt, old_id, new_id, user=None, jid=None, nickname=None): - self.log_message(txt, nickname, typ=1) - message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid) - if message: - self.text_win.modify_message(old_id, message) - self.core.refresh_window() - return True - return False - - def last_words_completion(self): - """ - Complete the input with words recently said - """ - # build the list of the recent words - char_we_dont_want = string.punctuation+' ’„“”…«»' - words = list() - for msg in self._text_buffer.messages[:-40:-1]: - if not msg: - continue - txt = xhtml.clean_text(msg.txt) - for char in char_we_dont_want: - txt = txt.replace(char, ' ') - for word in txt.split(): - if len(word) >= 4 and word not in words: - words.append(word) - words.extend([word for word in config.get('words').split(':') if word]) - self.input.auto_completion(words, ' ', quotify=False) - - def on_enter(self): - txt = self.input.key_enter() - if txt: - if not self.execute_command(txt): - if txt.startswith('//'): - txt = txt[1:] - self.command_say(xhtml.convert_simple_to_full_colors(txt)) - self.cancel_paused_delay() - - @command_args_parser.raw - def command_xhtml(self, xhtml): - """" - /xhtml <custom xhtml> - """ - message = self.generate_xhtml_message(xhtml) - if message: - message.send() - - def generate_xhtml_message(self, arg): - if not arg: - return - try: - body = xhtml.clean_text(xhtml.xhtml_to_poezio_colors(arg)) - ET.fromstring(arg) - except: - self.core.information('Could not send custom xhtml', 'Error') - log.error('/xhtml: Unable to send custom xhtml', exc_info=True) - return - - msg = self.core.xmpp.make_message(self.get_dest_jid()) - msg['body'] = body - msg.enable('html') - msg['html']['body'] = arg - return msg - - def get_dest_jid(self): - return self.name - - @refresh_wrapper.always - def command_clear(self, ignored): - """ - /clear - """ - self._text_buffer.messages = [] - self.text_win.rebuild_everything(self._text_buffer) - - def send_chat_state(self, state, always_send=False): - """ - Send an empty chatstate message - """ - if not self.is_muc or self.joined: - if state in ('active', 'inactive', 'gone') and self.inactive and not always_send: - return - if (config.get_by_tabname('send_chat_states', self.general_jid) - and self.remote_wants_chatstates is not False): - msg = self.core.xmpp.make_message(self.get_dest_jid()) - msg['type'] = self.message_type - msg['chat_state'] = state - self.chat_state = state - msg.send() - return True - - def send_composing_chat_state(self, empty_after): - """ - Send the "active" or "composing" chatstate, depending - on the the current status of the input - """ - name = self.general_jid - if (config.get_by_tabname('send_chat_states', name) - and self.remote_wants_chatstates): - needed = 'inactive' if self.inactive else 'active' - self.cancel_paused_delay() - if not empty_after: - if self.chat_state != "composing": - self.send_chat_state("composing") - self.set_paused_delay(True) - elif empty_after and self.chat_state != needed: - self.send_chat_state(needed, True) - - def set_paused_delay(self, composing): - """ - we create a timed event that will put us to paused - in a few seconds - """ - if not config.get_by_tabname('send_chat_states', self.general_jid): - return - # First, cancel the delay if it already exists, before rescheduling - # it at a new date - self.cancel_paused_delay() - new_event = timed_events.DelayedEvent(4, self.send_chat_state, 'paused') - self.core.add_timed_event(new_event) - self.timed_event_paused = new_event - - def cancel_paused_delay(self): - """ - Remove that event from the list and set it to None. - Called for example when the input is emptied, or when the message - is sent - """ - if self.timed_event_paused is not None: - self.core.remove_timed_event(self.timed_event_paused) - self.timed_event_paused = None - - @command_args_parser.raw - def command_correct(self, line): - """ - /correct <fixed message> - """ - if not line: - self.core.command_help('correct') - return - if not self.last_sent_message: - self.core.information('There is no message to correct.') - return - self.command_say(line, correct=True) - - def completion_correct(self, the_input): - if self.last_sent_message and the_input.get_argument_position() == 1: - return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False) - - @property - def inactive(self): - """Whether we should send inactive or active as a chatstate""" - return self.core.status.show in ('xa', 'away') or\ - (hasattr(self, 'directed_presence') and not self.directed_presence) - - def move_separator(self): - self.text_win.remove_line_separator() - self.text_win.add_line_separator(self._text_buffer) - self.text_win.refresh() - self.input.refresh() - - def get_conversation_messages(self): - return self._text_buffer.messages - - def check_scrolled(self): - if self.text_win.pos != 0: - self.state = 'scrolled' - - @command_args_parser.raw - def command_say(self, line, correct=False): - pass - - def on_line_up(self): - return self.text_win.scroll_up(1) - - def on_line_down(self): - return self.text_win.scroll_down(1) - - def on_scroll_up(self): - return self.text_win.scroll_up(self.text_win.height-1) - - def on_scroll_down(self): - return self.text_win.scroll_down(self.text_win.height-1) - - def on_half_scroll_up(self): - return self.text_win.scroll_up((self.text_win.height-1) // 2) - - def on_half_scroll_down(self): - return self.text_win.scroll_down((self.text_win.height-1) // 2) - - @refresh_wrapper.always - def scroll_separator(self): - self.text_win.scroll_to_separator() - -class OneToOneTab(ChatTab): - - def __init__(self, jid=''): - ChatTab.__init__(self, jid) - - # Set to true once the first disco is done - self.__initial_disco = False - # change this to True or False when - # we know that the remote user wants chatstates, or not. - # None means we don’t know yet, and we send only "active" chatstates - self._remote_wants_chatstates = None - self.remote_supports_attention = True - self.remote_supports_receipts = True - self.check_features() - - @property - def remote_wants_chatstates(self): - return self._remote_wants_chatstates - - @remote_wants_chatstates.setter - def remote_wants_chatstates(self, value): - old_value = self._remote_wants_chatstates - self._remote_wants_chatstates = value - if (old_value is None and value != None) or \ - (old_value != value and value != None): - ok = get_theme().CHAR_OK - nope = get_theme().CHAR_EMPTY - support = ok if value else nope - if value: - msg = '\x19%s}Contact supports chat states [%s].' - else: - msg = '\x19%s}Contact does not support chat states [%s].' - color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - msg = msg % (color, support) - self.add_message(msg, typ=0) - self.core.refresh_window() - - def ack_message(self, msg_id, msg_jid): - """ - Ack a message - """ - new_msg = self._text_buffer.ack_message(msg_id, msg_jid) - if new_msg: - self.text_win.modify_message(msg_id, new_msg) - self.core.refresh_window() - - def nack_message(self, error, msg_id, msg_jid): - """ - Ack a message - """ - new_msg = self._text_buffer.nack_message(error, msg_id, msg_jid) - if new_msg: - self.text_win.modify_message(msg_id, new_msg) - self.core.refresh_window() - return True - return False - - @command_args_parser.raw - def command_xhtml(self, xhtml_data): - message = self.generate_xhtml_message(xhtml_data) - if message: - message['type'] = 'chat' - if self.remote_supports_receipts: - message._add_receipt = True - if self.remote_wants_chatstates: - message['chat_sate'] = 'active' - message.send() - body = xhtml.xhtml_to_poezio_colors(xhtml_data, force=True) - self._text_buffer.add_message(body, nickname=self.core.own_nick, - identifier=message['id'],) - self.refresh() - - def check_features(self): - "check the features supported by the other party" - if safeJID(self.get_dest_jid()).resource: - self.core.xmpp.plugin['xep_0030'].get_info( - jid=self.get_dest_jid(), timeout=5, - callback=self.features_checked) - - @command_args_parser.raw - def command_attention(self, message): - """/attention [message]""" - if message is not '': - self.command_say(message, attention=True) - else: - msg = self.core.xmpp.make_message(self.get_dest_jid()) - msg['type'] = 'chat' - msg['attention'] = True - msg.send() - - @command_args_parser.raw - def command_say(self, line, correct=False, attention=False): - pass - - def missing_command_callback(self, command_name): - if command_name not in ('correct', 'attention'): - return False - - if command_name == 'correct': - feature = 'message correction' - elif command_name == 'attention': - feature = 'attention requests' - msg = ('%s does not support %s, therefore the /%s ' - 'command is currently disabled in this tab.') - msg = msg % (self.name, feature, command_name) - self.core.information(msg, 'Info') - return True - - def _feature_attention(self, features): - "Check for the 'attention' features" - if 'urn:xmpp:attention:0' in features: - self.remote_supports_attention = True - self.register_command('attention', self.command_attention, - usage='[message]', - shortdesc='Request the attention.', - desc='Attention: Request the attention of ' - 'the contact. Can also send a message' - ' along with the attention.') - else: - self.remote_supports_attention = False - return self.remote_supports_attention - - def _feature_correct(self, features): - "Check for the 'correction' feature" - if not 'urn:xmpp:message-correct:0' in features: - if 'correct' in self.commands: - del self.commands['correct'] - elif not 'correct' in self.commands: - self.register_command('correct', self.command_correct, - desc='Fix the last message with whatever you want.', - shortdesc='Correct the last message.', - completion=self.completion_correct) - return 'correct' in self.commands - - def _feature_receipts(self, features): - "Check for the 'receipts' feature" - if 'urn:xmpp:receipts' in features: - self.remote_supports_receipts = True - else: - self.remote_supports_receipts = False - return self.remote_supports_receipts - - def features_checked(self, iq): - "Features check callback" - features = iq['disco_info'].get_features() or [] - before = ('correct' in self.commands, - self.remote_supports_attention, - self.remote_supports_receipts) - correct = self._feature_correct(features) - attention = self._feature_attention(features) - receipts = self._feature_receipts(features) - - if (correct, attention, receipts) == before and self.__initial_disco: - return - else: - self.__initial_disco = True - - if not (correct or attention or receipts): - return # don’t display anything - - ok = get_theme().CHAR_OK - nope = get_theme().CHAR_EMPTY - - correct = ok if correct else nope - attention = ok if attention else nope - receipts = ok if receipts else nope - - msg = ('\x19%s}Contact supports: correction [%s], ' - 'attention [%s], receipts [%s].') - color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - msg = msg % (color, correct, attention, receipts) - self.add_message(msg, typ=0) - self.core.refresh_window() - - diff --git a/src/tabs/bookmarkstab.py b/src/tabs/bookmarkstab.py deleted file mode 100644 index 7f5069ea..00000000 --- a/src/tabs/bookmarkstab.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Defines the data-forms Tab -""" - -import logging -log = logging.getLogger(__name__) - -import windows -from bookmarks import Bookmark, BookmarkList, stanza_storage -from tabs import Tab -from common import safeJID - - -class BookmarksTab(Tab): - """ - A tab displaying lines of bookmarks, each bookmark having - a 4 widgets to set the jid/password/autojoin/storage method - """ - plugin_commands = {} - def __init__(self, bookmarks: BookmarkList): - Tab.__init__(self) - self.name = "Bookmarks" - self.bookmarks = bookmarks - self.new_bookmarks = [] - self.removed_bookmarks = [] - self.header_win = windows.ColumnHeaderWin(('room@server/nickname', - 'password', - 'autojoin', - 'storage')) - self.bookmarks_win = windows.BookmarksWin(self.bookmarks, - self.height-4, - self.width, 1, 0) - self.help_win = windows.HelpText('Ctrl+Y: save, Ctrl+G: cancel, ' - '↑↓: change lines, tab: change ' - 'column, M-a: add bookmark, C-k' - ': delete bookmark') - self.info_header = windows.BookmarksInfoWin() - self.key_func['KEY_UP'] = self.bookmarks_win.go_to_previous_line_input - self.key_func['KEY_DOWN'] = self.bookmarks_win.go_to_next_line_input - self.key_func['^I'] = self.bookmarks_win.go_to_next_horizontal_input - self.key_func['^G'] = self.on_cancel - self.key_func['^Y'] = self.on_save - self.key_func['M-a'] = self.add_bookmark - self.key_func['^K'] = self.del_bookmark - self.resize() - self.update_commands() - - def add_bookmark(self): - new_bookmark = Bookmark(safeJID('room@example.tld/nick'), method='local') - self.new_bookmarks.append(new_bookmark) - self.bookmarks_win.add_bookmark(new_bookmark) - - def del_bookmark(self): - current = self.bookmarks_win.del_current_bookmark() - if current in self.new_bookmarks: - self.new_bookmarks.remove(current) - else: - self.removed_bookmarks.append(current) - - def on_cancel(self): - self.core.close_tab() - return True - - def on_save(self): - self.bookmarks_win.save() - if find_duplicates(self.new_bookmarks): - self.core.information('Duplicate bookmarks in list (saving aborted)', 'Error') - return - for bm in self.new_bookmarks: - if safeJID(bm.jid): - if not self.bookmarks[bm.jid]: - self.bookmarks.append(bm) - else: - self.core.information('Invalid JID for bookmark: %s/%s' % (bm.jid, bm.nick), 'Error') - return - - for bm in self.removed_bookmarks: - if bm in self.bookmarks: - self.bookmarks.remove(bm) - - def send_cb(success): - if success: - self.core.information('Bookmarks saved.', 'Info') - else: - self.core.information('Remote bookmarks not saved.', 'Error') - log.debug('alerte %s', str(stanza_storage(self.bookmarks.bookmarks))) - self.bookmarks.save(self.core.xmpp, callback=send_cb) - self.core.close_tab() - return True - - def on_input(self, key, raw=False): - if key in self.key_func: - res = self.key_func[key]() - if res: - return res - self.bookmarks_win.refresh_current_input() - else: - self.bookmarks_win.on_input(key) - - def resize(self): - self.need_resize = False - self.header_win.resize_columns({ - 'room@server/nickname': self.width//3, - 'password': self.width//3, - 'autojoin': self.width//6, - 'storage': self.width//6 - }) - info_height = self.core.information_win_size - tab_height = Tab.tab_win_height() - self.header_win.resize(1, self.width, 0, 0) - self.bookmarks_win.resize(self.height - 3 - tab_height - info_height, - self.width, 1, 0) - self.help_win.resize(1, self.width, self.height - 1, 0) - self.info_header.resize(1, self.width, - self.height - 2 - tab_height - info_height, 0) - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height - 3: - return - info_height = self.core.information_win_size - tab_height = Tab.tab_win_height() - self.bookmarks_win.resize(self.height - 3 - tab_height - info_height, - self.width, 1, 0) - self.info_header.resize(1, self.width, - self.height - 2 - tab_height - info_height, 0) - - def refresh(self): - if self.need_resize: - self.resize() - self.header_win.refresh() - self.refresh_tab_win() - self.help_win.refresh() - self.info_header.refresh(self.bookmarks.preferred) - self.info_win.refresh() - self.bookmarks_win.refresh() - - -def find_duplicates(bm_list): - jids = set() - for bookmark in bm_list: - if bookmark.jid in jids: - return True - jids.add(bookmark.jid) - return False - diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py deleted file mode 100644 index 1d8c60a4..00000000 --- a/src/tabs/conversationtab.py +++ /dev/null @@ -1,484 +0,0 @@ -""" -Module for the ConversationTabs - -A ConversationTab is a direct chat between two JIDs, outside of a room. - -There are two different instances of a ConversationTab: -- A DynamicConversationTab that implements XEP-0296 (best practices for - resource locking), which means it will switch the resource it is - focused on depending on the presences received. This is the default. -- A StaticConversationTab that will stay focused on one resource all - the time. - -""" -import logging -log = logging.getLogger(__name__) - -import curses - -from . basetabs import OneToOneTab, Tab - -import common -import fixes -import windows -import xhtml -from common import safeJID -from config import config -from decorators import refresh_wrapper -from roster import roster -from theming import get_theme, dump_tuple -from decorators import command_args_parser - -class ConversationTab(OneToOneTab): - """ - The tab containg a normal conversation (not from a MUC) - Must not be instantiated, use Static or Dynamic version only. - """ - plugin_commands = {} - plugin_keys = {} - additional_informations = {} - message_type = 'chat' - def __init__(self, jid): - OneToOneTab.__init__(self, jid) - self.nick = None - self.nick_sent = False - self.state = 'normal' - self.name = jid # a conversation tab is linked to one specific full jid OR bare jid - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) - self.upper_bar = windows.ConversationStatusMessageWin() - self.input = windows.MessageInput() - # keys - self.key_func['^I'] = self.completion - # commands - self.register_command('unquery', self.command_unquery, - shortdesc='Close the tab.') - self.register_command('close', self.command_unquery, - shortdesc='Close the tab.') - self.register_command('version', self.command_version, - desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).', - shortdesc='Get the software version of the user.') - self.register_command('info', self.command_info, - shortdesc='Get the status of the contact.') - self.register_command('last_activity', self.command_last_activity, - usage='[jid]', - desc='Get the last activity of the given or the current contact.', - shortdesc='Get the activity.', - completion=self.core.completion_last_activity) - self.resize() - self.update_commands() - self.update_keys() - - @property - def general_jid(self): - return safeJID(self.name).bare - - @staticmethod - def add_information_element(plugin_name, callback): - """ - Lets a plugin add its own information to the ConversationInfoWin - """ - ConversationTab.additional_informations[plugin_name] = callback - - @staticmethod - def remove_information_element(plugin_name): - del ConversationTab.additional_informations[plugin_name] - - def completion(self): - self.complete_commands(self.input) - - @command_args_parser.raw - def command_say(self, line, attention=False, correct=False): - msg = self.core.xmpp.make_message(self.get_dest_jid()) - msg['type'] = 'chat' - msg['body'] = line - if not self.nick_sent: - msg['nick'] = self.core.own_nick - self.nick_sent = True - # trigger the event BEFORE looking for colors. - # and before displaying the message in the window - # This lets a plugin insert \x19xxx} colors, that will - # be converted in xhtml. - self.core.events.trigger('conversation_say', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - replaced = False - if correct or msg['replace']['id']: - msg['replace']['id'] = self.last_sent_message['id'] - if config.get_by_tabname('group_corrections', self.name): - try: - self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], jid=self.core.xmpp.boundjid, - nickname=self.core.own_nick) - replaced = True - except: - log.error('Unable to correct a message', exc_info=True) - else: - del msg['replace'] - if msg['body'].find('\x19') != -1: - msg.enable('html') - msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) - msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', self.general_jid) and - self.remote_wants_chatstates is not False): - needed = 'inactive' if self.inactive else 'active' - msg['chat_state'] = needed - if attention and self.remote_supports_attention: - msg['attention'] = True - self.core.events.trigger('conversation_say_after', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - if not replaced: - self.add_message(msg['body'], - nickname=self.core.own_nick, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=msg['id'], - jid=self.core.xmpp.boundjid, - typ=1) - - self.last_sent_message = msg - if self.remote_supports_receipts: - msg._add_receipt = True - msg.send() - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - - @command_args_parser.quoted(0, 1) - def command_last_activity(self, args): - """ - /last_activity [jid] - """ - if args and args[0]: - return self.core.command_last_activity(args[0]) - - def callback(iq): - if iq['type'] != 'result': - if iq['error']['type'] == 'auth': - self.core.information('You are not allowed to see the activity of this contact.', 'Error') - else: - self.core.information('Error retrieving the activity', 'Error') - return - seconds = iq['last_activity']['seconds'] - status = iq['last_activity']['status'] - from_ = iq['from'] - msg = '\x19%s}The last activity of %s was %s ago%s' - if not safeJID(from_).user: - msg = '\x19%s}The uptime of %s is %s.' % ( - dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - from_, - common.parse_secs_to_str(seconds)) - else: - msg = '\x19%s}The last activity of %s was %s ago%s' % ( - dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - from_, - common.parse_secs_to_str(seconds), - (' and his/her last status was %s' % status) if status else '',) - self.add_message(msg) - self.core.refresh_window() - - self.core.xmpp.plugin['xep_0012'].get_last_activity(self.get_dest_jid(), callback=callback) - - @refresh_wrapper.conditional - @command_args_parser.ignored - def command_info(self): - contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - if resource: - status = ('Status: %s' % resource.status) if resource.status else '' - self._text_buffer.add_message("\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % { - 'show': resource.show or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) - return True - else: - self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) - return True - - @command_args_parser.ignored - def command_unquery(self): - self.core.close_tab() - - @command_args_parser.quoted(0, 1) - def command_version(self, args): - """ - /version [jid] - """ - def callback(res): - if not res: - return self.core.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.core.information(version, 'Info') - if args: - return self.core.command_version(args[0]) - jid = safeJID(self.name) - if not jid.resource: - if jid in roster: - resource = roster[jid].get_highest_priority_resource() - jid = resource.jid if resource else jid - fixes.get_version(self.core.xmpp, jid, - callback=callback) - - def resize(self): - self.need_resize = False - if self.size.tab_degrade_y: - display_bar = False - info_win_height = 0 - tab_win_height = 0 - bar_height = 0 - else: - display_bar = True - info_win_height = self.core.information_win_size - tab_win_height = Tab.tab_win_height() - bar_height = 1 - - self.text_win.resize(self.height - 2 - bar_height - info_win_height - - tab_win_height, - self.width, bar_height, 0) - self.text_win.rebuild_everything(self._text_buffer) - if display_bar: - self.upper_bar.resize(1, self.width, 0, 0) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height - 1, 0) - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - display_bar = display_info_win = not self.size.tab_degrade_y - - self.text_win.refresh() - - if display_bar: - self.upper_bar.refresh(self.get_dest_jid(), roster[self.get_dest_jid()]) - self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_informations) - - if display_info_win: - self.info_win.refresh() - self.refresh_tab_win() - self.input.refresh() - - def refresh_info_header(self): - self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], - self.text_win, self.chatstate, ConversationTab.additional_informations) - self.input.refresh() - - def get_nick(self): - jid = safeJID(self.name) - contact = roster[jid.bare] - if contact: - return contact.name or jid.user - else: - if self.nick: - return self.nick - return jid.user - - def on_input(self, key, raw): - if not raw and key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key, raw=raw) - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - self.send_composing_chat_state(empty_after) - return False - - def on_lose_focus(self): - contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - if self.input.text: - self.state = 'nonempty' - else: - self.state = 'normal' - self.text_win.remove_line_separator() - self.text_win.add_line_separator(self._text_buffer) - if (config.get_by_tabname('send_chat_states', self.general_jid) - and (not self.input.get_text() - or not self.input.get_text().startswith('//'))): - if resource: - self.send_chat_state('inactive') - self.check_scrolled() - - def on_gain_focus(self): - contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - - self.state = 'current' - curses.curs_set(1) - if (config.get_by_tabname('send_chat_states', self.general_jid) - and (not self.input.get_text() - or not self.input.get_text().startswith('//'))): - if resource: - self.send_chat_state('active') - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: - return - self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0) - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - - def get_text_window(self): - return self.text_win - - def on_close(self): - Tab.on_close(self) - if config.get_by_tabname('send_chat_states', self.general_jid): - self.send_chat_state('gone') - - def matching_names(self): - res = [] - jid = safeJID(self.name) - res.append((2, jid.bare)) - res.append((1, jid.user)) - contact = roster[self.name] - if contact and contact.name: - res.append((0, contact.name)) - return res - -class DynamicConversationTab(ConversationTab): - """ - A conversation tab associated with one bare JID that can be “locked” to - a full jid, and unlocked, as described in the XEP-0296. - Only one DynamicConversationTab can be opened for a given jid. - """ - def __init__(self, jid, resource=None): - self.locked_resource = None - self.name = safeJID(jid).bare - if resource: - self.lock(resource) - self.info_header = windows.DynamicConversationInfoWin() - ConversationTab.__init__(self, jid) - self.register_command('unlock', self.unlock_command, - shortdesc='Unlock the conversation from a particular resource.') - - def lock(self, resource): - """ - Lock the tab to the resource. - """ - assert(resource) - if resource != self.locked_resource: - self.locked_resource = resource - info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID) - - message = ('%(info)sConversation locked to ' - '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % { - 'info': info, - 'jid_c': jid_c, - 'jid': self.name, - 'resource': resource} - self.add_message(message, typ=0) - self.check_features() - - def unlock_command(self, arg=None): - self.unlock() - self.refresh_info_header() - - def unlock(self, from_=None): - """ - Unlock the tab from a resource. It is now “associated” with the bare - jid. - """ - self.remote_wants_chatstates = None - if self.locked_resource != None: - self.locked_resource = None - info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID) - - if from_: - message = ('%(info)sConversation unlocked (received activity' - ' from %(jid_c)s%(jid)s%(info)s).') % { - 'info': info, - 'jid_c': jid_c, - 'jid': from_} - self.add_message(message, typ=0) - else: - message = '%sConversation unlocked.' % info - self.add_message(message, typ=0) - - def get_dest_jid(self): - """ - Returns the full jid (using the locked resource), or the bare jid if - the conversation is not locked. - """ - if self.locked_resource: - return "%s/%s" % (self.name, self.locked_resource) - return self.name - - def refresh(self): - """ - Different from the parent class only for the info_header object. - """ - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - display_bar = display_info_win = not self.size.tab_degrade_y - - self.text_win.refresh() - if display_bar: - self.upper_bar.refresh(self.name, roster[self.name]) - if self.locked_resource: - displayed_jid = "%s/%s" % (self.name, self.locked_resource) - else: - displayed_jid = self.name - self.info_header.refresh(displayed_jid, roster[self.name], - self.text_win, self.chatstate, - ConversationTab.additional_informations) - if display_info_win: - self.info_win.refresh() - - self.refresh_tab_win() - self.input.refresh() - - def refresh_info_header(self): - """ - Different from the parent class only for the info_header object. - """ - if self.locked_resource: - displayed_jid = "%s/%s" % (self.name, self.locked_resource) - else: - displayed_jid = self.name - self.info_header.refresh(displayed_jid, roster[self.name], - self.text_win, self.chatstate, ConversationTab.additional_informations) - self.input.refresh() - -class StaticConversationTab(ConversationTab): - """ - A conversation tab associated with one Full JID. It cannot be locked to - an different resource or unlocked. - """ - def __init__(self, jid): - assert(safeJID(jid).resource) - self.info_header = windows.ConversationInfoWin() - ConversationTab.__init__(self, jid) - - diff --git a/src/tabs/data_forms.py b/src/tabs/data_forms.py deleted file mode 100644 index 0fad2974..00000000 --- a/src/tabs/data_forms.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Defines the data-forms Tab -""" - -import logging -log = logging.getLogger(__name__) - -import windows -from tabs import Tab - -class DataFormsTab(Tab): - """ - A tab contaning various window type, displaying - a form that the user needs to fill. - """ - plugin_commands = {} - def __init__(self, form, on_cancel, on_send, kwargs): - Tab.__init__(self) - self._form = form - self._on_cancel = on_cancel - self._on_send = on_send - self._kwargs = kwargs - self.fields = [] - for field in self._form: - self.fields.append(field) - self.topic_win = windows.Topic() - self.form_win = windows.FormWin(form, self.height-4, self.width, 1, 0) - self.help_win = windows.HelpText("Ctrl+Y: send form, Ctrl+G: cancel") - self.help_win_dyn = windows.HelpText() - self.key_func['KEY_UP'] = self.form_win.go_to_previous_input - self.key_func['KEY_DOWN'] = self.form_win.go_to_next_input - self.key_func['^G'] = self.on_cancel - self.key_func['^Y'] = self.on_send - self.resize() - self.update_commands() - - def on_cancel(self): - self._on_cancel(self._form, **self._kwargs) - return True - - def on_send(self): - self._form.reply() - self.form_win.reply() - self._on_send(self._form, **self._kwargs) - return True - - def on_input(self, key, raw=False): - if key in self.key_func: - res = self.key_func[key]() - if res: - return res - self.help_win_dyn.refresh(self.form_win.get_help_message()) - self.form_win.refresh_current_input() - else: - self.form_win.on_input(key) - - def resize(self): - self.need_resize = False - self.topic_win.resize(1, self.width, 0, 0) - self.form_win.resize(self.height - 3 - Tab.tab_win_height(), - self.width, 1, 0) - self.help_win.resize(1, self.width, self.height - 1, 0) - self.help_win_dyn.resize(1, self.width, - self.height - 2 - Tab.tab_win_height(), 0) - self.lines = [] - - def refresh(self): - if self.need_resize: - self.resize() - self.topic_win.refresh(self._form['title']) - self.refresh_tab_win() - self.help_win.refresh() - self.help_win_dyn.refresh(self.form_win.get_help_message()) - self.form_win.refresh() - diff --git a/src/tabs/listtab.py b/src/tabs/listtab.py deleted file mode 100644 index 4d8bab9c..00000000 --- a/src/tabs/listtab.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -A generic tab that displays a serie of items in a scrollable, searchable, -sortable list. It should be inherited, to actually provide methods that -insert items in the list, and that lets the user interact with them. -""" - -import logging -log = logging.getLogger(__name__) - -import curses -import collections - -import windows -from common import safeJID -from decorators import refresh_wrapper - -from . import Tab - - -class ListTab(Tab): - plugin_commands = {} - plugin_keys = {} - - def __init__(self, name, help_message, header_text, cols): - """Parameters: - name: The name of the tab - help_message: The default help message displayed instead of the - input - header_text: The text displayed on the header line, at the top of - the tab - cols: a tuple of 2-tuples. e.g. (('column1_name', number), - ('column2_name', number)) - """ - Tab.__init__(self) - self.state = 'normal' - self.name = name - columns = collections.OrderedDict() - for col, num in cols: - columns[col] = num - self.list_header = windows.ColumnHeaderWin(list(columns)) - self.listview = windows.ListWin(columns) - self.info_header = windows.MucListInfoWin(header_text) - self.default_help_message = windows.HelpText(help_message) - self.input = self.default_help_message - self.key_func["KEY_DOWN"] = self.move_cursor_down - self.key_func["KEY_UP"] = self.move_cursor_up - self.key_func['^I'] = self.completion - self.key_func["/"] = self.on_slash - self.key_func['KEY_LEFT'] = self.list_header.sel_column_left - self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right - self.key_func[' '] = self.sort_by - self.register_command('close', self.close, - shortdesc='Close this tab.') - self.resize() - self.update_keys() - self.update_commands() - - def get_columns_sizes(self): - """ - Must be implemented in subclasses. Must return a dict like this: - {'column1_name': size1, - 'column2_name': size2} - Where the size are calculated based on the size of the tab etc - """ - raise NotImplementedError - - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - if self.size.tab_degrade_y: - display_info_win = False - else: - display_info_win = True - - self.info_header.refresh(window=self.listview) - if display_info_win: - self.info_win.refresh() - self.refresh_tab_win() - self.list_header.refresh() - self.listview.refresh() - self.input.refresh() - - def resize(self): - if self.size.tab_degrade_y: - info_win_height = 0 - tab_win_height = 0 - else: - info_win_height = self.core.information_win_size - tab_win_height = Tab.tab_win_height() - - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - column_size = self.get_columns_sizes() - self.list_header.resize_columns(column_size) - self.list_header.resize(1, self.width, 0, 0) - self.listview.resize_columns(column_size) - self.listview.resize(self.height - 3 - info_win_height - tab_win_height, - self.width, 1, 0) - self.input.resize(1, self.width, self.height-1, 0) - - def on_slash(self): - """ - '/' is pressed, activate the input - """ - curses.curs_set(1) - self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) - self.input.resize(1, self.width, self.height-1, 0) - self.input.do_command("/") # we add the slash - - def close(self, arg=None): - self.input.on_delete() - self.core.close_tab(self) - - def set_error(self, msg, code, body): - """ - If there's an error (retrieving the values etc) - """ - self._error_message = 'Error: %(code)s - %(msg)s: %(body)s' % {'msg':msg, 'body':body, 'code':code} - self.info_header.message = self._error_message - self.info_header.refresh() - curses.doupdate() - - def sort_by(self): - if self.list_header.get_order(): - self.listview.sort_by_column( - col_name=self.list_header.get_sel_column(), - asc=False) - self.list_header.set_order(False) - self.list_header.refresh() - else: - self.listview.sort_by_column( - col_name=self.list_header.get_sel_column(), - asc=True) - self.list_header.set_order(True) - self.list_header.refresh() - self.core.doupdate() - - @refresh_wrapper.always - def reset_help_message(self, _=None): - if self.closed: - return True - curses.curs_set(0) - self.input = self.default_help_message - self.input.resize(1, self.width, self.height-1, 0) - return True - - def execute_slash_command(self, txt): - if txt.startswith('/'): - self.input.key_enter() - self.execute_command(txt) - return self.reset_help_message() - - def completion(self): - if isinstance(self.input, windows.Input): - self.complete_commands(self.input) - - def on_input(self, key, raw): - res = self.input.do_command(key, raw=raw) - if res and not isinstance(self.input, windows.Input): - return True - elif res: - return False - if not raw and key in self.key_func: - return self.key_func[key]() - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: - return - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0) - - def on_lose_focus(self): - self.state = 'normal' - - def on_gain_focus(self): - self.state = 'current' - curses.curs_set(0) - - def on_scroll_up(self): - return self.listview.scroll_up() - - def on_scroll_down(self): - return self.listview.scroll_down() - - def move_cursor_up(self): - self.listview.move_cursor_up() - self.listview.refresh() - self.core.doupdate() - - def move_cursor_down(self): - self.listview.move_cursor_down() - self.listview.refresh() - self.core.doupdate() - - def matching_names(self): - return [(2, self.name)] - - diff --git a/src/tabs/muclisttab.py b/src/tabs/muclisttab.py deleted file mode 100644 index 92d55190..00000000 --- a/src/tabs/muclisttab.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -A MucListTab is a tab listing the rooms on a conference server. - -It has no functionnality except scrolling the list, and allowing the -user to join the rooms. -""" -import logging -log = logging.getLogger(__name__) - -from . import ListTab - -from slixmpp.plugins.xep_0030.stanza.items import DiscoItem - -class MucListTab(ListTab): - """ - A tab listing rooms from a specific server, displaying various information, - scrollable, and letting the user join them, etc - """ - plugin_commands = {} - plugin_keys = {} - - def __init__(self, server): - ListTab.__init__(self, server, - "“j”: join room.", - 'Chatroom list on server %s (Loading)' % server, - (('node-part', 0), ('name', 2), ('users', 3))) - self.key_func['j'] = self.join_selected - self.key_func['J'] = self.join_selected_no_focus - self.key_func['^M'] = self.join_selected - - def get_columns_sizes(self): - return {'node-part': int(self.width* 2 / 8), - 'name': int(self.width * 5 / 8), - 'users': self.width - int(self.width * 2 / 8) - - int(self.width * 5 / 8)} - - def join_selected_no_focus(self): - return - - def on_muc_list_item_received(self, iq): - """ - Callback called when a disco#items result is received - Used with command_list - """ - if iq['type'] == 'error': - self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text']) - return - def get_items(): - substanza = iq['disco_items'] - for item in substanza['substanzas']: - if isinstance(item, DiscoItem): - yield (item['jid'], item['node'], item['name']) - items = [(item[0].split('@')[0], - item[0], - item[2] or '', '') for item in get_items()] - self.listview.set_lines(items) - self.info_header.message = 'Chatroom list on server %s' % self.name - if self.core.current_tab() is self: - self.refresh() - else: - self.state = 'highlight' - self.refresh_tab_win() - self.core.doupdate() - - def join_selected(self): - row = self.listview.get_selected_row() - if not row: - return - self.core.command_join(row[1]) - diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py deleted file mode 100644 index 1f3ec6d8..00000000 --- a/src/tabs/muctab.py +++ /dev/null @@ -1,1720 +0,0 @@ -""" -Module for the MucTab - -A MucTab is a tab for multi-user chats as defined in XEP-0045. - -It keeps track of many things such as part/joins, maintains an -user list, and updates private tabs when necessary. -""" - -import logging -log = logging.getLogger(__name__) - -import bisect -import curses -import os -import random -import re -from datetime import datetime - -from . import ChatTab, Tab - -import common -import fixes -import multiuserchat as muc -import timed_events -import windows -import xhtml -from common import safeJID -from config import config -from decorators import refresh_wrapper, command_args_parser -from logger import logger -from roster import roster -from theming import get_theme, dump_tuple -from user import User - - -SHOW_NAME = { - 'dnd': 'busy', - 'away': 'away', - 'xa': 'not available', - 'chat': 'chatty', - '': 'available' - } - -NS_MUC_USER = 'http://jabber.org/protocol/muc#user' - - -class MucTab(ChatTab): - """ - The tab containing a multi-user-chat room. - It contains an userlist, an input, a topic, an information and a chat zone - """ - message_type = 'groupchat' - plugin_commands = {} - plugin_keys = {} - def __init__(self, jid, nick, password=None): - self.joined = False - ChatTab.__init__(self, jid) - if self.joined == False: - self._state = 'disconnected' - self.own_nick = nick - self.name = jid - self.password = password - self.users = [] - self.privates = [] # private conversations - self.topic = '' - self.topic_from = '' - self.remote_wants_chatstates = True - # Self ping event, so we can cancel it when we leave the room - self.self_ping_event = None - # We send active, composing and paused states to the MUC because - # the chatstate may or may not be filtered by the MUC, - # that’s not our problem. - self.topic_win = windows.Topic() - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) - self.v_separator = windows.VerticalSeparator() - self.user_win = windows.UserList() - self.info_header = windows.MucInfoWin() - self.input = windows.MessageInput() - self.ignores = [] # set of Users - # keys - self.key_func['^I'] = self.completion - self.key_func['M-u'] = self.scroll_user_list_down - self.key_func['M-y'] = self.scroll_user_list_up - self.key_func['M-n'] = self.go_to_next_hl - self.key_func['M-p'] = self.go_to_prev_hl - # commands - self.register_command('ignore', self.command_ignore, - usage='<nickname>', - desc='Ignore a specified nickname.', - shortdesc='Ignore someone', - completion=self.completion_ignore) - self.register_command('unignore', self.command_unignore, - usage='<nickname>', - desc='Remove the specified nickname from the ignore list.', - shortdesc='Unignore someone.', - completion=self.completion_unignore) - self.register_command('kick', self.command_kick, - usage='<nick> [reason]', - desc='Kick the user with the specified nickname.' - ' You also can give an optional reason.', - shortdesc='Kick someone.', - completion=self.completion_quoted) - self.register_command('ban', self.command_ban, - usage='<nick> [reason]', - desc='Ban the user with the specified nickname.' - ' You also can give an optional reason.', - shortdesc='Ban someone', - completion=self.completion_quoted) - self.register_command('role', self.command_role, - usage='<nick> <role> [reason]', - desc='Set the role of an user. Roles can be:' - ' none, visitor, participant, moderator.' - ' You also can give an optional reason.', - shortdesc='Set the role of an user.', - completion=self.completion_role) - self.register_command('affiliation', self.command_affiliation, - usage='<nick or jid> <affiliation>', - desc='Set the affiliation of an user. Affiliations can be:' - ' outcast, none, member, admin, owner.', - shortdesc='Set the affiliation of an user.', - completion=self.completion_affiliation) - self.register_command('topic', self.command_topic, - usage='<subject>', - desc='Change the subject of the room.', - shortdesc='Change the subject.', - completion=self.completion_topic) - self.register_command('query', self.command_query, - usage='<nick> [message]', - desc='Open a private conversation with <nick>. This nick' - ' has to be present in the room you\'re currently in.' - ' If you specified a message after the nickname, it ' - 'will immediately be sent to this user.', - shortdesc='Query an user.', - completion=self.completion_quoted) - self.register_command('part', self.command_part, - usage='[message]', - desc='Disconnect from a room. You can' - ' specify an optional message.', - shortdesc='Leave the room.') - self.register_command('close', self.command_close, - usage='[message]', - desc='Disconnect from a room and close the tab.' - ' You can specify an optional message if ' - 'you are still connected.', - shortdesc='Close the tab.') - self.register_command('nick', self.command_nick, - usage='<nickname>', - desc='Change your nickname in the current room.', - shortdesc='Change your nickname.', - completion=self.completion_nick) - self.register_command('recolor', self.command_recolor, - usage='[random]', - desc='Re-assign a color to all participants of the' - ' current room, based on the last time they talked.' - ' Use this if the participants currently talking ' - 'have too many identical colors. Use /recolor random' - ' for a non-deterministic result.', - shortdesc='Change the nicks colors.', - completion=self.completion_recolor) - self.register_command('color', self.command_color, - usage='<nick> <color>', - desc='Fix a color for a nick. Use "unset" instead of a color' - ' to remove the attribution', - shortdesc='Fix a color for a nick.', - completion=self.completion_color) - self.register_command('cycle', self.command_cycle, - usage='[message]', - desc='Leave the current room and rejoin it immediately.', - shortdesc='Leave and re-join the room.') - self.register_command('info', self.command_info, - usage='<nickname>', - desc='Display some information about the user ' - 'in the MUC: its/his/her role, affiliation,' - ' status and status message.', - shortdesc='Show an user\'s infos.', - completion=self.completion_info) - self.register_command('configure', self.command_configure, - desc='Configure the current room, through a form.', - shortdesc='Configure the room.') - self.register_command('version', self.command_version, - usage='<jid or nick>', - desc='Get the software version of the given JID' - ' or nick in room (usually its XMPP client' - ' and Operating System).', - shortdesc='Get the software version of a jid.', - completion=self.completion_version) - self.register_command('names', self.command_names, - desc='Get the users in the room with their roles.', - shortdesc='List the users.') - self.register_command('invite', self.command_invite, - desc='Invite a contact to this room', - usage='<jid> [reason]', - shortdesc='Invite a contact to this room', - completion=self.completion_invite) - - if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks - del self.commands["nick"] - - self.resize() - self.update_commands() - self.update_keys() - - @property - def general_jid(self): - return self.name - - @property - def is_muc(self): - return True - - @property - def last_connection(self): - last_message = self._text_buffer.last_message - if last_message: - return last_message.time - return None - - @refresh_wrapper.always - def go_to_next_hl(self): - """ - Go to the next HL in the room, or the last - """ - self.text_win.next_highlight() - - @refresh_wrapper.always - def go_to_prev_hl(self): - """ - Go to the previous HL in the room, or the first - """ - self.text_win.previous_highlight() - - def completion_version(self, the_input): - """Completion for /version""" - compare_users = lambda x: x.last_talked - userlist = [] - for user in sorted(self.users, key=compare_users, reverse=True): - if user.nick != self.own_nick: - userlist.append(user.nick) - comp = [] - for jid in (jid for jid in roster.jids() if len(roster[jid])): - for resource in roster[jid].resources: - comp.append(resource.jid) - comp.sort() - userlist.extend(comp) - - return the_input.auto_completion(userlist, quotify=False) - - def completion_info(self, the_input): - """Completion for /info""" - compare_users = lambda x: x.last_talked - userlist = [] - for user in sorted(self.users, key=compare_users, reverse=True): - userlist.append(user.nick) - return the_input.auto_completion(userlist, quotify=False) - - def completion_nick(self, the_input): - """Completion for /nick""" - nicks = [os.environ.get('USER'), - config.get('default_nick'), - self.core.get_bookmark_nickname(self.name)] - nicks = [i for i in nicks if i] - return the_input.auto_completion(nicks, '', quotify=False) - - def completion_recolor(self, the_input): - if the_input.get_argument_position() == 1: - return the_input.new_completion(['random'], 1, '', quotify=False) - return True - - def completion_color(self, the_input): - """Completion for /color""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - userlist = [user.nick for user in self.users] - if self.own_nick in userlist: - userlist.remove(self.own_nick) - return the_input.new_completion(userlist, 1, '', quotify=True) - elif n == 2: - colors = [i for i in xhtml.colors if i] - colors.sort() - colors.append('unset') - colors.append('random') - return the_input.new_completion(colors, 2, '', quotify=False) - - def completion_ignore(self, the_input): - """Completion for /ignore""" - userlist = [user.nick for user in self.users] - if self.own_nick in userlist: - userlist.remove(self.own_nick) - userlist.sort() - return the_input.auto_completion(userlist, quotify=False) - - def completion_role(self, the_input): - """Completion for /role""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - userlist = [user.nick for user in self.users] - if self.own_nick in userlist: - userlist.remove(self.own_nick) - return the_input.new_completion(userlist, 1, '', quotify=True) - elif n == 2: - possible_roles = ['none', 'visitor', 'participant', 'moderator'] - return the_input.new_completion(possible_roles, 2, '', - quotify=True) - - def completion_affiliation(self, the_input): - """Completion for /affiliation""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - userlist = [user.nick for user in self.users] - if self.own_nick in userlist: - userlist.remove(self.own_nick) - jidlist = [user.jid.bare for user in self.users] - if self.core.xmpp.boundjid.bare in jidlist: - jidlist.remove(self.core.xmpp.boundjid.bare) - userlist.extend(jidlist) - return the_input.new_completion(userlist, 1, '', quotify=True) - elif n == 2: - possible_affiliations = ['none', 'member', 'admin', - 'owner', 'outcast'] - return the_input.new_completion(possible_affiliations, 2, '', - quotify=True) - - @command_args_parser.quoted(1, 1, ['']) - def command_invite(self, args): - """/invite <jid> [reason]""" - if args is None: - return self.core.command_help('invite') - jid, reason = args - self.core.command_invite('%s %s "%s"' % (jid, self.name, reason)) - - def completion_invite(self, the_input): - """Completion for /invite""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - return the_input.new_completion(roster.jids(), 1, quotify=True) - - def scroll_user_list_up(self): - self.user_win.scroll_up() - self.user_win.refresh(self.users) - self.input.refresh() - - def scroll_user_list_down(self): - self.user_win.scroll_down() - self.user_win.refresh(self.users) - self.input.refresh() - - @command_args_parser.quoted(1) - def command_info(self, args): - """ - /info <nick> - """ - if args is None: - return self.core.command_help('info') - nick = args[0] - user = self.get_user_by_name(nick) - if not user: - return self.core.information("Unknown user: %s" % nick, "Error") - theme = get_theme() - inf = '\x19' + dump_tuple(theme.COLOR_INFORMATION_TEXT) + '}' - if user.jid: - user_jid = '%s (\x19%s}%s\x19o%s)' % ( - inf, - dump_tuple(theme.COLOR_MUC_JID), - user.jid, - inf) - else: - user_jid = '' - info = ('\x19%s}%s\x19o%s%s: show: \x19%s}%s\x19o%s, affiliation:' - ' \x19%s}%s\x19o%s, role: \x19%s}%s\x19o%s') % ( - dump_tuple(user.color), - nick, - user_jid, - inf, - dump_tuple(theme.color_show(user.show)), - user.show or 'Available', - inf, - dump_tuple(theme.color_role(user.role)), - user.affiliation or 'None', - inf, - dump_tuple(theme.color_role(user.role)), - user.role or 'None', - '\n%s' % user.status if user.status else '') - self.add_message(info, typ=0) - self.core.refresh_window() - - @command_args_parser.quoted(0) - def command_configure(self, ignored): - """ - /configure - """ - def on_form_received(form): - if not form: - self.core.information( - 'Could not retrieve the configuration form', - 'Error') - return - self.core.open_new_form(form, self.cancel_config, self.send_config) - - fixes.get_room_form(self.core.xmpp, self.name, on_form_received) - - def cancel_config(self, form): - """ - The user do not want to send his/her config, send an iq cancel - """ - muc.cancel_config(self.core.xmpp, self.name) - self.core.close_tab() - - def send_config(self, form): - """ - The user sends his/her config to the server - """ - muc.configure_room(self.core.xmpp, self.name, form) - self.core.close_tab() - - @command_args_parser.raw - def command_cycle(self, msg): - """/cycle [reason]""" - self.command_part(msg) - self.disconnect() - self.user_win.pos = 0 - self.core.disable_private_tabs(self.name) - self.join() - - def join(self): - """ - Join the room - """ - status = self.core.get_status() - if self.last_connection: - delta = datetime.now() - self.last_connection - seconds = delta.seconds + delta.days * 24 * 3600 - else: - seconds = 0 - muc.join_groupchat(self.core, self.name, self.own_nick, - self.password, - status=status.message, - show=status.show, - seconds=seconds) - - @command_args_parser.quoted(0, 1, ['']) - def command_recolor(self, args): - """ - /recolor [random] - Re-assign color to the participants of the room - """ - deterministic = config.get_by_tabname('deterministic_nick_colors', self.name) - if deterministic: - for user in self.users: - if user.nick == self.own_nick: - continue - color = self.search_for_color(user.nick) - if color != '': - continue - user.set_deterministic_color() - if args[0] == 'random': - self.core.information('"random" was provided, but poezio is ' - 'configured to use deterministic colors', - 'Warning') - self.user_win.refresh(self.users) - self.input.refresh() - return - compare_users = lambda x: x.last_talked - users = list(self.users) - sorted_users = sorted(users, key=compare_users, reverse=True) - full_sorted_users = sorted_users[:] - # search our own user, to remove it from the list - # Also remove users whose color is fixed - for user in full_sorted_users: - color = self.search_for_color(user.nick) - if user.nick == self.own_nick: - sorted_users.remove(user) - user.color = get_theme().COLOR_OWN_NICK - elif color != '': - sorted_users.remove(user) - user.change_color(color, deterministic) - colors = list(get_theme().LIST_COLOR_NICKNAMES) - if args[0] == 'random': - random.shuffle(colors) - for i, user in enumerate(sorted_users): - user.color = colors[i % len(colors)] - self.text_win.rebuild_everything(self._text_buffer) - self.user_win.refresh(self.users) - self.text_win.refresh() - self.input.refresh() - - @command_args_parser.quoted(2, 2, ['']) - def command_color(self, args): - """ - /color <nick> <color> - Fix a color for a nick. - Use "unset" instead of a color to remove the attribution. - User "random" to attribute a random color. - """ - if args is None: - return self.core.command_help('color') - nick = args[0] - color = args[1].lower() - user = self.get_user_by_name(nick) - if not color in xhtml.colors and color not in ('unset', 'random'): - return self.core.information("Unknown color: %s" % color, 'Error') - if user and user.nick == self.own_nick: - return self.core.information("You cannot change the color of your" - " own nick.", 'Error') - if color == 'unset': - if config.remove_and_save(nick, 'muc_colors'): - self.core.information('Color for nick %s unset' % (nick)) - else: - if color == 'random': - color = random.choice(list(xhtml.colors)) - if user: - user.change_color(color) - config.set_and_save(nick, color, 'muc_colors') - nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name) - if nick_color_aliases: - # if any user in the room has a nick which is an alias of the - # nick, update its color - for tab in self.core.get_tabs(MucTab): - for u in tab.users: - nick_alias = re.sub('^_*', '', u.nick) - nick_alias = re.sub('_*$', '', nick_alias) - if nick_alias == nick: - u.change_color(color) - self.text_win.rebuild_everything(self._text_buffer) - self.user_win.refresh(self.users) - self.text_win.refresh() - self.input.refresh() - - @command_args_parser.quoted(1) - def command_version(self, args): - """ - /version <jid or nick> - """ - def callback(res): - if not res: - return self.core.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.core.information(version, 'Info') - if args is None: - return self.core.command_help('version') - nick = args[0] - if nick in [user.nick for user in self.users]: - jid = safeJID(self.name).bare - jid = safeJID(jid + '/' + nick) - else: - jid = safeJID(nick) - fixes.get_version(self.core.xmpp, jid, - callback=callback) - - @command_args_parser.quoted(1) - def command_nick(self, args): - """ - /nick <nickname> - """ - if args is None: - return self.core.command_help('nick') - nick = args[0] - if not self.joined: - return self.core.information('/nick only works in joined rooms', - 'Info') - current_status = self.core.get_status() - if not safeJID(self.name + '/' + nick): - return self.core.information('Invalid nick', 'Info') - muc.change_nick(self.core, self.name, nick, - current_status.message, - current_status.show) - - @command_args_parser.quoted(0, 1, ['']) - def command_part(self, args): - """ - /part [msg] - """ - arg = args[0] - msg = None - if self.joined: - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_quit = get_theme().CHAR_QUIT - spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR) - - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(get_theme().COLOR_OWN_NICK) - else: - color = 3 - - if arg: - msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} ' - 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})' - ' left the chatroom' - ' (\x19o%(reason)s\x19%(info_col)s})') % { - 'info_col': info_col, 'reason': arg, - 'spec': char_quit, 'color': color, - 'color_spec': spec_col, - 'nick': self.own_nick, - } - else: - msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} ' - 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})' - ' left the chatroom') % { - 'info_col': info_col, - 'spec': char_quit, 'color': color, - 'color_spec': spec_col, - 'nick': self.own_nick, - } - - self.add_message(msg, typ=2) - self.disconnect() - muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg) - self.core.disable_private_tabs(self.name, reason=msg) - if self == self.core.current_tab(): - self.refresh() - self.core.doupdate() - - @command_args_parser.raw - def command_close(self, msg): - """ - /close [msg] - """ - self.command_part(msg) - self.core.close_tab() - - @command_args_parser.quoted(1, 1) - def command_query(self, args): - """ - /query <nick> [message] - """ - if args is None: - return self.core.command_help('query') - nick = args[0] - r = None - for user in self.users: - if user.nick == nick: - r = self.core.open_private_window(self.name, user.nick) - if r and len(args) == 2: - msg = args[1] - self.core.current_tab().command_say( - xhtml.convert_simple_to_full_colors(msg)) - if not r: - self.core.information("Cannot find user: %s" % nick, 'Error') - - @command_args_parser.raw - def command_topic(self, subject): - """ - /topic [new topic] - """ - if not subject: - self._text_buffer.add_message( - "\x19%s}The subject of the room is: %s %s" % - (dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - self.topic, - '(set by %s)' % self.topic_from if self.topic_from - else '')) - self.refresh() - return - - muc.change_subject(self.core.xmpp, self.name, subject) - - @command_args_parser.quoted(0) - def command_names(self, args): - """ - /names - """ - if not self.joined: - return - - aff = { - 'owner': get_theme().CHAR_AFFILIATION_OWNER, - 'admin': get_theme().CHAR_AFFILIATION_ADMIN, - 'member': get_theme().CHAR_AFFILIATION_MEMBER, - 'none': get_theme().CHAR_AFFILIATION_NONE, - } - - colors = {} - colors["visitor"] = dump_tuple(get_theme().COLOR_USER_VISITOR) - colors["moderator"] = dump_tuple(get_theme().COLOR_USER_MODERATOR) - colors["participant"] = dump_tuple(get_theme().COLOR_USER_PARTICIPANT) - color_other = dump_tuple(get_theme().COLOR_USER_NONE) - - buff = ['Users: %s \n' % len(self.users)] - for user in self.users: - affiliation = aff.get(user.affiliation, - get_theme().CHAR_AFFILIATION_NONE) - color = colors.get(user.role, color_other) - buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % ( - color, affiliation, dump_tuple(user.color), user.nick)) - - buff.append('\n') - message = ' '.join(buff) - - self._text_buffer.add_message(message) - self.text_win.refresh() - self.input.refresh() - - def completion_topic(self, the_input): - if the_input.get_argument_position() == 1: - return the_input.auto_completion([self.topic], '', quotify=False) - - def completion_quoted(self, the_input): - """Nick completion, but with quotes""" - if the_input.get_argument_position(quoted=True) == 1: - compare_users = lambda x: x.last_talked - word_list = [] - for user in sorted(self.users, key=compare_users, reverse=True): - if user.nick != self.own_nick: - word_list.append(user.nick) - - return the_input.new_completion(word_list, 1, quotify=True) - - @command_args_parser.quoted(1, 1) - def command_kick(self, args): - """ - /kick <nick> [reason] - """ - if args is None: - return self.core.command_help('kick') - if len(args) == 2: - msg = ' "%s"' % args[1] - else: - msg = '' - self.command_role('"'+args[0]+ '" none'+msg) - - @command_args_parser.quoted(1, 1) - def command_ban(self, args): - """ - /ban <nick> [reason] - """ - def callback(iq): - if iq['type'] == 'error': - self.core.room_error(iq, self.name) - if args is None: - return self.core.command_help('ban') - if len(args) > 1: - msg = args[1] - else: - msg = '' - nick = args[0] - - if nick in [user.nick for user in self.users]: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - 'outcast', nick=nick, - callback=callback, reason=msg) - else: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - 'outcast', jid=safeJID(nick), - callback=callback, reason=msg) - if not res: - self.core.information('Could not ban user', 'Error') - - @command_args_parser.quoted(2, 1, ['']) - def command_role(self, args): - """ - /role <nick> <role> [reason] - Changes the role of an user - roles can be: none, visitor, participant, moderator - """ - def callback(iq): - if iq['type'] == 'error': - self.core.room_error(iq, self.name) - - if args is None: - return self.core.command_help('role') - - nick, role, reason = args[0], args[1].lower(), args[2] - - valid_roles = ('none', 'visitor', 'participant', 'moderator') - - if not self.joined or role not in valid_roles: - return self.core.information('The role must be one of ' + ', '.join(valid_roles), - 'Error') - - if not safeJID(self.name + '/' + nick): - return self.core.information('Invalid nick', 'Info') - muc.set_user_role(self.core.xmpp, self.name, nick, reason, role, - callback=callback) - - @command_args_parser.quoted(2) - def command_affiliation(self, args): - """ - /affiliation <nick> <role> - Changes the affiliation of an user - affiliations can be: outcast, none, member, admin, owner - """ - def callback(iq): - if iq['type'] == 'error': - self.core.room_error(iq, self.name) - - if args is None: - return self.core.command_help('affiliation') - - nick, affiliation = args[0], args[1].lower() - - if not self.joined: - return - - valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner') - if affiliation not in valid_affiliations: - return self.core.information('The affiliation must be one of ' + ', '.join(valid_affiliations), - 'Error') - - if nick in [user.nick for user in self.users]: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - affiliation, nick=nick, - callback=callback) - else: - res = muc.set_user_affiliation(self.core.xmpp, self.name, - affiliation, jid=safeJID(nick), - callback=callback) - if not res: - self.core.information('Could not set affiliation', 'Error') - - @command_args_parser.raw - def command_say(self, line, correct=False): - """ - /say <message> - Or normal input + enter - """ - needed = 'inactive' if self.inactive else 'active' - msg = self.core.xmpp.make_message(self.name) - msg['type'] = 'groupchat' - msg['body'] = line - # trigger the event BEFORE looking for colors. - # This lets a plugin insert \x19xxx} colors, that will - # be converted in xhtml. - self.core.events.trigger('muc_say', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - if msg['body'].find('\x19') != -1: - msg.enable('html') - msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) - msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', self.general_jid) - and self.remote_wants_chatstates is not False): - msg['chat_state'] = needed - if correct: - msg['replace']['id'] = self.last_sent_message['id'] - self.cancel_paused_delay() - self.core.events.trigger('muc_say_after', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - self.last_sent_message = msg - msg.send() - self.chat_state = needed - - @command_args_parser.raw - def command_xhtml(self, msg): - message = self.generate_xhtml_message(msg) - if message: - message['type'] = 'groupchat' - message.send() - - @command_args_parser.quoted(1) - def command_ignore(self, args): - """ - /ignore <nick> - """ - if args is None: - return self.core.command_help('ignore') - - nick = args[0] - user = self.get_user_by_name(nick) - if not user: - self.core.information('%s is not in the room' % nick) - elif user in self.ignores: - self.core.information('%s is already ignored' % nick) - else: - self.ignores.append(user) - self.core.information("%s is now ignored" % nick, 'info') - - @command_args_parser.quoted(1) - def command_unignore(self, args): - """ - /unignore <nick> - """ - if args is None: - return self.core.command_help('unignore') - - nick = args[0] - user = self.get_user_by_name(nick) - if not user: - self.core.information('%s is not in the room' % nick) - elif user not in self.ignores: - self.core.information('%s is not ignored' % nick) - else: - self.ignores.remove(user) - self.core.information('%s is now unignored' % nick) - - def completion_unignore(self, the_input): - if the_input.get_argument_position() == 1: - users = [user.nick for user in self.ignores] - return the_input.auto_completion(users, quotify=False) - - def resize(self): - """ - Resize the whole window. i.e. all its sub-windows - """ - self.need_resize = False - if config.get('hide_user_list') or self.size.tab_degrade_x: - display_user_list = False - text_width = self.width - else: - display_user_list = True - text_width = (self.width // 10) * 9 - - if self.size.tab_degrade_y: - display_info_win = False - tab_win_height = 0 - info_win_height = 0 - else: - display_info_win = True - tab_win_height = Tab.tab_win_height() - info_win_height = self.core.information_win_size - - - self.user_win.resize(self.height - 3 - info_win_height - - tab_win_height, - self.width - (self.width // 10) * 9 - 1, - 1, - (self.width // 10) * 9 + 1) - self.v_separator.resize(self.height - 3 - info_win_height - tab_win_height, - 1, 1, 9 * (self.width // 10)) - - self.topic_win.resize(1, self.width, 0, 0) - - self.text_win.resize(self.height - 3 - info_win_height - - tab_win_height, - text_width, 1, 0) - self.text_win.rebuild_everything(self._text_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - if config.get('hide_user_list') or self.size.tab_degrade_x: - display_user_list = False - else: - display_user_list = True - display_info_win = not self.size.tab_degrade_y - - self.topic_win.refresh(self.get_single_line_topic()) - self.text_win.refresh() - if display_user_list: - self.v_separator.refresh() - self.user_win.refresh(self.users) - self.info_header.refresh(self, self.text_win) - self.refresh_tab_win() - if display_info_win: - self.info_win.refresh() - self.input.refresh() - - def on_input(self, key, raw): - if not raw and key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key, raw=raw) - empty_after = self.input.get_text() == '' - empty_after = empty_after or (self.input.get_text().startswith('/') - and not - self.input.get_text().startswith('//')) - self.send_composing_chat_state(empty_after) - return False - - def completion(self): - """ - Called when Tab is pressed, complete the nickname in the input - """ - if self.complete_commands(self.input): - return - - # If we are not completing a command or a command argument, - # complete a nick - compare_users = lambda x: x.last_talked - word_list = [] - for user in sorted(self.users, key=compare_users, reverse=True): - if user.nick != self.own_nick: - word_list.append(user.nick) - after = config.get('after_completion') + ' ' - input_pos = self.input.pos - if ' ' not in self.input.get_text()[:input_pos] or ( - self.input.last_completion and - self.input.get_text()[:input_pos] == - self.input.last_completion + after): - add_after = after - else: - if not config.get('add_space_after_completion'): - add_after = '' - else: - add_after = ' ' - self.input.auto_completion(word_list, add_after, quotify=False) - empty_after = self.input.get_text() == '' - empty_after = empty_after or (self.input.get_text().startswith('/') - and not - self.input.get_text().startswith('//')) - self.send_composing_chat_state(empty_after) - - def get_nick(self): - if not config.get('show_muc_jid'): - return safeJID(self.name).user - return self.name - - def get_text_window(self): - return self.text_win - - def on_lose_focus(self): - if self.joined: - if self.input.text: - self.state = 'nonempty' - else: - self.state = 'normal' - else: - self.state = 'disconnected' - self.text_win.remove_line_separator() - self.text_win.add_line_separator(self._text_buffer) - if (config.get_by_tabname('send_chat_states', self.general_jid) and - not self.input.get_text()): - self.send_chat_state('inactive') - self.check_scrolled() - - def on_gain_focus(self): - self.state = 'current' - if (self.text_win.built_lines and self.text_win.built_lines[-1] is None - and not config.get('show_useless_separator')): - self.text_win.remove_line_separator() - curses.curs_set(1) - if self.joined and config.get_by_tabname('send_chat_states', - self.general_jid) and not self.input.get_text(): - self.send_chat_state('active') - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: - return - if config.get("hide_user_list"): - text_width = self.width - else: - text_width = (self.width//10)*9 - self.user_win.resize(self.height - 3 - self.core.information_win_size - - Tab.tab_win_height(), - self.width - (self.width // 10) * 9 - 1, - 1, - (self.width // 10) * 9 + 1) - self.v_separator.resize(self.height - 3 - self.core.information_win_size - Tab.tab_win_height(), - 1, 1, 9 * (self.width // 10)) - self.text_win.resize(self.height - 3 - self.core.information_win_size - - Tab.tab_win_height(), - text_width, 1, 0) - self.info_header.resize(1, self.width, - self.height-2-self.core.information_win_size - - Tab.tab_win_height(), - 0) - - def handle_presence(self, presence): - from_nick = presence['from'].resource - from_room = presence['from'].bare - xpath = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER) - status_codes = set() - for status_code in presence.findall(xpath): - status_codes.add(status_code.attrib['code']) - - # Check if it's not an error presence. - if presence['type'] == 'error': - return self.core.room_error(presence, from_room) - affiliation = presence['muc']['affiliation'] - show = presence['show'] - status = presence['status'] - role = presence['muc']['role'] - jid = presence['muc']['jid'] - typ = presence['type'] - deterministic = config.get_by_tabname('deterministic_nick_colors', self.name) - color = self.search_for_color(from_nick) - if not self.joined: # user in the room BEFORE us. - # ignore redondant presence message, see bug #1509 - if (from_nick not in [user.nick for user in self.users] - and typ != "unavailable"): - new_user = User(from_nick, affiliation, show, - status, role, jid, deterministic, color) - bisect.insort_left(self.users, new_user) - self.core.events.trigger('muc_join', presence, self) - if '110' in status_codes or self.own_nick == from_nick: - # second part of the condition is a workaround for old - # ejabberd or every gateway in the world that just do - # not send a 110 status code with the presence - self.own_nick = from_nick - self.joined = True - if self.name in self.core.initial_joins: - self.core.initial_joins.remove(self.name) - self._state = 'normal' - elif self != self.core.current_tab(): - self._state = 'joined' - if (self.core.current_tab() is self - and self.core.status.show not in ('xa', 'away')): - self.send_chat_state('active') - new_user.color = get_theme().COLOR_OWN_NICK - - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(new_user.color) - else: - color = 3 - - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - warn_col = dump_tuple(get_theme().COLOR_WARNING_TEXT) - spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR) - - self.add_message( - '\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You ' - '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined' - ' the chatroom' % - { - 'nick': from_nick, - 'spec': get_theme().CHAR_JOIN, - 'color_spec': spec_col, - 'nick_col': color, - 'info_col': info_col, - }, - typ=2) - if '201' in status_codes: - self.add_message( - '\x19%(info_col)s}Info: The room ' - 'has been created' % - {'info_col': info_col}, - typ=0) - if '170' in status_codes: - self.add_message( - '\x19%(warn_col)s}Warning:\x19%(info_col)s}' - ' This room is publicly logged' % - {'info_col': info_col, - 'warn_col': warn_col}, - typ=0) - if '100' in status_codes: - self.add_message( - '\x19%(warn_col)s}Warning:\x19%(info_col)s}' - ' This room is not anonymous.' % - {'info_col': info_col, - 'warn_col': warn_col}, - typ=0) - if self.core.current_tab() is not self: - self.refresh_tab_win() - self.core.current_tab().input.refresh() - self.core.doupdate() - self.core.enable_private_tabs(self.name) - # Enable the self ping event, to regularly check if we - # are still in the room. - self.enable_self_ping_event() - else: - change_nick = '303' in status_codes - kick = '307' in status_codes and typ == 'unavailable' - ban = '301' in status_codes and typ == 'unavailable' - shutdown = '332' in status_codes and typ == 'unavailable' - non_member = '322' in status_codes and typ == 'unavailable' - user = self.get_user_by_name(from_nick) - # New user - if not user: - self.core.events.trigger('muc_join', presence, self) - self.on_user_join(from_nick, affiliation, show, status, role, - jid, color) - # nick change - elif change_nick: - self.core.events.trigger('muc_nickchange', presence, self) - self.on_user_nick_change(presence, user, from_nick, from_room) - elif ban: - self.core.events.trigger('muc_ban', presence, self) - self.core.on_user_left_private_conversation(from_room, - from_nick, status) - self.on_user_banned(presence, user, from_nick) - # kick - elif kick: - self.core.events.trigger('muc_kick', presence, self) - self.core.on_user_left_private_conversation(from_room, - from_nick, status) - self.on_user_kicked(presence, user, from_nick) - elif shutdown: - self.core.events.trigger('muc_shutdown', presence, self) - self.on_muc_shutdown() - elif non_member: - self.core.events.trigger('muc_shutdown', presence, self) - self.on_non_member_kicked() - # user quit - elif typ == 'unavailable': - self.on_user_leave_groupchat(user, jid, status, - from_nick, from_room) - # status change - else: - self.on_user_change_status(user, from_nick, from_room, - affiliation, role, show, status) - if self.core.current_tab() is self: - self.text_win.refresh() - self.user_win.refresh_if_changed(self.users) - self.info_header.refresh(self, self.text_win) - self.input.refresh() - self.core.doupdate() - - def on_non_member_kicked(self): - """We have been kicked because the MUC is members-only""" - self.add_message( - '\x19%(info_col)s}You have been kicked because you ' - 'are not a member and the room is now members-only.' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - self.disconnect() - - def on_muc_shutdown(self): - """We have been kicked because the MUC service is shutting down""" - self.add_message( - '\x19%(info_col)s}You have been kicked because the' - ' MUC service is shutting down.' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - typ=2) - self.disconnect() - - def on_user_join(self, from_nick, affiliation, show, status, role, jid, color): - """ - When a new user joins the groupchat - """ - deterministic = config.get_by_tabname('deterministic_nick_colors', self.name) - user = User(from_nick, affiliation, - show, status, role, jid, deterministic, color) - bisect.insort_left(self.users, user) - hide_exit_join = config.get_by_tabname('hide_exit_join', - self.general_jid) - if hide_exit_join != 0: - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR) - char_join = get_theme().CHAR_JOIN - if not jid.full: - msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s' - '\x19%(info_col)s} joined the chatroom') % { - 'nick': from_nick, 'spec': char_join, - 'color': color, - 'info_col': info_col, - 'color_spec': spec_col, - } - else: - msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s' - '\x19%(info_col)s} (\x19%(jid_color)s}%(jid)s\x19' - '%(info_col)s}) joined the chatroom') % { - 'spec': char_join, 'nick': from_nick, - 'color':color, 'jid':jid.full, - 'info_col': info_col, - 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID), - 'color_spec': spec_col, - } - self.add_message(msg, typ=2) - self.core.on_user_rejoined_private_conversation(self.name, from_nick) - - def on_user_nick_change(self, presence, user, from_nick, from_room): - new_nick = presence.find('{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER) - ).attrib['nick'] - if user.nick == self.own_nick: - self.own_nick = new_nick - # also change our nick in all private discussions of this room - self.core.on_muc_own_nickchange(self) - else: - color = config.get_by_tabname(new_nick, 'muc_colors') - if color != '': - deterministic = config.get_by_tabname('deterministic_nick_colors', - self.name) - user.change_color(color, deterministic) - user.change_nick(new_nick) - self.users.remove(user) - bisect.insort_left(self.users, user) - - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - self.add_message('\x19%(color)s}%(old)s\x19%(info_col)s} is' - ' now known as \x19%(color)s}%(new)s' % { - 'old':from_nick, 'new':new_nick, - 'color':color, 'info_col': info_col}, - typ=2) - # rename the private tabs if needed - self.core.rename_private_tabs(self.name, from_nick, new_nick) - - def on_user_banned(self, presence, user, from_nick): - """ - When someone is banned from a muc - """ - self.users.remove(user) - by = presence.find('{%s}x/{%s}item/{%s}actor' % - (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) - reason = presence.find('{%s}x/{%s}item/{%s}reason' % - (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) - by = by.attrib['jid'] if by is not None else None - - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_kick = get_theme().CHAR_KICK - - if from_nick == self.own_nick: # we are banned - if by: - kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}' - ' have been banned by \x194}%(by)s') % { - 'spec': char_kick, 'by': by, - 'info_col': info_col} - else: - kick_msg = ('\x191}%(spec)s \x193}You\x19' - '%(info_col)s} have been banned.') % { - 'spec': char_kick, 'info_col': info_col} - self.core.disable_private_tabs(self.name, reason=kick_msg) - self.disconnect() - self.refresh_tab_win() - self.core.current_tab().input.refresh() - self.core.doupdate() - if config.get_by_tabname('autorejoin', self.general_jid): - delay = config.get_by_tabname('autorejoin_delay', - self.general_jid) - delay = common.parse_str_to_secs(delay) - if delay <= 0: - muc.join_groupchat(self.core, self.name, self.own_nick) - else: - self.core.add_timed_event(timed_events.DelayedEvent( - delay, - muc.join_groupchat, - self.core, - self.name, - self.own_nick)) - - else: - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - - if by: - kick_msg = ('\x191}%(spec)s \x19%(color)s}' - '%(nick)s\x19%(info_col)s} ' - 'has been banned by \x194}%(by)s') % { - 'spec': char_kick, 'nick': from_nick, - 'color': color, 'by': by, - 'info_col': info_col} - else: - kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s' - '\x19%(info_col)s} has been banned') % { - 'spec': char_kick, 'nick': from_nick, - 'color': color, 'info_col': info_col} - if reason is not None and reason.text: - kick_msg += ('\x19%(info_col)s} Reason: \x196}' - '%(reason)s\x19%(info_col)s}') % { - 'reason': reason.text, 'info_col': info_col} - self.add_message(kick_msg, typ=2) - - def on_user_kicked(self, presence, user, from_nick): - """ - When someone is kicked from a muc - """ - self.users.remove(user) - actor_elem = presence.find('{%s}x/{%s}item/{%s}actor' % - (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) - reason = presence.find('{%s}x/{%s}item/{%s}reason' % - (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) - by = None - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - char_kick = get_theme().CHAR_KICK - if actor_elem is not None: - by = actor_elem.get('nick') or actor_elem.get('jid') - if from_nick == self.own_nick: # we are kicked - if by: - kick_msg = ('\x191}%(spec)s \x193}You\x19' - '%(info_col)s} have been kicked' - ' by \x193}%(by)s') % { - 'spec': char_kick, 'by': by, - 'info_col': info_col} - else: - kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}' - ' have been kicked.') % { - 'spec': char_kick, - 'info_col': info_col} - self.core.disable_private_tabs(self.name, reason=kick_msg) - self.disconnect() - self.refresh_tab_win() - self.core.current_tab().input.refresh() - self.core.doupdate() - # try to auto-rejoin - if config.get_by_tabname('autorejoin', self.general_jid): - delay = config.get_by_tabname('autorejoin_delay', - self.general_jid) - delay = common.parse_str_to_secs(delay) - if delay <= 0: - muc.join_groupchat(self.core, self.name, self.own_nick) - else: - self.core.add_timed_event(timed_events.DelayedEvent( - delay, - muc.join_groupchat, - self.core, - self.name, - self.own_nick)) - else: - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - if by: - kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s' - '\x19%(info_col)s} has been kicked by ' - '\x193}%(by)s') % { - 'spec': char_kick, 'nick':from_nick, - 'color':color, 'by':by, 'info_col': info_col} - else: - kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s' - '\x19%(info_col)s} has been kicked') % { - 'spec': char_kick, 'nick': from_nick, - 'color':color, 'info_col': info_col} - if reason is not None and reason.text: - kick_msg += ('\x19%(info_col)s} Reason: \x196}' - '%(reason)s') % { - 'reason': reason.text, 'info_col': info_col} - self.add_message(kick_msg, typ=2) - - def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room): - """ - When an user leaves a groupchat - """ - self.users.remove(user) - if self.own_nick == user.nick: - # We are now out of the room. - # Happens with some buggy (? not sure) servers - self.disconnect() - self.core.disable_private_tabs(from_room) - self.refresh_tab_win() - - hide_exit_join = config.get_by_tabname('hide_exit_join', - self.general_jid) - - if hide_exit_join <= -1 or user.has_talked_since(hide_exit_join): - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR) - - if not jid.full: - leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}' - '%(nick)s\x19%(info_col)s} has left the ' - 'chatroom') % { - 'nick':from_nick, 'color':color, - 'spec':get_theme().CHAR_QUIT, - 'info_col': info_col, - 'color_spec': spec_col} - else: - jid_col = dump_tuple(get_theme().COLOR_MUC_JID) - leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}' - '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}' - '%(jid)s\x19%(info_col)s}) has left the ' - 'chatroom') % { - 'spec':get_theme().CHAR_QUIT, - 'nick':from_nick, 'color':color, - 'jid':jid.full, 'info_col': info_col, - 'color_spec': spec_col, - 'jid_col': jid_col} - if status: - leave_msg += ' (\x19o%s\x19%s})' % (status, info_col) - self.add_message(leave_msg, typ=2) - self.core.on_user_left_private_conversation(from_room, from_nick, - status) - - def on_user_change_status( - self, user, from_nick, from_room, affiliation, role, show, status): - """ - When an user changes her status - """ - # build the message - display_message = False # flag to know if something significant enough - # to be displayed has changed - if config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - color = dump_tuple(user.color) - else: - color = 3 - if from_nick == self.own_nick: - msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - 'color': color} - else: - msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % { - 'nick': from_nick, 'color': color, - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)} - if affiliation != user.affiliation: - msg += 'affiliation: %s, ' % affiliation - display_message = True - if role != user.role: - msg += 'role: %s, ' % role - display_message = True - if show != user.show and show in SHOW_NAME: - msg += 'show: %s, ' % SHOW_NAME[show] - display_message = True - if status != user.status: - # if the user sets his status to nothing - if status: - msg += 'status: %s, ' % status - display_message = True - elif show in SHOW_NAME and show == user.show: - msg += 'show: %s, ' % SHOW_NAME[show] - display_message = True - if not display_message: - return - msg = msg[:-2] # remove the last ", " - hide_status_change = config.get_by_tabname('hide_status_change', - self.general_jid) - if hide_status_change < -1: - hide_status_change = -1 - if ((hide_status_change == -1 or \ - user.has_talked_since(hide_status_change) or\ - user.nick == self.own_nick)\ - and\ - (affiliation != user.affiliation or\ - role != user.role or\ - show != user.show or\ - status != user.status))\ - or\ - (affiliation != user.affiliation or\ - role != user.role): - # display the message in the room - self._text_buffer.add_message(msg) - self.core.on_user_changed_status_in_private('%s/%s' % - (from_room, from_nick), - msg) - self.users.remove(user) - # finally, effectively change the user status - user.update(affiliation, show, status, role) - bisect.insort_left(self.users, user) - - def disconnect(self): - """ - Set the state of the room as not joined, so - we can know if we can join it, send messages to it, etc - """ - self.users = [] - if self is not self.core.current_tab(): - self.state = 'disconnected' - self.joined = False - self.disable_self_ping_event() - - def get_single_line_topic(self): - """ - Return the topic as a single-line string (for the window header) - """ - return self.topic.replace('\n', '|') - - def log_message(self, txt, nickname, time=None, typ=1): - """ - Log the messages in the archives, if it needs - to be - """ - if time is None and self.joined: # don't log the history messages - if not logger.log_message(self.name, nickname, txt, typ=typ): - self.core.information('Unable to write in the log file', - 'Error') - - def do_highlight(self, txt, time, nickname): - """ - Set the tab color and returns the nick color - """ - highlighted = False - if not time and nickname and nickname != self.own_nick and self.joined: - - if re.search(r'\b' + self.own_nick.lower() + r'\b', txt.lower()): - if self.state != 'current': - self.state = 'highlight' - highlighted = True - else: - highlight_words = config.get_by_tabname('highlight_on', - self.general_jid) - highlight_words = highlight_words.split(':') - for word in highlight_words: - if word and word.lower() in txt.lower(): - if self.state != 'current': - self.state = 'highlight' - highlighted = True - break - if highlighted: - beep_on = config.get('beep_on').split() - if 'highlight' in beep_on and 'message' not in beep_on: - if not config.get_by_tabname('disable_beep', self.name): - curses.beep() - return highlighted - - def get_user_by_name(self, nick): - """ - Gets the user associated with the given nick, or None if not found - """ - for user in self.users: - if user.nick == nick: - return user - return None - - def add_message(self, txt, time=None, nickname=None, **kwargs): - """ - Note that user can be None even if nickname is not None. It happens - when we receive an history message said by someone who is not - in the room anymore - Return True if the message highlighted us. False otherwise. - """ - - # reset self-ping interval - if self.self_ping_event: - self.enable_self_ping_event() - - self.log_message(txt, nickname, time=time, typ=kwargs.get('typ', 1)) - args = dict() - for key, value in kwargs.items(): - if key not in ('typ', 'forced_user'): - args[key] = value - if nickname is not None: - user = self.get_user_by_name(nickname) - else: - user = None - - if user: - user.set_last_talked(datetime.now()) - args['user'] = user - if not user and kwargs.get('forced_user'): - args['user'] = kwargs['forced_user'] - - if (not time and nickname and nickname != self.own_nick - and self.state != 'current'): - if (self.state != 'highlight' and - config.get_by_tabname('notify_messages', self.name)): - self.state = 'message' - if time and not txt.startswith('/me'): - txt = '\x19%(info_col)s}%(txt)s' % { - 'txt': txt, - 'info_col': dump_tuple(get_theme().COLOR_LOG_MSG)} - elif not nickname: - txt = '\x19%(info_col)s}%(txt)s' % { - 'txt': txt, - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)} - elif not kwargs.get('highlight'): # TODO - args['highlight'] = self.do_highlight(txt, time, nickname) - time = time or datetime.now() - self._text_buffer.add_message(txt, time, nickname, **args) - return args.get('highlight', False) - - def modify_message(self, txt, old_id, new_id, - time=None, nickname=None, user=None, jid=None): - self.log_message(txt, nickname, time=time, typ=1) - highlight = self.do_highlight(txt, time, nickname) - message = self._text_buffer.modify_message(txt, old_id, new_id, - highlight=highlight, - time=time, user=user, - jid=jid) - if message: - self.text_win.modify_message(old_id, message) - return highlight - return False - - def matching_names(self): - return [(1, safeJID(self.name).user), (3, self.name)] - - def enable_self_ping_event(self): - delay = config.get_by_tabname("self_ping_delay", self.general_jid, default=0) - if delay <= 0: # use 0 or some negative value to disable it - return - self.disable_self_ping_event() - self.self_ping_event = timed_events.DelayedEvent(delay, self.send_self_ping) - self.core.add_timed_event(self.self_ping_event) - - def disable_self_ping_event(self): - if self.self_ping_event is not None: - self.core.remove_timed_event(self.self_ping_event) - self.self_ping_event = None - - def send_self_ping(self): - to = self.name + "/" + self.own_nick - self.core.xmpp.plugin['xep_0199'].send_ping(jid=to, - callback=self.on_self_ping_result, - timeout_callback=self.on_self_ping_failed, - timeout=60) - - def on_self_ping_result(self, iq): - if iq["type"] == "error": - self.command_cycle(iq["error"]["text"] or "not in this room") - self.core.refresh_window() - else: # Re-send a self-ping in a few seconds - self.enable_self_ping_event() - - def search_for_color(self, nick): - """ - Search for the color of a nick in the config file. - Also, look at the colors of its possible aliases if nick_color_aliases - is set. - """ - color = config.get_by_tabname(nick, 'muc_colors') - if color != '': - return color - nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name) - if nick_color_aliases: - nick_alias = re.sub('^_*(.*?)_*$', '\\1', nick) - color = config.get_by_tabname(nick_alias, 'muc_colors') - return color - - def on_self_ping_failed(self, iq): - self.command_cycle("the MUC server is not responding") - self.core.refresh_window() diff --git a/src/tabs/privatetab.py b/src/tabs/privatetab.py deleted file mode 100644 index a715a922..00000000 --- a/src/tabs/privatetab.py +++ /dev/null @@ -1,362 +0,0 @@ -""" -Module for the PrivateTab - -A PrivateTab is a private conversation opened with someone from a MUC -(see muctab.py). The conversation happens with both JID being relative -to the MUC (room@server/nick1 and room@server/nick2). - -This tab references his parent room, and is modified to keep track of -both participant’s nicks. It also has slightly different features than -the ConversationTab (such as tab-completion on nicks from the room). - -""" -import logging -log = logging.getLogger(__name__) - -import curses - -from . import OneToOneTab, MucTab, Tab - -import fixes -import windows -import xhtml -from common import safeJID -from config import config -from decorators import refresh_wrapper -from logger import logger -from theming import get_theme, dump_tuple -from decorators import command_args_parser - -class PrivateTab(OneToOneTab): - """ - The tab containg a private conversation (someone from a MUC) - """ - message_type = 'chat' - plugin_commands = {} - additional_informations = {} - plugin_keys = {} - def __init__(self, name, nick): - OneToOneTab.__init__(self, name) - self.own_nick = nick - self.name = name - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) - self.info_header = windows.PrivateInfoWin() - self.input = windows.MessageInput() - # keys - self.key_func['^I'] = self.completion - # commands - self.register_command('info', self.command_info, - desc='Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.', - shortdesc='Info about the user.') - self.register_command('unquery', self.command_unquery, - shortdesc='Close the tab.') - self.register_command('close', self.command_unquery, - shortdesc='Close the tab.') - self.register_command('version', self.command_version, - desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).', - shortdesc='Get the software version of a jid.') - self.resize() - self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab) - self.on = True - self.update_commands() - self.update_keys() - - @property - def general_jid(self): - return self.name - - def get_dest_jid(self): - return self.name - - @property - def nick(self): - return self.get_nick() - - @staticmethod - def add_information_element(plugin_name, callback): - """ - Lets a plugin add its own information to the PrivateInfoWin - """ - PrivateTab.additional_informations[plugin_name] = callback - - @staticmethod - def remove_information_element(plugin_name): - del PrivateTab.additional_informations[plugin_name] - - def load_logs(self, log_nb): - logs = logger.get_logs(safeJID(self.name).full.replace('/', '\\'), log_nb) - return logs - - def log_message(self, txt, nickname, time=None, typ=1): - """ - Log the messages in the archives. - """ - if not logger.log_message(self.name, nickname, txt, date=time, typ=typ): - self.core.information('Unable to write in the log file', 'Error') - - def on_close(self): - self.parent_muc.privates.remove(self) - - def completion(self): - """ - Called when Tab is pressed, complete the nickname in the input - """ - if self.complete_commands(self.input): - return - - # If we are not completing a command or a command's argument, complete a nick - compare_users = lambda x: x.last_talked - word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\ - if user.nick != self.own_nick] - after = config.get('after_completion') + ' ' - input_pos = self.input.pos - if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\ - self.input.get_text()[:input_pos] == self.input.last_completion + after): - add_after = after - else: - add_after = '' - self.input.auto_completion(word_list, add_after, quotify=False) - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - self.send_composing_chat_state(empty_after) - - @command_args_parser.raw - def command_say(self, line, attention=False, correct=False): - if not self.on: - return - msg = self.core.xmpp.make_message(self.name) - msg['type'] = 'chat' - msg['body'] = line - # trigger the event BEFORE looking for colors. - # This lets a plugin insert \x19xxx} colors, that will - # be converted in xhtml. - self.core.events.trigger('private_say', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - user = self.parent_muc.get_user_by_name(self.own_nick) - replaced = False - if correct or msg['replace']['id']: - msg['replace']['id'] = self.last_sent_message['id'] - if config.get_by_tabname('group_corrections', self.name): - try: - self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], - user=user, jid=self.core.xmpp.boundjid, nickname=self.own_nick) - replaced = True - except: - log.error('Unable to correct a message', exc_info=True) - else: - del msg['replace'] - - if msg['body'].find('\x19') != -1: - msg.enable('html') - msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) - msg['body'] = xhtml.clean_text(msg['body']) - if (config.get_by_tabname('send_chat_states', self.general_jid) and - self.remote_wants_chatstates is not False): - needed = 'inactive' if self.inactive else 'active' - msg['chat_state'] = needed - if attention and self.remote_supports_attention: - msg['attention'] = True - self.core.events.trigger('private_say_after', msg, self) - if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - return - if not replaced: - self.add_message(msg['body'], - nickname=self.own_nick or self.core.own_nick, - forced_user=user, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=msg['id'], - jid=self.core.xmpp.boundjid, - typ=1) - - self.last_sent_message = msg - if self.remote_supports_receipts: - msg._add_receipt = True - msg.send() - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() - - @command_args_parser.ignored - def command_unquery(self): - """ - /unquery - """ - self.core.close_tab() - - @command_args_parser.quoted(0, 1) - def command_version(self, args): - """ - /version - """ - def callback(res): - if not res: - return self.core.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.core.information(version, 'Info') - if args: - return self.core.command_version(args[0]) - jid = safeJID(self.name) - fixes.get_version(self.core.xmpp, jid, - callback=callback) - - @command_args_parser.quoted(0, 1) - def command_info(self, arg): - """ - /info - """ - if arg and arg[0]: - self.parent_muc.command_info(arg[0]) - else: - user = safeJID(self.name).resource - self.parent_muc.command_info(user) - - def resize(self): - self.need_resize = False - - if self.size.tab_degrade_y: - info_win_height = 0 - tab_win_height = 0 - else: - info_win_height = self.core.information_win_size - tab_win_height = Tab.tab_win_height() - - self.text_win.resize(self.height - 2 - info_win_height - tab_win_height, - self.width, 0, 0) - self.text_win.rebuild_everything(self._text_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_height - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - display_info_win = not self.size.tab_degrade_y - - self.text_win.refresh() - self.info_header.refresh(self.name, self.text_win, self.chatstate, - PrivateTab.additional_informations) - if display_info_win: - self.info_win.refresh() - - self.refresh_tab_win() - self.input.refresh() - - def refresh_info_header(self): - self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_informations) - self.input.refresh() - - def get_nick(self): - return safeJID(self.name).resource - - def on_input(self, key, raw): - if not raw and key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key, raw=raw) - if not self.on: - return False - empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined: - self.send_composing_chat_state(empty_after) - return False - - def on_lose_focus(self): - if self.input.text: - self.state = 'nonempty' - else: - self.state = 'normal' - - self.text_win.remove_line_separator() - self.text_win.add_line_separator(self._text_buffer) - tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname('send_chat_states', - self.general_jid) and not self.input.get_text() and self.on: - self.send_chat_state('inactive') - self.check_scrolled() - - def on_gain_focus(self): - self.state = 'current' - curses.curs_set(1) - tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - if tab and tab.joined and config.get_by_tabname('send_chat_states', - self.general_jid,) and not self.input.get_text() and self.on: - self.send_chat_state('active') - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: - return - self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0) - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - - def get_text_window(self): - return self.text_win - - @refresh_wrapper.conditional - def rename_user(self, old_nick, new_nick): - """ - The user changed her nick in the corresponding muc: update the tab’s name and - display a message. - """ - self.add_message('\x193}%(old)s\x19%(info_col)s} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2) - new_jid = safeJID(self.name).bare+'/'+new_nick - self.name = new_jid - return self.core.current_tab() is self - - @refresh_wrapper.conditional - def user_left(self, status_message, from_nick): - """ - The user left the associated MUC - """ - self.deactivate() - if not status_message: - self.add_message('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room' % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2) - else: - self.add_message('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"' % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2) - return self.core.current_tab() is self - - @refresh_wrapper.conditional - def user_rejoined(self, nick): - """ - The user (or at least someone with the same nick) came back in the MUC - """ - self.activate() - self.check_features() - tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab) - color = 3 - if tab and config.get_by_tabname('display_user_color_in_join_part', - self.general_jid): - user = tab.get_user_by_name(nick) - if user: - color = dump_tuple(user.color) - self.add_message('\x194}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2) - return self.core.current_tab() is self - - def activate(self, reason=None): - self.on = True - if reason: - self.add_message(txt=reason, typ=2) - - def deactivate(self, reason=None): - self.on = False - self.remote_wants_chatstates = None - if reason: - self.add_message(txt=reason, typ=2) - - def matching_names(self): - return [(3, safeJID(self.name).resource), (4, self.name)] - - diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py deleted file mode 100644 index a5c22304..00000000 --- a/src/tabs/rostertab.py +++ /dev/null @@ -1,1280 +0,0 @@ -""" -The RosterInfoTab is the tab showing roster info, the list of contacts, -half of it is dedicated to showing the information buffer, and a small -rectangle shows the current contact info. - -This module also includes functions to match users in the roster. -""" -import logging -log = logging.getLogger(__name__) - -import base64 -import curses -import difflib -import os -import ssl -from os import getenv, path -from functools import partial - -from . import Tab - -import common -import windows -from common import safeJID -from config import config -from contact import Contact, Resource -from decorators import refresh_wrapper -from roster import RosterGroup, roster -from theming import get_theme, dump_tuple -from decorators import command_args_parser - -class RosterInfoTab(Tab): - """ - A tab, splitted in two, containing the roster and infos - """ - plugin_commands = {} - plugin_keys = {} - def __init__(self): - Tab.__init__(self) - self.name = "Roster" - self.v_separator = windows.VerticalSeparator() - self.information_win = windows.TextWin() - self.core.information_buffer.add_window(self.information_win) - self.roster_win = windows.RosterWin() - self.contact_info_win = windows.ContactInfoWin() - self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") - self.input = self.default_help_message - self.state = 'normal' - self.key_func['^I'] = self.completion - self.key_func["/"] = self.on_slash - # disable most of the roster features when in anonymous mode - if not self.core.xmpp.anon: - self.key_func[' '] = self.on_space - self.key_func["KEY_UP"] = self.move_cursor_up - self.key_func["KEY_DOWN"] = self.move_cursor_down - self.key_func["M-u"] = self.move_cursor_to_next_contact - self.key_func["M-y"] = self.move_cursor_to_prev_contact - self.key_func["M-U"] = self.move_cursor_to_next_group - self.key_func["M-Y"] = self.move_cursor_to_prev_group - self.key_func["M-[1;5B"] = self.move_cursor_to_next_group - self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group - self.key_func["l"] = self.command_last_activity - self.key_func["o"] = self.toggle_offline_show - self.key_func["v"] = self.get_contact_version - self.key_func["i"] = self.show_contact_info - self.key_func["s"] = self.start_search - self.key_func["S"] = self.start_search_slow - self.key_func["n"] = self.change_contact_name - self.register_command('deny', self.command_deny, - usage='[jid]', - desc='Deny your presence to the provided JID (or the ' - 'selected contact in your roster), who is asking' - 'you to be in his/here roster.', - shortdesc='Deny an user your presence.', - completion=self.completion_deny) - self.register_command('accept', self.command_accept, - usage='[jid]', - desc='Allow the provided JID (or the selected contact ' - 'in your roster), to see your presence.', - shortdesc='Allow an user your presence.', - completion=self.completion_deny) - self.register_command('add', self.command_add, - usage='<jid>', - desc='Add the specified JID to your roster, ask him to' - ' allow you to see his presence, and allow him to' - ' see your presence.', - shortdesc='Add an user to your roster.') - self.register_command('name', self.command_name, - usage='<jid> [name]', - shortdesc='Set the given JID\'s name.', - completion=self.completion_name) - self.register_command('groupadd', self.command_groupadd, - usage='<jid> <group>', - desc='Add the given JID to the given group.', - shortdesc='Add an user to a group', - completion=self.completion_groupadd) - self.register_command('groupmove', self.command_groupmove, - usage='<jid> <old group> <new group>', - desc='Move the given JID from the old group to the new group.', - shortdesc='Move an user to another group.', - completion=self.completion_groupmove) - self.register_command('groupremove', self.command_groupremove, - usage='<jid> <group>', - desc='Remove the given JID from the given group.', - shortdesc='Remove an user from a group.', - completion=self.completion_groupremove) - self.register_command('remove', self.command_remove, - usage='[jid]', - desc='Remove the specified JID from your roster. This ' - 'will unsubscribe you from its presence, cancel ' - 'its subscription to yours, and remove the item ' - 'from your roster.', - shortdesc='Remove an user from your roster.', - completion=self.completion_remove) - self.register_command('export', self.command_export, - usage='[/path/to/file]', - desc='Export your contacts into /path/to/file if ' - 'specified, or $HOME/poezio_contacts if not.', - shortdesc='Export your roster to a file.', - completion=partial(self.completion_file, 1)) - self.register_command('import', self.command_import, - usage='[/path/to/file]', - desc='Import your contacts from /path/to/file if ' - 'specified, or $HOME/poezio_contacts if not.', - shortdesc='Import your roster from a file.', - completion=partial(self.completion_file, 1)) - self.register_command('password', self.command_password, - usage='<password>', - shortdesc='Change your password') - - self.register_command('reconnect', self.command_reconnect, - desc='Disconnect from the remote server if you are ' - 'currently connected and then connect to it again.', - shortdesc='Disconnect and reconnect to the server.') - self.register_command('disconnect', self.command_disconnect, - desc='Disconnect from the remote server.', - shortdesc='Disconnect from the server.') - self.register_command('clear', self.command_clear, - shortdesc='Clear the info buffer.') - self.register_command('last_activity', self.command_last_activity, - usage='<jid>', - desc='Informs you of the last activity of a JID.', - shortdesc='Get the activity of someone.', - completion=self.core.completion_last_activity) - - self.resize() - self.update_commands() - self.update_keys() - - def check_blocking(self, features): - if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon: - self.register_command('block', self.command_block, - usage='[jid]', - shortdesc='Prevent a JID from talking to you.', - completion=self.completion_block) - self.register_command('unblock', self.command_unblock, - usage='[jid]', - shortdesc='Allow a JID to talk to you.', - completion=self.completion_unblock) - self.register_command('list_blocks', self.command_list_blocks, - shortdesc='Show the blocked contacts.') - self.core.xmpp.del_event_handler('session_start', self.check_blocking) - self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message) - - def check_saslexternal(self, features): - if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon: - self.register_command('certs', self.command_certs, - desc='List the fingerprints of certificates' - ' which can connect to your account.', - shortdesc='List allowed client certs.') - self.register_command('cert_add', self.command_cert_add, - desc='Add a client certificate to the authorized ones. ' - 'It must have an unique name and be contained in ' - 'a PEM file. [management] is a boolean indicating' - ' if a client connected using this certificate can' - ' manage the certificates itself.', - shortdesc='Add a client certificate.', - usage='<name> <certificate path> [management]', - completion=self.completion_cert_add) - self.register_command('cert_disable', self.command_cert_disable, - desc='Remove a certificate from the list ' - 'of allowed ones. Clients currently ' - 'using this certificate will not be ' - 'forcefully disconnected.', - shortdesc='Disable a certificate', - usage='<name>') - self.register_command('cert_revoke', self.command_cert_revoke, - desc='Remove a certificate from the list ' - 'of allowed ones. Clients currently ' - 'using this certificate will be ' - 'forcefully disconnected.', - shortdesc='Revoke a certificate', - usage='<name>') - self.register_command('cert_fetch', self.command_cert_fetch, - desc='Retrieve a certificate with its ' - 'name. It will be stored in <path>.', - shortdesc='Fetch a certificate', - usage='<name> <path>', - completion=self.completion_cert_fetch) - - @property - def selected_row(self): - return self.roster_win.get_selected_row() - - @command_args_parser.ignored - def command_certs(self): - """ - /certs - """ - def cb(iq): - if iq['type'] == 'error': - self.core.information('Unable to retrieve the certificate list.', - 'Error') - return - certs = [] - for item in iq['sasl_certs']['items']: - users = '\n'.join(item['users']) - certs.append((item['name'], users)) - - if not certs: - return self.core.information('No certificates found', 'Info') - msg = 'Certificates:\n' - msg += '\n'.join(((' %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs)) - self.core.information(msg, 'Info') - - self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3) - - @command_args_parser.quoted(2, 1) - def command_cert_add(self, args): - """ - /cert_add <name> <certfile> [cert-management] - """ - if not args or len(args) < 2: - return self.core.command_help('cert_add') - def cb(iq): - if iq['type'] == 'error': - self.core.information('Unable to add the certificate.', 'Error') - else: - self.core.information('Certificate added.', 'Info') - - name = args[0] - - try: - with open(args[1]) as fd: - crt = fd.read() - crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '') - except Exception as e: - self.core.information('Unable to read the certificate: %s' % e, 'Error') - return - - if len(args) > 2: - management = args[2] - if management: - management = management.lower() - if management not in ('false', '0'): - management = True - else: - management = False - else: - management = False - else: - management = True - - self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb, - allow_management=management) - - def completion_cert_add(self, the_input): - """ - completion for /cert_add <name> <path> [management] - """ - text = the_input.get_text() - args = common.shell_split(text) - n = the_input.get_argument_position() - log.debug('%s %s %s', the_input.text, n, the_input.pos) - if n == 1: - return - elif n == 2: - return self.completion_file(2, the_input) - elif n == 3: - return the_input.new_completion(['true', 'false'], n) - - @command_args_parser.quoted(1) - def command_cert_disable(self, args): - """ - /cert_disable <name> - """ - if not args: - return self.core.command_help('cert_disable') - def cb(iq): - if iq['type'] == 'error': - self.core.information('Unable to disable the certificate.', 'Error') - else: - self.core.information('Certificate disabled.', 'Info') - - name = args[0] - - self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb) - - @command_args_parser.quoted(1) - def command_cert_revoke(self, args): - """ - /cert_revoke <name> - """ - if not args: - return self.core.command_help('cert_revoke') - def cb(iq): - if iq['type'] == 'error': - self.core.information('Unable to revoke the certificate.', 'Error') - else: - self.core.information('Certificate revoked.', 'Info') - - name = args[0] - - self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb) - - - @command_args_parser.quoted(2) - def command_cert_fetch(self, args): - """ - /cert_fetch <name> <path> - """ - if not args or len(args) < 2: - return self.core.command_help('cert_fetch') - def cb(iq): - if iq['type'] == 'error': - self.core.information('Unable to fetch the certificate.', - 'Error') - return - - cert = None - for item in iq['sasl_certs']['items']: - if item['name'] == name: - cert = base64.b64decode(item['x509cert']) - break - - if not cert: - return self.core.information('Certificate not found.', 'Info') - - cert = ssl.DER_cert_to_PEM_cert(cert) - with open(path, 'w') as fd: - fd.write(cert) - - self.core.information('File stored at %s' % path, 'Info') - - name = args[0] - path = args[1] - - self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb) - - def completion_cert_fetch(self, the_input): - """ - completion for /cert_fetch <name> <path> - """ - text = the_input.get_text() - args = common.shell_split(text) - n = the_input.get_argument_position() - log.debug('%s %s %s', the_input.text, n, the_input.pos) - if n == 1: - return - elif n == 2: - return self.completion_file(2, the_input) - - def on_blocked_message(self, message): - """ - When we try to send a message to a blocked contact - """ - tab = self.core.get_conversation_by_jid(message['from'], False) - if not tab: - log.debug('Received message from nonexistent tab: %s', message['from']) - message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % { - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), - 'jid': message['from'], - } - tab.add_message(message) - - @command_args_parser.quoted(0, 1) - def command_block(self, args): - """ - /block [jid] - """ - def callback(iq): - if iq['type'] == 'error': - return self.core.information('Could not block the contact.', 'Error') - elif iq['type'] == 'result': - return self.core.information('Contact blocked.', 'Info') - - item = self.roster_win.selected_row - if args: - jid = safeJID(args[0]) - elif isinstance(item, Contact): - jid = item.bare_jid - elif isinstance(item, Resource): - jid = item.jid.bare - self.core.xmpp.plugin['xep_0191'].block(jid, callback=callback) - - def completion_block(self, the_input): - """ - Completion for /block - """ - if the_input.get_argument_position() == 1: - jids = roster.jids() - return the_input.new_completion(jids, 1, '', quotify=False) - - @command_args_parser.quoted(0, 1) - def command_unblock(self, args): - """ - /unblock [jid] - """ - def callback(iq): - if iq['type'] == 'error': - return self.core.information('Could not unblock the contact.', 'Error') - elif iq['type'] == 'result': - return self.core.information('Contact unblocked.', 'Info') - - item = self.roster_win.selected_row - if args: - jid = safeJID(args[0]) - elif isinstance(item, Contact): - jid = item.bare_jid - elif isinstance(item, Resource): - jid = item.jid.bare - self.core.xmpp.plugin['xep_0191'].unblock(jid, callback=callback) - - def completion_unblock(self, the_input): - """ - Completion for /unblock - """ - def on_result(iq): - if iq['type'] == 'error': - return - l = sorted(str(item) for item in iq['blocklist']['items']) - return the_input.new_completion(l, 1, quotify=False) - - if the_input.get_argument_position(): - self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result) - return True - - @command_args_parser.ignored - def command_list_blocks(self): - """ - /list_blocks - """ - def callback(iq): - if iq['type'] == 'error': - return self.core.information('Could not retrieve the blocklist.', 'Error') - s = 'List of blocked JIDs:\n' - items = (str(item) for item in iq['blocklist']['items']) - jids = '\n'.join(items) - if jids: - s += jids - else: - s = 'No blocked JIDs.' - self.core.information(s, 'Info') - - self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback) - - @command_args_parser.ignored - def command_reconnect(self): - """ - /reconnect - """ - if self.core.xmpp.is_connected(): - self.core.disconnect(reconnect=True) - else: - self.core.xmpp.connect() - - @command_args_parser.ignored - def command_disconnect(self): - """ - /disconnect - """ - self.core.disconnect() - - @command_args_parser.quoted(0, 1) - def command_last_activity(self, args): - """ - /activity [jid] - """ - item = self.roster_win.selected_row - if args: - jid = args[0] - elif isinstance(item, Contact): - jid = item.bare_jid - elif isinstance(item, Resource): - jid = item.jid - else: - self.core.information('No JID selected.', 'Error') - return - self.core.command_last_activity(jid) - - def resize(self): - self.need_resize = False - if self.size.tab_degrade_x: - display_info = False - roster_width = self.width - else: - display_info = True - roster_width = self.width // 2 - if self.size.tab_degrade_y: - display_contact_win = False - contact_win_h = 0 - else: - display_contact_win = True - contact_win_h = 4 - if self.size.tab_degrade_y: - tab_win_height = 0 - else: - tab_win_height = Tab.tab_win_height() - - info_width = self.width - roster_width - 1 - if display_info: - self.v_separator.resize(self.height - 1 - tab_win_height, - 1, 0, roster_width) - self.information_win.resize(self.height - 1 - tab_win_height - - contact_win_h, - info_width, 0, roster_width + 1, - self.core.information_buffer) - if display_contact_win: - self.contact_info_win.resize(contact_win_h, - info_width, - self.height - tab_win_height - - contact_win_h - 1, - roster_width + 1) - self.roster_win.resize(self.height - 1 - Tab.tab_win_height(), - roster_width, 0, 0) - self.input.resize(1, self.width, self.height-1, 0) - self.default_help_message.resize(1, self.width, self.height-1, 0) - - def completion(self): - # Check if we are entering a command (with the '/' key) - if isinstance(self.input, windows.Input) and\ - not self.input.help_message: - self.complete_commands(self.input) - - def completion_file(self, complete_number, the_input): - """ - Generic quoted completion for files/paths - (use functools.partial to use directly as a completion - for a command) - """ - text = the_input.get_text() - args = common.shell_split(text) - n = the_input.get_argument_position() - if n == complete_number: - if args[n-1] == '' or len(args) < n+1: - home = os.getenv('HOME') or '/' - return the_input.new_completion([home, '/tmp'], n, quotify=True) - path_ = args[n] - if path.isdir(path_): - dir_ = path_ - base = '' - else: - dir_ = path.dirname(path_) - base = path.basename(path_) - try: - names = os.listdir(dir_) - except OSError: - names = [] - names_filtered = [name for name in names if name.startswith(base)] - if names_filtered: - names = names_filtered - if not names: - names = [path_] - end_list = [] - for name in names: - value = os.path.join(dir_, name) - if not name.startswith('.'): - end_list.append(value) - - return the_input.new_completion(end_list, n, quotify=True) - - @command_args_parser.ignored - def command_clear(self): - """ - /clear - """ - self.core.information_buffer.messages = [] - self.information_win.rebuild_everything(self.core.information_buffer) - self.core.information_win.rebuild_everything(self.core.information_buffer) - self.refresh() - - @command_args_parser.quoted(1) - def command_password(self, args): - """ - /password <password> - """ - def callback(iq): - if iq['type'] == 'result': - self.core.information('Password updated', 'Account') - if config.get('password'): - config.silent_set('password', args[0]) - else: - self.core.information('Unable to change the password', 'Account') - self.core.xmpp.plugin['xep_0077'].change_password(args[0], callback=callback) - - @command_args_parser.quoted(0, 1) - def command_deny(self, args): - """ - /deny [jid] - Denies a JID from our roster - """ - if not args: - item = self.roster_win.selected_row - if isinstance(item, Contact): - jid = item.bare_jid - else: - self.core.information('No subscription to deny') - return - else: - jid = safeJID(args[0]).bare - if not jid in [jid for jid in roster.jids()]: - self.core.information('No subscription to deny') - return - - contact = roster[jid] - if contact: - contact.unauthorize() - self.core.information('Subscription to %s was revoked' % jid, - 'Roster') - - @command_args_parser.quoted(1) - def command_add(self, args): - """ - Add the specified JID to the roster, and set automatically - accept the reverse subscription - """ - if args is None: - self.core.information('No JID specified', 'Error') - return - jid = safeJID(safeJID(args[0]).bare) - if not str(jid): - self.core.information('The provided JID (%s) is not valid' % (args[0],), 'Error') - return - if jid in roster and roster[jid].subscription in ('to', 'both'): - return self.core.information('Already subscribed.', 'Roster') - roster.add(jid) - roster.modified() - self.core.information('%s was added to the roster' % jid, 'Roster') - - @command_args_parser.quoted(1, 1) - def command_name(self, args): - """ - Set a name for the specified JID in your roster - """ - def callback(iq): - if not iq: - self.core.information('The name could not be set.', 'Error') - log.debug('Error in /name:\n%s', iq) - if args is None: - return self.core.command_help('name') - jid = safeJID(args[0]).bare - name = args[1] if len(args) == 2 else '' - - contact = roster[jid] - if contact is None: - self.core.information('No such JID in roster', 'Error') - return - - groups = set(contact.groups) - if 'none' in groups: - groups.remove('none') - subscription = contact.subscription - self.core.xmpp.update_roster(jid, name=name, groups=groups, - subscription=subscription, callback=callback) - - @command_args_parser.quoted(2) - def command_groupadd(self, args): - """ - Add the specified JID to the specified group - """ - if args is None: - return self.core.command_help('groupadd') - jid = safeJID(args[0]).bare - group = args[1] - - contact = roster[jid] - if contact is None: - self.core.information('No such JID in roster', 'Error') - return - - new_groups = set(contact.groups) - if group in new_groups: - self.core.information('JID already in group', 'Error') - return - - roster.modified() - new_groups.add(group) - try: - new_groups.remove('none') - except KeyError: - pass - - name = contact.name - subscription = contact.subscription - - def callback(iq): - if iq: - roster.update_contact_groups(jid) - else: - self.core.information('The group could not be set.', 'Error') - log.debug('Error in groupadd:\n%s', iq) - - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) - - @command_args_parser.quoted(3) - def command_groupmove(self, args): - """ - Remove the specified JID from the first specified group and add it to the second one - """ - if args is None: - return self.core.command_help('groupmove') - jid = safeJID(args[0]).bare - group_from = args[1] - group_to = args[2] - - contact = roster[jid] - if not contact: - self.core.information('No such JID in roster', 'Error') - return - - new_groups = set(contact.groups) - if 'none' in new_groups: - new_groups.remove('none') - - if group_to == 'none' or group_from == 'none': - self.core.information('"none" is not a group.', 'Error') - return - - if group_from not in new_groups: - self.core.information('JID not in first group', 'Error') - return - - if group_to in new_groups: - self.core.information('JID already in second group', 'Error') - return - - if group_to == group_from: - self.core.information('The groups are the same.', 'Error') - return - - roster.modified() - new_groups.add(group_to) - if 'none' in new_groups: - new_groups.remove('none') - - new_groups.remove(group_from) - name = contact.name - subscription = contact.subscription - - def callback(iq): - if iq: - roster.update_contact_groups(contact) - else: - self.core.information('The group could not be set') - log.debug('Error in groupmove:\n%s', iq) - - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) - - @command_args_parser.quoted(2) - def command_groupremove(self, args): - """ - Remove the specified JID from the specified group - """ - if args is None: - return self.core.command_help('groupremove') - - jid = safeJID(args[0]).bare - group = args[1] - - contact = roster[jid] - if contact is None: - self.core.information('No such JID in roster', 'Error') - return - - new_groups = set(contact.groups) - try: - new_groups.remove('none') - except KeyError: - pass - if group not in new_groups: - self.core.information('JID not in group', 'Error') - return - - roster.modified() - - new_groups.remove(group) - name = contact.name - subscription = contact.subscription - - def callback(iq): - if iq: - roster.update_contact_groups(jid) - else: - self.core.information('The group could not be set') - log.debug('Error in groupremove:\n%s', iq) - - self.core.xmpp.update_roster(jid, name=name, groups=new_groups, - subscription=subscription, callback=callback) - - @command_args_parser.quoted(0, 1) - def command_remove(self, args): - """ - Remove the specified JID from the roster. i.e.: unsubscribe - from its presence, and cancel its subscription to our. - """ - if args: - jid = safeJID(args[0]).bare - else: - item = self.roster_win.selected_row - if isinstance(item, Contact): - jid = item.bare_jid - else: - self.core.information('No roster item to remove') - return - roster.remove(jid) - del roster[jid] - - @command_args_parser.quoted(0, 1) - def command_import(self, args): - """ - Import the contacts - """ - if args: - if args[0].startswith('/'): - filepath = args[0] - else: - filepath = path.join(getenv('HOME'), args[0]) - else: - filepath = path.join(getenv('HOME'), 'poezio_contacts') - if not path.isfile(filepath): - self.core.information('The file %s does not exist' % filepath, 'Error') - return - try: - handle = open(filepath, 'r', encoding='utf-8') - lines = handle.readlines() - handle.close() - except IOError: - self.core.information('Could not open %s' % filepath, 'Error') - log.error('Unable to correct a message', exc_info=True) - return - for jid in lines: - self.command_add(jid.lstrip('\n')) - self.core.information('Contacts imported from %s' % filepath, 'Info') - - @command_args_parser.quoted(0, 1) - def command_export(self, args): - """ - Export the contacts - """ - if args: - if args[0].startswith('/'): - filepath = args[0] - else: - filepath = path.join(getenv('HOME'), args[0]) - else: - filepath = path.join(getenv('HOME'), 'poezio_contacts') - if path.isfile(filepath): - self.core.information('The file already exists', 'Error') - return - elif not path.isdir(path.dirname(filepath)): - self.core.information('Parent directory not found', 'Error') - return - if roster.export(filepath): - self.core.information('Contacts exported to %s' % filepath, 'Info') - else: - self.core.information('Failed to export contacts to %s' % filepath, 'Info') - - def completion_remove(self, the_input): - """ - Completion for /remove - """ - jids = [jid for jid in roster.jids()] - return the_input.auto_completion(jids, '', quotify=False) - - def completion_name(self, the_input): - """Completion for /name""" - n = the_input.get_argument_position() - if n == 1: - jids = [jid for jid in roster.jids()] - return the_input.new_completion(jids, n, quotify=True) - return False - - def completion_groupadd(self, the_input): - n = the_input.get_argument_position() - if n == 1: - jids = sorted(jid for jid in roster.jids()) - return the_input.new_completion(jids, n, '', quotify=True) - elif n == 2: - groups = sorted(group for group in roster.groups if group != 'none') - return the_input.new_completion(groups, n, '', quotify=True) - return False - - def completion_groupmove(self, the_input): - args = common.shell_split(the_input.text) - n = the_input.get_argument_position() - if n == 1: - jids = sorted(jid for jid in roster.jids()) - return the_input.new_completion(jids, n, '', quotify=True) - elif n == 2: - contact = roster[args[1]] - if not contact: - return False - groups = list(contact.groups) - if 'none' in groups: - groups.remove('none') - return the_input.new_completion(groups, n, '', quotify=True) - elif n == 3: - groups = sorted(group for group in roster.groups) - return the_input.new_completion(groups, n, '', quotify=True) - return False - - def completion_groupremove(self, the_input): - args = common.shell_split(the_input.text) - n = the_input.get_argument_position() - if n == 1: - jids = sorted(jid for jid in roster.jids()) - return the_input.new_completion(jids, n, '', quotify=True) - elif n == 2: - contact = roster[args[1]] - if contact is None: - return False - groups = sorted(contact.groups) - try: - groups.remove('none') - except ValueError: - pass - return the_input.new_completion(groups, n, '', quotify=True) - return False - - def completion_deny(self, the_input): - """ - Complete the first argument from the list of the - contact with ask=='subscribe' - """ - jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values() - if contact.pending_in) - return the_input.new_completion(jids, 1, '', quotify=False) - - @command_args_parser.quoted(0, 1) - def command_accept(self, args): - """ - Accept a JID from in roster. Authorize it AND subscribe to it - """ - if not args: - item = self.roster_win.selected_row - if isinstance(item, Contact): - jid = item.bare_jid - else: - self.core.information('No subscription to accept') - return - else: - jid = safeJID(args[0]).bare - nodepart = safeJID(jid).user - jid = safeJID(jid) - # crappy transports putting resources inside the node part - if '\\2f' in nodepart: - jid.user = nodepart.split('\\2f')[0] - contact = roster[jid] - if contact is None: - return - contact.pending_in = False - roster.modified() - self.core.xmpp.send_presence(pto=jid, ptype='subscribed') - self.core.xmpp.client_roster.send_last_presence() - if contact.subscription in ('from', 'none') and not contact.pending_out: - self.core.xmpp.send_presence(pto=jid, ptype='subscribe', pnick=self.core.own_nick) - - self.core.information('%s is now authorized' % jid, 'Roster') - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - - display_info = not self.size.tab_degrade_x - display_contact_win = not self.size.tab_degrade_y - - self.roster_win.refresh(roster) - if display_info: - self.v_separator.refresh() - self.information_win.refresh() - if display_contact_win: - self.contact_info_win.refresh( - self.roster_win.get_selected_row()) - self.refresh_tab_win() - self.input.refresh() - - def on_input(self, key, raw): - if key == '^M': - selected_row = self.roster_win.get_selected_row() - res = self.input.do_command(key, raw=raw) - if res and not isinstance(self.input, windows.Input): - return True - elif res: - return False - if key == '^M': - self.core.on_roster_enter_key(selected_row) - return selected_row - elif not raw and key in self.key_func: - return self.key_func[key]() - - @refresh_wrapper.conditional - def toggle_offline_show(self): - """ - Show or hide offline contacts - """ - option = 'roster_show_offline' - value = config.get(option) - success = config.silent_set(option, str(not value)) - roster.modified() - if not success: - self.core.information('Unable to write in the config file', 'Error') - return True - - def on_slash(self): - """ - '/' is pressed, we enter "input mode" - """ - if isinstance(self.input, windows.YesNoInput): - return - curses.curs_set(1) - self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) - self.input.resize(1, self.width, self.height-1, 0) - self.input.do_command("/") # we add the slash - - def reset_help_message(self, _=None): - self.input = self.default_help_message - if self.core.current_tab() is self: - curses.curs_set(0) - self.input.refresh() - self.core.doupdate() - return True - - def execute_slash_command(self, txt): - if txt.startswith('/'): - self.input.key_enter() - self.execute_command(txt) - return self.reset_help_message() - - def on_lose_focus(self): - self.state = 'normal' - - def on_gain_focus(self): - self.state = 'current' - if isinstance(self.input, windows.HelpText): - curses.curs_set(0) - else: - curses.curs_set(1) - - @refresh_wrapper.conditional - def move_cursor_down(self): - if isinstance(self.input, windows.Input) and not self.input.history_disabled: - return - return self.roster_win.move_cursor_down() - - @refresh_wrapper.conditional - def move_cursor_up(self): - if isinstance(self.input, windows.Input) and not self.input.history_disabled: - return - return self.roster_win.move_cursor_up() - - def move_cursor_to_prev_contact(self): - self.roster_win.move_cursor_up() - while not isinstance(self.roster_win.get_selected_row(), Contact): - if not self.roster_win.move_cursor_up(): - break - self.roster_win.refresh(roster) - - def move_cursor_to_next_contact(self): - self.roster_win.move_cursor_down() - while not isinstance(self.roster_win.get_selected_row(), Contact): - if not self.roster_win.move_cursor_down(): - break - self.roster_win.refresh(roster) - - def move_cursor_to_prev_group(self): - self.roster_win.move_cursor_up() - while not isinstance(self.roster_win.get_selected_row(), RosterGroup): - if not self.roster_win.move_cursor_up(): - break - self.roster_win.refresh(roster) - - def move_cursor_to_next_group(self): - self.roster_win.move_cursor_down() - while not isinstance(self.roster_win.get_selected_row(), RosterGroup): - if not self.roster_win.move_cursor_down(): - break - self.roster_win.refresh(roster) - - def on_scroll_down(self): - return self.roster_win.move_cursor_down(self.height // 2) - - def on_scroll_up(self): - return self.roster_win.move_cursor_up(self.height // 2) - - @refresh_wrapper.conditional - def on_space(self): - if isinstance(self.input, windows.Input): - return - selected_row = self.roster_win.get_selected_row() - if isinstance(selected_row, RosterGroup): - selected_row.toggle_folded() - roster.modified() - return True - elif isinstance(selected_row, Contact): - group = "none" - found_group = False - pos = self.roster_win.pos - while not found_group and pos >= 0: - row = self.roster_win.roster_cache[pos] - pos -= 1 - if isinstance(row, RosterGroup): - found_group = True - group = row.name - selected_row.toggle_folded(group) - roster.modified() - return True - return False - - def get_contact_version(self): - """ - Show the versions of the resource(s) currently selected - """ - selected_row = self.roster_win.get_selected_row() - if isinstance(selected_row, Contact): - for resource in selected_row.resources: - self.core.command_version(str(resource.jid)) - elif isinstance(selected_row, Resource): - self.core.command_version(str(selected_row.jid)) - else: - self.core.information('Nothing to get versions from', 'Info') - - def show_contact_info(self): - """ - Show the contact info (resource number, status, presence, etc) - when 'i' is pressed. - """ - selected_row = self.roster_win.get_selected_row() - if isinstance(selected_row, Contact): - cont = selected_row - res = selected_row.get_highest_priority_resource() - acc = [] - acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence if res else 'unavailable')) - if res: - acc.append('%s connected resource%s' % (len(cont), '' if len(cont) == 1 else 's')) - acc.append('Current status: %s' % res.status) - if cont.tune: - acc.append('Tune: %s' % common.format_tune_string(cont.tune)) - if cont.mood: - acc.append('Mood: %s' % cont.mood) - if cont.activity: - acc.append('Activity: %s' % cont.activity) - if cont.gaming: - acc.append('Game: %s' % (common.format_gaming_string(cont.gaming))) - msg = '\n'.join(acc) - elif isinstance(selected_row, Resource): - res = selected_row - msg = 'Resource: %s (%s)\nCurrent status: %s\nPriority: %s' % ( - res.jid, - res.presence, - res.status, - res.priority) - elif isinstance(selected_row, RosterGroup): - rg = selected_row - msg = 'Group: %s [%s/%s] contacts online' % ( - rg.name, - rg.get_nb_connected_contacts(), - len(rg),) - else: - msg = None - if msg: - self.core.information(msg, 'Info') - - def change_contact_name(self): - """ - Auto-fill a /name command when 'n' is pressed - """ - selected_row = self.roster_win.get_selected_row() - if isinstance(selected_row, Contact): - jid = selected_row.bare_jid - elif isinstance(selected_row, Resource): - jid = safeJID(selected_row.jid).bare - else: - return - self.on_slash() - self.input.text = '/name "%s" ' % jid - self.input.key_end() - self.input.refresh() - - @refresh_wrapper.always - def start_search(self): - """ - Start the search. The input should appear with a short instruction - in it. - """ - if isinstance(self.input, windows.YesNoInput): - return - curses.curs_set(1) - self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) - self.input.resize(1, self.width, self.height-1, 0) - self.input.disable_history() - roster.modified() - self.refresh() - return True - - @refresh_wrapper.always - def start_search_slow(self): - if isinstance(self.input, windows.YesNoInput): - return - curses.curs_set(1) - self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter_slow) - self.input.resize(1, self.width, self.height-1, 0) - self.input.disable_history() - return True - - def set_roster_filter_slow(self, txt): - roster.contact_filter = (jid_and_name_match_slow, txt) - roster.modified() - self.refresh() - return False - - def set_roster_filter(self, txt): - roster.contact_filter = (jid_and_name_match, txt) - roster.modified() - self.refresh() - return False - - @refresh_wrapper.always - def on_search_terminate(self, txt): - curses.curs_set(0) - roster.contact_filter = None - self.reset_help_message() - roster.modified() - return True - - def on_close(self): - return - -def diffmatch(search, string): - """ - Use difflib and a loop to check if search_pattern can - be 'almost' found INSIDE a string. - 'almost' being defined by difflib - """ - if len(search) > len(string): - return False - l = len(search) - ratio = 0.7 - for i in range(len(string) - l + 1): - if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio: - return True - return False - -def jid_and_name_match(contact, txt): - """ - Match jid with text precisely - """ - if not txt: - return True - txt = txt.lower() - if txt in safeJID(contact.bare_jid).bare.lower(): - return True - if txt in contact.name.lower(): - return True - return False - -def jid_and_name_match_slow(contact, txt): - """ - A function used to know if a contact in the roster should - be shown in the roster - """ - if not txt: - return True # Everything matches when search is empty - user = safeJID(contact.bare_jid).bare - if diffmatch(txt, user): - return True - if contact.name and diffmatch(txt, contact.name): - return True - return False diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py deleted file mode 100644 index b063ad35..00000000 --- a/src/tabs/xmltab.py +++ /dev/null @@ -1,360 +0,0 @@ -""" -The XMLTab is here for debugging purposes, it shows the incoming and -outgoing stanzas. It has a few useful functions that can filter stanzas -in order to only show the relevant ones, and it can also be frozen or -unfrozen on demand so that the relevant information is not drowned by -the traffic. -""" -import logging -log = logging.getLogger(__name__) - -import curses -import os -from slixmpp.xmlstream import matcher -from slixmpp.xmlstream.tostring import tostring -from slixmpp.xmlstream.stanzabase import ElementBase -from xml.etree import ElementTree as ET - -from . import Tab - -import text_buffer -import windows -from xhtml import clean_text -from decorators import command_args_parser, refresh_wrapper -from common import safeJID - - -class MatchJID(object): - - def __init__(self, jid, dest=''): - self.jid = jid - self.dest = dest - - def match(self, xml): - from_ = safeJID(xml['from']) - to_ = safeJID(xml['to']) - if self.jid.full == self.jid.bare: - from_ = from_.bare - to_ = to_.bare - - if self.dest == 'from': - return from_ == self.jid - elif self.dest == 'to': - return to_ == self.jid - return self.jid in (from_, to_) - - def __repr__(self): - return '%s%s%s' % (self.dest, ': ' if self.dest else '', self.jid) - -MATCHERS_MAPPINGS = { - MatchJID: ('JID', lambda obj: repr(obj)), - matcher.MatcherId: ('ID', lambda obj: obj._criteria), - matcher.MatchXMLMask: ('XMLMask', lambda obj: tostring(obj._criteria)), - matcher.MatchXPath: ('XPath', lambda obj: obj._criteria) -} - -class XMLTab(Tab): - def __init__(self): - Tab.__init__(self) - self.state = 'normal' - self.name = 'XMLTab' - self.filters = [] - - self.core_buffer = self.core.xml_buffer - self.filtered_buffer = text_buffer.TextBuffer() - - self.info_header = windows.XMLInfoWin() - self.text_win = windows.XMLTextWin() - self.core_buffer.add_window(self.text_win) - self.default_help_message = windows.HelpText("/ to enter a command") - - self.register_command('close', self.close, - shortdesc="Close this tab.") - self.register_command('clear', self.command_clear, - shortdesc='Clear the current buffer.') - self.register_command('reset', self.command_reset, - shortdesc='Reset the stanza filter.') - self.register_command('filter_id', self.command_filter_id, - usage='<id>', - desc='Show only the stanzas with the id <id>.', - shortdesc='Filter by id.') - self.register_command('filter_xpath', self.command_filter_xpath, - usage='<xpath>', - desc='Show only the stanzas matching the xpath <xpath>.' - ' Any occurrences of %n will be replaced by jabber:client.', - shortdesc='Filter by XPath.') - self.register_command('filter_jid', self.command_filter_jid, - usage='<jid>', - desc='Show only the stanzas matching the jid <jid> in from= or to=.', - shortdesc='Filter by JID.') - self.register_command('filter_from', self.command_filter_from, - usage='<jid>', - desc='Show only the stanzas matching the jid <jid> in from=.', - shortdesc='Filter by JID from.') - self.register_command('filter_to', self.command_filter_to, - usage='<jid>', - desc='Show only the stanzas matching the jid <jid> in to=.', - shortdesc='Filter by JID to.') - self.register_command('filter_xmlmask', self.command_filter_xmlmask, - usage='<xml mask>', - desc='Show only the stanzas matching the given xml mask.', - shortdesc='Filter by xml mask.') - self.register_command('dump', self.command_dump, - usage='<filename>', - desc='Writes the content of the XML buffer into a file.', - shortdesc='Write in a file.') - self.input = self.default_help_message - self.key_func['^T'] = self.close - self.key_func['^I'] = self.completion - self.key_func["KEY_DOWN"] = self.on_scroll_down - self.key_func["KEY_UP"] = self.on_scroll_up - self.key_func["^K"] = self.on_freeze - self.key_func["/"] = self.on_slash - self.resize() - # Used to display the infobar - self.filter_type = '' - self.filter = '' - - def gen_filter_repr(self): - if not self.filters: - self.filter_type = '' - self.filter = '' - return - filter_types = map(lambda x: MATCHERS_MAPPINGS[type(x)][0], self.filters) - filter_strings = map(lambda x: MATCHERS_MAPPINGS[type(x)][1](x), self.filters) - self.filter_type = ','.join(filter_types) - self.filter = ','.join(filter_strings) - - def update_filters(self, matcher): - if not self.filters: - messages = self.core_buffer.messages[:] - self.filtered_buffer.messages = [] - self.core_buffer.del_window(self.text_win) - self.filtered_buffer.add_window(self.text_win) - else: - messages = self.filtered_buffer.messages - self.filtered_buffer.messages = [] - self.filters.append(matcher) - new_messages = [] - for msg in messages: - try: - if msg.txt.strip() and self.match_stanza(ElementBase(ET.fromstring(clean_text(msg.txt)))): - new_messages.append(msg) - except ET.ParseError: - log.debug('Malformed XML : %s', msg.txt, exc_info=True) - self.filtered_buffer.messages = new_messages - self.text_win.rebuild_everything(self.filtered_buffer) - self.gen_filter_repr() - - def on_freeze(self): - """ - Freeze the display. - """ - self.text_win.toggle_lock() - self.refresh() - - def match_stanza(self, stanza): - for matcher in self.filters: - if not matcher.match(stanza): - return False - return True - - @command_args_parser.raw - def command_filter_xmlmask(self, mask): - """/filter_xmlmask <xml mask>""" - try: - self.update_filters(matcher.MatchXMLMask(mask)) - self.refresh() - except Exception as e: - self.core.information('Invalid XML Mask: %s' % e, 'Error') - self.command_reset('') - - @command_args_parser.raw - def command_filter_to(self, jid): - """/filter_jid_to <jid>""" - jid_obj = safeJID(jid) - if not jid_obj: - return self.core.information('Invalid JID: %s' % jid, 'Error') - - self.update_filters(MatchJID(jid_obj, dest='to')) - self.refresh() - - @command_args_parser.raw - def command_filter_from(self, jid): - """/filter_jid_from <jid>""" - jid_obj = safeJID(jid) - if not jid_obj: - return self.core.information('Invalid JID: %s' % jid, 'Error') - - self.update_filters(MatchJID(jid_obj, dest='from')) - self.refresh() - - @command_args_parser.raw - def command_filter_jid(self, jid): - """/filter_jid <jid>""" - jid_obj = safeJID(jid) - if not jid_obj: - return self.core.information('Invalid JID: %s' % jid, 'Error') - - self.update_filters(MatchJID(jid_obj)) - self.refresh() - - @command_args_parser.quoted(1) - def command_filter_id(self, args): - """/filter_id <id>""" - if args is None: - return self.core.command_help('filter_id') - - self.update_filters(matcher.MatcherId(args[0])) - self.refresh() - - @command_args_parser.raw - def command_filter_xpath(self, xpath): - """/filter_xpath <xpath>""" - try: - self.update_filters(matcher.MatchXPath(xpath.replace('%n', self.core.xmpp.default_ns))) - self.refresh() - except: - self.core.information('Invalid XML Path', 'Error') - self.command_reset('') - - @command_args_parser.ignored - def command_reset(self): - """/reset""" - if self.filters: - self.filters = [] - self.filtered_buffer.del_window(self.text_win) - self.core_buffer.add_window(self.text_win) - self.text_win.rebuild_everything(self.core_buffer) - self.filter_type = '' - self.filter = '' - self.refresh() - - @command_args_parser.quoted(1) - def command_dump(self, args): - """/dump <filename>""" - if args is None: - return self.core.command_help('dump') - if self.filters: - xml = self.filtered_buffer.messages[:] - else: - xml = self.core_buffer.messages[:] - text = '\n'.join(('%s %s %s' % (msg.str_time, msg.nickname, clean_text(msg.txt)) for msg in xml)) - filename = os.path.expandvars(os.path.expanduser(args[0])) - try: - with open(filename, 'w') as fd: - fd.write(text) - except Exception as e: - self.core.information('Could not write the XML dump: %s' % e, 'Error') - - def on_slash(self): - """ - '/' is pressed, activate the input - """ - curses.curs_set(1) - self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) - self.input.resize(1, self.width, self.height-1, 0) - self.input.do_command("/") # we add the slash - - @refresh_wrapper.always - def reset_help_message(self, _=None): - if self.closed: - return True - if self.core.current_tab() is self: - curses.curs_set(0) - self.input = self.default_help_message - return True - - def on_scroll_up(self): - return self.text_win.scroll_up(self.text_win.height-1) - - def on_scroll_down(self): - return self.text_win.scroll_down(self.text_win.height-1) - - @command_args_parser.ignored - def command_clear(self): - """ - /clear - """ - if self.filters: - buffer = self.core_buffer - else: - buffer = self.filtered_buffer - buffer.messages = [] - self.text_win.rebuild_everything(buffer) - self.refresh() - self.core.doupdate() - - def execute_slash_command(self, txt): - if txt.startswith('/'): - self.input.key_enter() - self.execute_command(txt) - return self.reset_help_message() - - def completion(self): - if isinstance(self.input, windows.Input): - self.complete_commands(self.input) - - def on_input(self, key, raw): - res = self.input.do_command(key, raw=raw) - if res: - return True - if not raw and key in self.key_func: - return self.key_func[key]() - - def close(self, arg=None): - self.core.close_tab() - - def resize(self): - self.need_resize = False - if self.size.tab_degrade_y: - info_win_size = 0 - tab_win_height = 0 - else: - info_win_size = self.core.information_win_size - tab_win_height = Tab.tab_win_height() - - self.text_win.resize(self.height - info_win_size - tab_win_height - 2, - self.width, 0, 0) - self.text_win.rebuild_everything(self.core.xml_buffer) - self.info_header.resize(1, self.width, - self.height - 2 - info_win_size - - tab_win_height, - 0) - self.input.resize(1, self.width, self.height-1, 0) - - def refresh(self): - if self.need_resize: - self.resize() - log.debug(' TAB Refresh: %s', self.__class__.__name__) - - if self.size.tab_degrade_y: - display_info_win = False - else: - display_info_win = True - - self.text_win.refresh() - self.info_header.refresh(self.filter_type, self.filter, self.text_win) - self.refresh_tab_win() - if display_info_win: - self.info_win.refresh() - self.input.refresh() - - def on_lose_focus(self): - self.state = 'normal' - - def on_gain_focus(self): - self.state = 'current' - curses.curs_set(0) - - def on_close(self): - self.command_clear('') - self.core.xml_tab = False - - def on_info_win_size_changed(self): - if self.core.information_win_size >= self.height-3: - return - self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0) - self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - - diff --git a/src/text_buffer.py b/src/text_buffer.py deleted file mode 100644 index dd5bc58a..00000000 --- a/src/text_buffer.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -Define the TextBuffer class - -A text buffer contains a list of intermediate representations of messages -(not xml stanzas, but neither the Lines used in windows.py. - -Each text buffer can be linked to multiple windows, that will be rendered -independantly by their TextWins. -""" - -import logging -log = logging.getLogger(__name__) - -import collections - -from datetime import datetime -from config import config -from theming import get_theme, dump_tuple - -message_fields = ('txt nick_color time str_time nickname user identifier' - ' highlight me old_message revisions jid ack') -Message = collections.namedtuple('Message', message_fields) - -class CorrectionError(Exception): - pass - -class AckError(Exception): - pass - -def other_elems(self): - "Helper for the repr_message function" - acc = ['Message('] - fields = message_fields.split() - fields.remove('old_message') - for field in fields: - acc.append('%s=%s' % (field, repr(getattr(self, field)))) - return ', '.join(acc) + ', old_message=' - -def repr_message(self): - """ - repr() for the Message class, for debug purposes, since the default - repr() is recursive, so it can stack overflow given too many revisions - of a message - """ - init = other_elems(self) - acc = [init] - next_message = self.old_message - rev = 1 - while next_message: - acc.append(other_elems(next_message)) - next_message = next_message.old_message - rev += 1 - acc.append('None') - while rev: - acc.append(')') - rev -= 1 - return ''.join(acc) - -Message.__repr__ = repr_message -Message.__str__ = repr_message - -class TextBuffer(object): - """ - This class just keep trace of messages, in a list with various - informations and attributes. - """ - def __init__(self, messages_nb_limit=None): - - if messages_nb_limit is None: - messages_nb_limit = config.get('max_messages_in_memory') - self.messages_nb_limit = messages_nb_limit - # Message objects - self.messages = [] - # we keep track of one or more windows - # so we can pass the new messages to them, as they are added, so - # they (the windows) can build the lines from the new message - self.windows = [] - - def add_window(self, win): - self.windows.append(win) - - @property - def last_message(self): - return self.messages[-1] if self.messages else None - - - @staticmethod - def make_message(txt, time, nickname, nick_color, history, user, - identifier, str_time=None, highlight=False, - old_message=None, revisions=0, jid=None, ack=0): - """ - Create a new Message object with parameters, check for /me messages, - and delayed messages - """ - time = time or datetime.now() - if txt.startswith('/me '): - me = True - txt = '\x19%s}%s' % (dump_tuple(get_theme().COLOR_ME_MESSAGE), - txt[4:]) - else: - me = False - if history: - txt = txt.replace('\x19o', '\x19o\x19%s}' % - dump_tuple(get_theme().COLOR_LOG_MSG)) - str_time = time.strftime("%Y-%m-%d %H:%M:%S") - else: - if str_time is None: - str_time = time.strftime("%H:%M:%S") - else: - str_time = '' - - msg = Message( - txt='%s\x19o'%(txt.replace('\t', ' '),), - nick_color=nick_color, - time=time, - str_time=str_time, - nickname=nickname, - user=user, - identifier=identifier, - highlight=highlight, - me=me, - old_message=old_message, - revisions=revisions, - jid=jid, - ack=ack) - log.debug('Set message %s with %s.', identifier, msg) - return msg - - def add_message(self, txt, time=None, nickname=None, - nick_color=None, history=None, user=None, highlight=False, - identifier=None, str_time=None, jid=None, ack=0): - """ - Create a message and add it to the text buffer - """ - msg = self.make_message(txt, time, nickname, nick_color, history, - user, identifier, str_time=str_time, - highlight=highlight, jid=jid, ack=ack) - self.messages.append(msg) - - while len(self.messages) > self.messages_nb_limit: - self.messages.pop(0) - - ret_val = None - show_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') - for window in self.windows: # make the associated windows - # build the lines from the new message - nb = window.build_new_message(msg, history=history, - highlight=highlight, - timestamp=show_timestamps, - nick_size=nick_size) - if ret_val is None: - ret_val = nb - if window.pos != 0: - window.scroll_up(nb) - - return ret_val or 1 - - def _find_message(self, old_id): - """ - Find a message in the text buffer from its message id - """ - for i in range(len(self.messages) -1, -1, -1): - msg = self.messages[i] - if msg.identifier == old_id: - return i - return -1 - - def ack_message(self, old_id, jid): - """Mark a message as acked""" - return self.edit_ack(1, old_id, jid) - - def nack_message(self, error, old_id, jid): - """Mark a message as errored""" - return self.edit_ack(-1, old_id, jid, append=error) - - def edit_ack(self, value, old_id, jid, append=''): - """ - Edit the ack status of a message, and optionally - append some text. - """ - i = self._find_message(old_id) - if i == -1: - return - msg = self.messages[i] - if msg.jid != jid: - raise AckError('Wrong JID for message id %s (was %s, expected %s)' % - (old_id, msg.jid, jid)) - - new_msg = list(msg) - new_msg[12] = value - if append: - new_msg[0] = new_msg[0] + append - new_msg = Message(*new_msg) - self.messages[i] = new_msg - return new_msg - - def modify_message(self, txt, old_id, new_id, highlight=False, - time=None, user=None, jid=None): - """ - Correct a message in a text buffer. - """ - - i = self._find_message(old_id) - - if i == -1: - log.debug('Message %s not found in text_buffer, abort replacement.', - old_id) - raise CorrectionError("nothing to replace") - - msg = self.messages[i] - - if msg.user and msg.user is not user: - raise CorrectionError("Different users") - elif len(msg.str_time) > 8: # ugly - raise CorrectionError("Delayed message") - elif not msg.user and (msg.jid is None or jid is None): - raise CorrectionError('Could not check the ' - 'identity of the sender') - elif not msg.user and msg.jid != jid: - raise CorrectionError('Messages %s and %s have not been ' - 'sent by the same fullJID' % - (old_id, new_id)) - - if not time: - time = msg.time - message = self.make_message(txt, time, msg.nickname, - msg.nick_color, None, msg.user, - new_id, highlight=highlight, - old_message=msg, - revisions=msg.revisions + 1, - jid=jid) - self.messages[i] = message - log.debug('Replacing message %s with %s.', old_id, new_id) - return message - - def del_window(self, win): - self.windows.remove(win) - - def __del__(self): - size = len(self.messages) - log.debug('** Deleting %s messages from textbuffer', size) diff --git a/src/theming.py b/src/theming.py deleted file mode 100755 index 5d263741..00000000 --- a/src/theming.py +++ /dev/null @@ -1,534 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -Define the variables (colors and some other stuff) that are -used when drawing the interface. - -Colors are numbers from -1 to 7 (if only 8 colors are supported) or -1 to 255 -if 256 colors are available. -If only 8 colors are available, all colors > 8 are converted using the -table_256_to_16 dict. - -XHTML-IM colors are converted to -1 -> 255 colors if available, or directly to --1 -> 8 if we are in 8-color-mode. - -A pair_color is a background-foreground pair. All possible pairs are not created -at startup, because that would create 256*256 pairs, and almost all of them -would never be used. - -A theme should define color tuples, like ``(200, -1)``, and when they are to -be used by poezio's interface, they will be created once, and kept in a list for -later usage. -A color tuple is of the form ``(foreground, background, optional)`` -A color of -1 means the default color. So if you do not want to have -a background color, use ``(x, -1)``. -The optional third value of the tuple defines additional information. It -is a string and can contain one or more of these characters: - -- ``b``: bold -- ``u``: underlined -- ``x``: blink - -For example, ``(200, 208, 'bu')`` is bold, underlined and pink foreground on -orange background. - -A theme file is a python file containing one object named 'theme', which is an -instance of a class (derived from the Theme class) defined in that same file. -For example, in pinkytheme.py: - -.. code-block:: python - - import theming - class PinkyTheme(theming.Theme): - COLOR_NORMAL_TEXT = (200, -1) - - theme = PinkyTheme() - -if the command '/theme pinkytheme' is issued, we import the pinkytheme.py file -and set the global variable 'theme' to pinkytheme.theme. - -And in poezio's code we just use ``theme.COLOR_NORMAL_TEXT`` etc - -Since a theme inherites from the Theme class (defined here), if a color is not defined in a -theme file, the color is the default one. - -Some values in that class are a list of color tuple. -For example ``[(1, -1), (2, -1), (3, -1)]`` -Such a list SHOULD contain at least one color tuple. -It is used for example to define color gradient, etc. -""" - -import logging -log = logging.getLogger(__name__) - -from config import config - -import curses -import os -from os import path - -from importlib import machinery -finder = machinery.PathFinder() - -class Theme(object): - """ - The theme class, from which all themes should inherit. - All of the following values can be replaced in subclasses, in - order to create a new theme. - - Do not edit this file if you want to change the theme to suit your - needs. Create a new theme and share it if you think it can be useful - for others. - """ - @classmethod - def color_role(cls, role): - role_mapping = { - 'moderator': cls.COLOR_USER_MODERATOR, - 'participant': cls.COLOR_USER_PARTICIPANT, - 'visitor': cls.COLOR_USER_VISITOR, - 'none': cls.COLOR_USER_NONE, - '': cls.COLOR_USER_NONE - } - return role_mapping.get(role, cls.COLOR_USER_NONE) - - @classmethod - def char_affiliation(cls, affiliation): - affiliation_mapping = { - 'owner': cls.CHAR_AFFILIATION_OWNER, - 'admin': cls.CHAR_AFFILIATION_ADMIN, - 'member': cls.CHAR_AFFILIATION_MEMBER, - 'none': cls.CHAR_AFFILIATION_NONE - } - return affiliation_mapping.get(affiliation, cls.CHAR_AFFILIATION_NONE) - - @classmethod - def color_show(cls, show): - show_mapping = { - 'xa': cls.COLOR_STATUS_XA, - 'none': cls.COLOR_STATUS_NONE, - 'dnd': cls.COLOR_STATUS_DND, - 'away': cls.COLOR_STATUS_AWAY, - 'chat': cls.COLOR_STATUS_CHAT, - '': cls.COLOR_STATUS_ONLINE, - 'available': cls.COLOR_STATUS_ONLINE, - 'unavailable': cls.COLOR_STATUS_UNAVAILABLE, - } - return show_mapping.get(show, cls.COLOR_STATUS_NONE) - - @classmethod - def char_subscription(cls, sub, keep='incomplete'): - sub_mapping = { - 'from': cls.CHAR_ROSTER_FROM, - 'both': cls.CHAR_ROSTER_BOTH, - 'none': cls.CHAR_ROSTER_NONE, - 'to': cls.CHAR_ROSTER_TO, - } - if keep == 'incomplete' and sub == 'both': - return '' - if keep in ('both', 'none', 'to', 'from'): - return sub_mapping[sub] if sub == keep else '' - return sub_mapping.get(sub, '') - - # Message text color - COLOR_NORMAL_TEXT = (-1, -1) - COLOR_INFORMATION_TEXT = (5, -1) # TODO - COLOR_WARNING_TEXT = (1, -1) - - # Color of the commands in the help message - COLOR_HELP_COMMANDS = (208, -1) - - # "reverse" is a special value, available only for this option. It just - # takes the nick colors and reverses it. A theme can still specify a - # fixed color if need be. - COLOR_HIGHLIGHT_NICK = "reverse" - - # Color of the participant JID in a MUC - COLOR_MUC_JID = (4, -1) - - # User list color - COLOR_USER_VISITOR = (239, -1) - COLOR_USER_PARTICIPANT = (4, -1) - COLOR_USER_NONE = (0, -1) - COLOR_USER_MODERATOR = (1, -1) - - # nickname colors - COLOR_REMOTE_USER = (5, -1) - - # The character printed in color (COLOR_STATUS_*) before the nickname - # in the user list - CHAR_STATUS = '|' - - # The characters used for the chatstates in the user list - # in a MUC - CHAR_CHATSTATE_ACTIVE = 'A' - CHAR_CHATSTATE_COMPOSING = 'X' - CHAR_CHATSTATE_PAUSED = 'p' - - # These characters are used for the affiliation in the user list - # in a MUC - CHAR_AFFILIATION_OWNER = '~' - CHAR_AFFILIATION_ADMIN = '&' - CHAR_AFFILIATION_MEMBER = '+' - CHAR_AFFILIATION_NONE = '-' - - - # XML Tab - CHAR_XML_IN = 'IN ' - CHAR_XML_OUT = 'OUT' - COLOR_XML_IN = (1, -1) - COLOR_XML_OUT = (2, -1) - - # Color for the /me message - COLOR_ME_MESSAGE = (6, -1) - - # Color for the number of revisions of a message - COLOR_REVISIONS_MESSAGE = (3, -1, 'b') - - # Color for various important text. For example the "?" before JIDs in - # the roster that require an user action. - COLOR_IMPORTANT_TEXT = (3, 5, 'b') - - # Separators - COLOR_VERTICAL_SEPARATOR = (4, -1) - COLOR_NEW_TEXT_SEPARATOR = (2, -1) - COLOR_MORE_INDICATOR = (6, 4) - - # Time - CHAR_TIME_LEFT = '' - CHAR_TIME_RIGHT = '' - COLOR_TIME_STRING = (-1, -1) - - # Tabs - COLOR_TAB_NORMAL = (7, 4) - COLOR_TAB_NONEMPTY = (7, 4) - COLOR_TAB_SCROLLED = (5, 4) - COLOR_TAB_JOINED = (82, 4) - COLOR_TAB_CURRENT = (7, 6) - COLOR_TAB_COMPOSING = (7, 5) - COLOR_TAB_NEW_MESSAGE = (7, 5) - COLOR_TAB_HIGHLIGHT = (7, 3) - COLOR_TAB_PRIVATE = (7, 2) - COLOR_TAB_ATTENTION = (7, 1) - COLOR_TAB_DISCONNECTED = (7, 8) - - COLOR_VERTICAL_TAB_NORMAL = (4, -1) - COLOR_VERTICAL_TAB_NONEMPTY = (4, -1) - COLOR_VERTICAL_TAB_JOINED = (82, -1) - COLOR_VERTICAL_TAB_SCROLLED = (66, -1) - COLOR_VERTICAL_TAB_CURRENT = (7, 4) - COLOR_VERTICAL_TAB_NEW_MESSAGE = (5, -1) - COLOR_VERTICAL_TAB_COMPOSING = (5, -1) - COLOR_VERTICAL_TAB_HIGHLIGHT = (3, -1) - COLOR_VERTICAL_TAB_PRIVATE = (2, -1) - COLOR_VERTICAL_TAB_ATTENTION = (1, -1) - COLOR_VERTICAL_TAB_DISCONNECTED = (8, -1) - - # Nickname colors - # A list of colors randomly attributed to nicks in MUCs - # Setting more colors makes it harder to have two nicks with the same color, - # avoiding confusions. - LIST_COLOR_NICKNAMES = [ - (1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (9, -1), - (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (19, -1), - (20, -1), (21, -1), (22, -1), (23, -1), (24, -1), (25, -1), - (26, -1), (27, -1), (28, -1), (29, -1), (30, -1), (31, -1), - (32, -1), (33, -1), (34, -1), (35, -1), (36, -1), (37, -1), - (38, -1), (39, -1), (40, -1), (41, -1), (42, -1), (43, -1), - (44, -1), (45, -1), (46, -1), (47, -1), (48, -1), (49, -1), - (50, -1), (51, -1), (54, -1), (55, -1), (56, -1), (57, -1), - (58, -1), (60, -1), (61, -1), (62, -1), (63, -1), (64, -1), - (65, -1), (66, -1), (67, -1), (68, -1), (69, -1), (70, -1), - (71, -1), (72, -1), (73, -1), (74, -1), (75, -1), (76, -1), - (77, -1), (78, -1), (79, -1), (80, -1), (81, -1), (82, -1), - (83, -1), (84, -1), (85, -1), (86, -1), (87, -1), (88, -1), - (89, -1), (90, -1), (91, -1), (92, -1), (93, -1), (94, -1), - (95, -1), (96, -1), (97, -1), (98, -1), (99, -1), (100, -1), - (101, -1), (103, -1), (104, -1), (105, -1), (106, -1), (107, -1), - (108, -1), (109, -1), (110, -1), (111, -1), (112, -1), (113, -1), - (114, -1), (115, -1), (116, -1), (117, -1), (118, -1), (119, -1), - (120, -1), (121, -1), (122, -1), (123, -1), (124, -1), (125, -1), - (126, -1), (127, -1), (128, -1), (129, -1), (130, -1), (131, -1), - (132, -1), (133, -1), (134, -1), (135, -1), (136, -1), (137, -1), - (138, -1), (139, -1), (140, -1), (141, -1), (142, -1), (143, -1), - (144, -1), (145, -1), (146, -1), (147, -1), (148, -1), (149, -1), - (150, -1), (151, -1), (152, -1), (153, -1), (154, -1), (155, -1), - (156, -1), (157, -1), (158, -1), (159, -1), (160, -1), (161, -1), - (162, -1), (163, -1), (164, -1), (165, -1), (166, -1), (167, -1), - (168, -1), (169, -1), (170, -1), (171, -1), (172, -1), (173, -1), - (174, -1), (175, -1), (176, -1), (177, -1), (178, -1), (179, -1), - (180, -1), (181, -1), (182, -1), (183, -1), (184, -1), (185, -1), - (186, -1), (187, -1), (188, -1), (189, -1), (190, -1), (191, -1), - (192, -1), (193, -1), (196, -1), (197, -1), (198, -1), (199, -1), - (200, -1), (201, -1), (202, -1), (203, -1), (204, -1), (205, -1), - (206, -1), (207, -1), (208, -1), (209, -1), (210, -1), (211, -1), - (212, -1), (213, -1), (214, -1), (215, -1), (216, -1), (217, -1), - (218, -1), (219, -1), (220, -1), (221, -1), (222, -1), (223, -1), - (224, -1), (225, -1), (226, -1), (227, -1)] - - # This is your own nickname - COLOR_OWN_NICK = (254, -1) - - COLOR_LOG_MSG = (5, -1) - # This is for in-tab error messages - COLOR_ERROR_MSG = (9, 7, 'b') - # Status color - COLOR_STATUS_XA = (16, 90) - COLOR_STATUS_NONE = (16, 4) - COLOR_STATUS_DND = (16, 1) - COLOR_STATUS_AWAY = (16, 3) - COLOR_STATUS_CHAT = (16, 2) - COLOR_STATUS_UNAVAILABLE = (-1, 247) - COLOR_STATUS_ONLINE = (16, 4) - - # Bars - COLOR_WARNING_PROMPT = (16, 1, 'b') - COLOR_INFORMATION_BAR = (7, 4) - COLOR_TOPIC_BAR = (7, 4) - COLOR_SCROLLABLE_NUMBER = (220, 4, 'b') - COLOR_SELECTED_ROW = (-1, 33) - COLOR_PRIVATE_NAME = (-1, 4) - COLOR_CONVERSATION_NAME = (2, 4) - COLOR_CONVERSATION_RESOURCE = (121, 4) - COLOR_GROUPCHAT_NAME = (7, 4) - COLOR_COLUMN_HEADER = (36, 4) - COLOR_COLUMN_HEADER_SEL = (4, 36) - - # Strings for special messages (like join, quit, nick change, etc) - # Special messages - CHAR_JOIN = '--->' - CHAR_QUIT = '<---' - CHAR_KICK = '-!-' - CHAR_NEW_TEXT_SEPARATOR = '- ' - CHAR_OK = '✔' - CHAR_ERROR = '✖' - CHAR_EMPTY = ' ' - CHAR_ACK_RECEIVED = CHAR_OK - CHAR_NACK = CHAR_ERROR - CHAR_COLUMN_ASC = ' ▲' - CHAR_COLUMN_DESC = ' ▼' - CHAR_ROSTER_ERROR = CHAR_ERROR - CHAR_ROSTER_TUNE = '♪' - CHAR_ROSTER_ASKED = '?' - CHAR_ROSTER_ACTIVITY = 'A' - CHAR_ROSTER_MOOD = 'M' - CHAR_ROSTER_GAMING = 'G' - CHAR_ROSTER_FROM = '←' - CHAR_ROSTER_BOTH = '↔' - CHAR_ROSTER_TO = '→' - CHAR_ROSTER_NONE = '⇹' - - COLOR_CHAR_ACK = (2, -1) - COLOR_CHAR_NACK = (1, -1) - - COLOR_ROSTER_GAMING = (6, -1) - COLOR_ROSTER_MOOD = (2, -1) - COLOR_ROSTER_ACTIVITY = (3, -1) - COLOR_ROSTER_TUNE = (6, -1) - COLOR_ROSTER_ERROR = (1, -1) - COLOR_ROSTER_SUBSCRIPTION = (-1, -1) - - COLOR_JOIN_CHAR = (4, -1) - COLOR_QUIT_CHAR = (1, -1) - COLOR_KICK_CHAR = (1, -1) - - # Vertical tab list color - COLOR_VERTICAL_TAB_NUMBER = (34, -1) - - # Info messages color (the part before the ">") - INFO_COLORS = { - 'info': (5, -1), - 'error': (16, 1), - 'warning': (1, -1), - 'roster': (2, -1), - 'help': (10, -1), - 'headline': (11, -1, 'b'), - 'tune': (6, -1), - 'gaming': (6, -1), - 'mood': (2, -1), - 'activity': (3, -1), - 'default': (7, -1), - } - -# This is the default theme object, used if no theme is defined in the conf -theme = Theme() - -# a dict "color tuple -> color_pair" -# Each time we use a color tuple, we check if it has already been used. -# If not we create a new color_pair and keep it in that dict, to use it -# the next time. -curses_colors_dict = {} - -table_256_to_16 = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 0, 4, 4, 4, 12, 12, 2, 6, 4, 4, 12, 12, 2, 2, 6, 4, - 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, - 10, 10, 10, 14, 1, 5, 4, 4, 12, 12, 3, 8, 4, 4, 12, 12, - 2, 2, 6, 4, 12, 12, 2, 2, 2, 6, 12, 12, 10, 10, 10, 10, - 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 5, 4, 12, 12, 1, 1, - 5, 4, 12, 12, 3, 3, 8, 4, 12, 12, 2, 2, 2, 6, 12, 12, - 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, 1, 1, 1, 5, - 12, 12, 1, 1, 1, 5, 12, 12, 1, 1, 1, 5, 12, 12, 3, 3, - 3, 7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14, - 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, 13, 12, 9, 9, 9, 9, - 13, 12, 9, 9, 9, 9, 13, 12, 11, 11, 11, 11, 7, 12, 10, 10, - 10, 10, 10, 14, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, - 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, - 9, 13, 11, 11, 11, 11, 11, 15, 0, 0, 0, 0, 0, 0, 8, 8, - 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15, 15, 15, 15, 15 -] - -load_path = [] - -def color_256_to_16(color): - if color == -1: - return color - return table_256_to_16[color] - -def dump_tuple(tup): - """ - Dump a tuple to a string of fg,bg,attr (optional) - """ - return ','.join(str(i) for i in tup) - -def read_tuple(_str): - """ - Read a tuple dumped with dump_tumple - """ - attrs = _str.split(',') - char = attrs[2] if len(attrs) > 2 else None - return (int(attrs[0]), int(attrs[1])), char - -def to_curses_attr(color_tuple): - """ - Takes a color tuple (as defined at the top of this file) and - returns a valid curses attr that can be passed directly to attron() or attroff() - """ - # extract the color from that tuple - if len(color_tuple) == 3: - colors = (color_tuple[0], color_tuple[1]) - else: - colors = color_tuple - - bold = False - if curses.COLORS != 256: - # We are not in a term supporting 256 colors, so we convert - # colors to numbers between -1 and 8 - colors = (color_256_to_16(colors[0]), color_256_to_16(colors[1])) - if colors[0] >= 8: - colors = (colors[0] - 8, colors[1]) - bold = True - if colors[1] >= 8: - colors = (colors[0], colors[1] - 8) - - # check if we already used these colors - try: - pair = curses_colors_dict[colors] - except KeyError: - pair = len(curses_colors_dict) + 1 - curses.init_pair(pair, colors[0], colors[1]) - curses_colors_dict[colors] = pair - curses_pair = curses.color_pair(pair) - if len(color_tuple) == 3: - additional_val = color_tuple[2] - if 'b' in additional_val or bold is True: - curses_pair = curses_pair | curses.A_BOLD - if 'u' in additional_val: - curses_pair = curses_pair | curses.A_UNDERLINE - if 'a' in additional_val: - curses_pair = curses_pair | curses.A_BLINK - return curses_pair - -def get_theme(): - """ - Returns the current theme - """ - return theme - -def update_themes_dir(option=None, value=None): - global load_path - load_path = [] - - # import from the git sources - default_dir = path.join( - path.dirname(path.dirname(__file__)), - 'data/themes') - if path.exists(default_dir): - load_path.append(default_dir) - - # import from the user-defined prefs - themes_dir = path.expanduser( - value or - config.get('themes_dir') or - path.join(os.environ.get('XDG_DATA_HOME') or - path.join(os.environ.get('HOME'), '.local', 'share'), - 'poezio', 'themes') - ) - try: - os.makedirs(themes_dir) - except OSError as e: - if e.errno != 17: - log.error('Unable to create the themes dir (%s)', themes_dir) - else: - load_path.append(themes_dir) - else: - load_path.append(themes_dir) - - # system-wide import - try: - import poezio_themes - except: - pass - else: - if poezio_themes.__path__: - load_path.append(list(poezio_themes.__path__)[0]) - - log.debug('Theme load path: %s', load_path) - -def reload_theme(): - theme_name = config.get('theme') - global theme - if theme_name == 'default' or not theme_name.strip(): - theme = Theme() - return - new_theme = None - exc = None - try: - loader = finder.find_module(theme_name, load_path) - if not loader: - return 'Failed to load the theme %s' % theme_name - new_theme = loader.load_module() - except Exception as e: - log.error('Failed to load the theme %s', theme_name, exc_info=True) - exc = e - - if not new_theme: - return 'Failed to load theme: %s' % exc - - if hasattr(new_theme, 'theme'): - theme = new_theme.theme - else: - return 'No theme present in the theme file' - -if __name__ == '__main__': - # Display some nice text with nice colors - s = curses.initscr() - curses.start_color() - curses.use_default_colors() - s.addstr('%s colors detected\n\n' % curses.COLORS, to_curses_attr((3, -1))) - for i in range(curses.COLORS): - s.addstr('%s ' % i, to_curses_attr((i, -1))) - s.addstr('\n') - s.refresh() - try: - s.getkey() - except KeyboardInterrupt: - pass - finally: - curses.endwin() - print() - diff --git a/src/timed_events.py b/src/timed_events.py deleted file mode 100644 index 7f43d05f..00000000 --- a/src/timed_events.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -Timed events are the standard way to schedule events for later in poezio. - -Once created, they must be added to the list of checked events with -:py:func:`Core.add_timed_event` (within poezio) or with -:py:func:`.PluginAPI.add_timed_event` (within a plugin). -""" - -import asyncio -import logging - -log = logging.getLogger(__name__) - -import datetime - -class DelayedEvent(object): - """ - A TimedEvent, but with the date calculated from now + a delay in seconds. - Use it if you want an event to happen in, e.g. 6 seconds. - """ - def __init__(self, delay, callback, *args): - """ - Create a new DelayedEvent. - - :param int delay: The number of seconds. - :param function callback: The handler that will be executed. - :param \*args: Optional arguments passed to the handler. - """ - self.callback = callback - self.args = args - self.delay = delay - # An asyncio handler, as returned by call_later() or call_at() - self.handler = None - -class TimedEvent(DelayedEvent): - """ - An event with a callback that is called when the specified time is passed. - - The callback and its arguments should be passed as the lasts arguments. - """ - def __init__(self, date, callback, *args): - """ - Create a new timed event. - - :param datetime.datetime date: Time at which the callback must be run. - :param function callback: The handler that will be executed. - :param \*args: Optional arguments passed to the handler. - """ - delta = date - datetime.datetime.now() - delay = delta.total_seconds() - DelayedEvent.__init__(self, delay, callback, *args) diff --git a/src/user.py b/src/user.py deleted file mode 100644 index 4142869b..00000000 --- a/src/user.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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. - -""" -Define the user class. -An user is a MUC participant, not a roster contact (see contact.py) -""" - -from random import choice -from datetime import timedelta, datetime -from hashlib import md5 -import xhtml - -from theming import get_theme - -import logging -log = logging.getLogger(__name__) - -ROLE_DICT = { - '':0, - 'none':0, - 'visitor':1, - 'participant':2, - 'moderator':3 - } - -class User(object): - """ - keep trace of an user in a Room - """ - __slots__ = ('last_talked', 'jid', 'chatstate', 'affiliation', 'show', 'status', 'role', 'nick', 'color') - - def __init__(self, nick, affiliation, show, status, role, jid, deterministic=True, color=''): - self.last_talked = datetime(1, 1, 1) # The oldest possible time - self.update(affiliation, show, status, role) - self.change_nick(nick) - if color != '': - self.change_color(color, deterministic) - else: - if deterministic: - self.set_deterministic_color() - else: - self.color = choice(get_theme().LIST_COLOR_NICKNAMES) - self.jid = jid - self.chatstate = None - - def set_deterministic_color(self): - theme = get_theme() - mod = len(theme.LIST_COLOR_NICKNAMES) - nick_pos = int(md5(self.nick.encode('utf-8')).hexdigest(), 16) % mod - self.color = theme.LIST_COLOR_NICKNAMES[nick_pos] - - def update(self, affiliation, show, status, role): - self.affiliation = affiliation - self.show = show - self.status = status - if role not in ROLE_DICT: # avoid unvalid roles - role = '' - self.role = role - - def change_nick(self, nick): - self.nick = nick - - def change_color(self, color_name, deterministic=False): - color = xhtml.colors.get(color_name) - if color == None: - log.error('Unknown color "%s"' % color_name) - if deterministic: - self.set_deterministic_color() - else: - self.color = choice(get_theme().LIST_COLOR_NICKNAMES) - else: - self.color = (color, -1) - - def set_last_talked(self, time): - """ - time: datetime object - """ - self.last_talked = time - - def has_talked_since(self, t): - """ - t: int - Return True if the user talked since the last s seconds - """ - if self.last_talked is None: - return False - delta = timedelta(0, t) - if datetime.now() - delta > self.last_talked: - return False - return True - - def __repr__(self): - return ">%s<" % (self.nick) - - def __eq__(self, b): - return self.role == b.role and self.nick == b.nick - - def __gt__(self, b): - if ROLE_DICT[self.role] == ROLE_DICT[b.role]: - return self.nick.lower() > b.nick.lower() - return ROLE_DICT[self.role] < ROLE_DICT[b.role] - - def __ge__(self, b): - if ROLE_DICT[self.role] == ROLE_DICT[b.role]: - return self.nick.lower() >= b.nick.lower() - return ROLE_DICT[self.role] <= ROLE_DICT[b.role] - - def __lt__(self, b): - if ROLE_DICT[self.role] == ROLE_DICT[b.role]: - return self.nick.lower() < b.nick.lower() - return ROLE_DICT[self.role] > ROLE_DICT[b.role] - - def __le__(self, b): - if ROLE_DICT[self.role] == ROLE_DICT[b.role]: - return self.nick.lower() <= b.nick.lower() - return ROLE_DICT[self.role] >= ROLE_DICT[b.role] diff --git a/src/windows/__init__.py b/src/windows/__init__.py deleted file mode 100644 index 5ec73961..00000000 --- a/src/windows/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Module exporting all the Windows, which are wrappers around curses wins -used to display information on the screen -""" - -from . base_wins import Win -from . data_forms import FormWin -from . bookmark_forms import BookmarksWin -from . info_bar import GlobalInfoBar, VerticalGlobalInfoBar -from . info_wins import InfoWin, XMLInfoWin, PrivateInfoWin, MucListInfoWin, \ - ConversationInfoWin, DynamicConversationInfoWin, MucInfoWin, \ - ConversationStatusMessageWin, BookmarksInfoWin -from . input_placeholders import HelpText, YesNoInput -from . inputs import Input, HistoryInput, MessageInput, CommandInput -from . list import ListWin, ColumnHeaderWin -from . misc import VerticalSeparator -from . muc import UserList, Topic -from . roster_win import RosterWin, ContactInfoWin -from . text_win import TextWin, XMLTextWin - diff --git a/src/windows/base_wins.py b/src/windows/base_wins.py deleted file mode 100644 index 464c6fa1..00000000 --- a/src/windows/base_wins.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Define the base window object and the constants/"globals" used -by the file of this module. - -A window is a little part of the screen, for example the input window, -the text window, the roster window, etc. -A Tab (see the src/tabs module) is composed of multiple Windows -""" - -import logging -log = logging.getLogger(__name__) - -import collections -import curses -import string - -import core -import singleton -from theming import to_curses_attr, read_tuple - -FORMAT_CHAR = '\x19' -# These are non-printable chars, so they should never appear in the input, -# I guess. But maybe we can find better chars that are even less risky. -format_chars = ['\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13', - '\x14', '\x15', '\x16', '\x17', '\x18'] - -# different colors allowed in the input -allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7') - -# msg is a reference to the corresponding Message tuple. text_start and -# text_end are the position delimiting the text in this line. -Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend') - -LINES_NB_LIMIT = 4096 - -class DummyWin(object): - def __getattribute__(self, name): - if name != '__bool__': - return lambda *args, **kwargs: (0, 0) - else: - return object.__getattribute__(self, name) - - def __bool__(self): - return False - -class Win(object): - _win_core = None - _tab_win = None - def __init__(self): - self._win = None - self.height, self.width = 0, 0 - - def _resize(self, height, width, y, x): - if height == 0 or width == 0: - self.height, self.width = height, width - return - self.height, self.width, self.x, self.y = height, width, x, y - try: - self._win = Win._tab_win.derwin(height, width, y, x) - except: - log.debug('DEBUG: mvwin returned ERR. Please investigate') - if self._win is None: - self._win = DummyWin() - - def resize(self, height, width, y, x): - """ - Override if something has to be done on resize - """ - self._resize(height, width, y, x) - - def _refresh(self): - self._win.noutrefresh() - - def addnstr(self, *args): - """ - Safe call to addnstr - """ - try: - self._win.addnstr(*args) - except: - # this actually mostly returns ERR, but works. - # more specifically, when the added string reaches the end - # of the screen. - pass - - def addstr(self, *args): - """ - Safe call to addstr - """ - try: - self._win.addstr(*args) - except: - pass - - def move(self, y, x): - try: - self._win.move(y, x) - except: - self._win.move(0, 0) - - def addstr_colored(self, text, y=None, x=None): - """ - Write a string on the window, setting the - attributes as they are in the string. - For example: - \x19bhello → hello in bold - \x191}Bonj\x192}our → 'Bonj' in red and 'our' in green - next_attr_char is the \x19 delimiter - attr_char is the char following it, it can be - one of 'u', 'b', 'c[0-9]' - """ - if y is not None and x is not None: - self.move(y, x) - next_attr_char = text.find(FORMAT_CHAR) - while next_attr_char != -1 and text: - if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char+1].lower() - else: - attr_char = str() - if next_attr_char != 0: - self.addstr(text[:next_attr_char]) - if attr_char == 'o': - self._win.attrset(0) - elif attr_char == 'u': - self._win.attron(curses.A_UNDERLINE) - elif attr_char == 'b': - self._win.attron(curses.A_BOLD) - if (attr_char in string.digits or attr_char == '-') and attr_char != '': - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] - if ',' in color_str: - tup, char = read_tuple(color_str) - self._win.attron(to_curses_attr(tup)) - if char: - if char == 'o': - self._win.attrset(0) - elif char == 'u': - self._win.attron(curses.A_UNDERLINE) - elif char == 'b': - self._win.attron(curses.A_BOLD) - else: - # this will reset previous bold/uderline sequences if any was used - self._win.attroff(curses.A_UNDERLINE) - self._win.attroff(curses.A_BOLD) - elif color_str: - self._win.attron(to_curses_attr((int(color_str), -1))) - text = text[next_attr_char+len(color_str)+2:] - else: - text = text[next_attr_char+2:] - next_attr_char = text.find(FORMAT_CHAR) - self.addstr(text) - - def finish_line(self, color=None): - """ - Write colored spaces until the end of line - """ - (y, x) = self._win.getyx() - size = self.width - x - if color: - self.addnstr(' '*size, size, to_curses_attr(color)) - else: - self.addnstr(' '*size, size) - - @property - def core(self): - if not Win._win_core: - Win._win_core = singleton.Singleton(core.Core) - return Win._win_core - diff --git a/src/windows/bookmark_forms.py b/src/windows/bookmark_forms.py deleted file mode 100644 index de1043c9..00000000 --- a/src/windows/bookmark_forms.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -Windows used inthe bookmarkstab -""" -import curses - -from . import Win -from . inputs import Input -from . data_forms import FieldInput -from theming import to_curses_attr, get_theme -from common import safeJID - -class BookmarkJIDInput(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - jid = safeJID(field.jid) - jid.resource = field.nick or None - self.text = jid.full - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def save(self): - jid = safeJID(self.get_text()) - self._field.jid = jid.bare - self._field.name = jid.bare - self._field.nick = jid.resource - - def get_help_message(self): - return 'Edit the text' - -class BookmarkMethodInput(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.options = ('local', 'remote') - # val_pos is the position of the currently selected option - self.val_pos = self.options.index(field.method) - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.method = self.options[self.val_pos] - - def get_help_message(self): - return '←, →: Select a value amongst the others' - -class BookmarkPasswordInput(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - self.text = field.password or '' - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def rewrite_text(self): - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) - if self.color: - (y, x) = self._win.getyx() - size = self.width-x - self.addnstr(' '*size, size, to_curses_attr(self.color)) - self.addstr(0, self.pos, '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.password = self.get_text() or None - - def get_help_message(self): - return 'Edit the secret text' - -class BookmarkAutojoinWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.last_key = 'KEY_RIGHT' - self.value = field.autojoin - - def do_command(self, key): - if key == 'KEY_LEFT' or key == 'KEY_RIGHT': - self.value = not self.value - self.last_key = key - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - format_string = '←{:^%s}→' % 7 - inp = format_string.format(repr(self.value)) - self.addstr(0, 0, inp) - if self.last_key == 'KEY_RIGHT': - self.move(0, 8) - else: - self.move(0, 0) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def save(self): - self._field.autojoin = self.value - - def get_help_message(self): - return '← and →: change the value between True and False' - - -class BookmarksWin(Win): - def __init__(self, bookmarks, height, width, y, x): - self._win = Win._tab_win.derwin(height, width, y, x) - self.scroll_pos = 0 - self._current_input = 0 - self.current_horizontal_input = 0 - self._bookmarks = list(bookmarks) - self.lines = [] - for bookmark in sorted(self._bookmarks, key=lambda x: x.jid): - self.lines.append((BookmarkJIDInput(bookmark), - BookmarkPasswordInput(bookmark), - BookmarkAutojoinWin(bookmark), - BookmarkMethodInput(bookmark))) - - @property - def current_input(self): - return self._current_input - - @current_input.setter - def current_input(self, value): - if 0 <= self._current_input < len(self.lines): - if 0 <= value < len(self.lines): - self.lines[self._current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self._current_input = value - else: - self._current_input = 0 - - def add_bookmark(self, bookmark): - self.lines.append((BookmarkJIDInput(bookmark), - BookmarkPasswordInput(bookmark), - BookmarkAutojoinWin(bookmark), - BookmarkMethodInput(bookmark))) - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input = 0 - self.current_input = len(self.lines) - 1 - if self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos = self.current_input - self.height + 1 - self.refresh() - - def del_current_bookmark(self): - if self.lines: - bm = self.lines[self.current_input][0]._field - to_delete = self.current_input - self.current_input -= 1 - del self.lines[to_delete] - if self.scroll_pos: - self.scroll_pos -= 1 - self.refresh() - return bm - - def resize(self, height, width, y, x): - self.height = height - self.width = width - self._win = Win._tab_win.derwin(height, width, y, x) - # Adjust the scroll position, if resizing made the window too small - # for the cursor to be visible - while self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - - def go_to_next_line_input(self): - if not self.lines: - return - if self.current_input == len(self.lines) - 1: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input + 1 - self.scroll_pos > self.height-1: - self.current_input += 1 - self.scroll_pos += 1 - self.refresh() - else: - self.current_input += 1 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_line_input(self): - if not self.lines: - return - if self.current_input == 0: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input -= 1 - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input < self.scroll_pos: - self.scroll_pos = self.current_input - self.refresh() - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_next_horizontal_input(self): - if not self.lines: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input += 1 - if self.current_horizontal_input > 3: - self.current_horizontal_input = 0 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_horizontal_input(self): - if not self.lines: - return - if self.current_horizontal_input == 0: - return - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_horizontal_input -= 1 - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - - def on_input(self, key): - if not self.lines: - return - self.lines[self.current_input][self.current_horizontal_input].do_command(key) - - def refresh(self): - # store the cursor status - self._win.erase() - y = - self.scroll_pos - for i in range(len(self.lines)): - self.lines[i][0].resize(1, self.width//3, y + 1, 0) - self.lines[i][1].resize(1, self.width//3, y + 1, self.width//3) - self.lines[i][2].resize(1, self.width//6, y + 1, 2*self.width//3) - self.lines[i][3].resize(1, self.width//6, y + 1, 5*self.width//6) - y += 1 - self._refresh() - for i, inp in enumerate(self.lines): - if i < self.scroll_pos: - continue - if i >= self.height + self.scroll_pos: - break - for j in range(4): - inp[j].refresh() - - if self.lines and self.current_input < self.height-1: - self.lines[self.current_input][self.current_horizontal_input].set_color(get_theme().COLOR_SELECTED_ROW) - self.lines[self.current_input][self.current_horizontal_input].refresh() - if not self.lines: - curses.curs_set(0) - else: - curses.curs_set(1) - - def refresh_current_input(self): - if self.lines: - self.lines[self.current_input][self.current_horizontal_input].refresh() - - def save(self): - for line in self.lines: - for item in line: - item.save() - diff --git a/src/windows/data_forms.py b/src/windows/data_forms.py deleted file mode 100644 index 410648ec..00000000 --- a/src/windows/data_forms.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -Windows used by the DataFormsTab. - -We only need to export the FormWin (which is not a real Win, as it -does not inherit from the Win base class), as it will create the -others when needed. -""" - -from . import Win -from . inputs import Input - -from theming import to_curses_attr, get_theme - -class FieldInput(object): - """ - All input type in a data form should inherite this class, - in addition with windows.Input or any relevant class from the - 'windows' library. - """ - def __init__(self, field): - self._field = field - self.color = get_theme().COLOR_NORMAL_TEXT - - def set_color(self, color): - self.color = color - self.refresh() - - def update_field_value(self, value): - raise NotImplementedError - - def resize(self, height, width, y, x): - self._resize(height, width, y, x) - - def is_dummy(self): - return False - - def reply(self): - """ - Set the correct response value in the field - """ - raise NotImplementedError - - def get_help_message(self): - """ - Should return a string explaining the keys of the input. - Will be displayed at each refresh on a line at the bottom of the tab. - """ - return '' - -class ColoredLabel(Win): - def __init__(self, text): - self.text = text - self.color = get_theme().COLOR_NORMAL_TEXT - Win.__init__(self) - - def resize(self, height, width, y, x): - self._resize(height, width, y, x) - - def set_color(self, color): - self.color = color - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addstr(0, 0, self.text) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - -class DummyInput(FieldInput, Win): - """ - Used for fields that do not require any input ('fixed') - """ - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - - def do_command(self): - return - - def refresh(self): - return - - def is_dummy(self): - return True - -class BooleanWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.last_key = 'KEY_RIGHT' - self.value = bool(field.getValue()) - - def do_command(self, key): - if key == 'KEY_LEFT' or key == 'KEY_RIGHT': - self.value = not self.value - self.last_key = key - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*(8), self.width) - self.addstr(0, 2, "%s"%self.value) - self.addstr(0, 8, '→') - self.addstr(0, 0, '←') - if self.last_key == 'KEY_RIGHT': - self.addstr(0, 8, '') - else: - self.addstr(0, 0, '') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.setAnswer(self.value) - - def get_help_message(self): - return '← and →: change the value between True and False' - -class TextMultiWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - self.options = field.getValue() - if not isinstance(self.options, list): - self.options = self.options.split('\n') if self.options else [] - self.val_pos = 0 - self.edition_input = None - if not isinstance(self.options, list): - if isinstance(self.options, str): - self.options = [self.options] - else: - self.options = [] - self.options.append('') - - def do_command(self, key): - if not self.edition_input: - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - elif key == '^M': - self.edition_input = Input() - self.edition_input.color = self.color - self.edition_input.resize(self.height, self.width, self.y, self.x) - self.edition_input.text = self.options[self.val_pos] - self.edition_input.key_end() - else: - if key == '^M': - self.options[self.val_pos] = self.edition_input.get_text() - if not self.options[self.val_pos] and self.val_pos != len(self.options) -1: - del self.options[self.val_pos] - if self.val_pos == len(self.options) -1: - self.val_pos -= 1 - self.edition_input = None - if not self.options or self.options[-1] != '': - self.options.append('') - else: - self.edition_input.do_command(key) - self.refresh() - - def refresh(self): - if not self.edition_input: - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - else: - self.edition_input.refresh() - - def reply(self): - values = [val for val in self.options if val] - self._field.setAnswer(values) - - def get_help_message(self): - if not self.edition_input: - help_msg = '← and →: browse the available entries. ' - if self.val_pos == len(self.options)-1: - help_msg += 'Enter: add an entry' - else: - help_msg += 'Enter: edit this entry' - else: - help_msg = 'Enter: finish editing this entry.' - return help_msg - -class ListMultiWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - values = field.getValue() or [] - self.options = [[option, True if option['value'] in values else False]\ - for option in field.get_options()] - self.val_pos = 0 - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - elif key == ' ': - self.options[self.val_pos][1] = not self.options[self.val_pos][1] - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos] - self.addstr(0, self.width//2-len(option)//2, option[0]['label']) - self.addstr(0, 2, '✔' if option[1] else '☐') - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.delOptions() - values = [option[0]['value'] for option in self.options if option[1] is True] - self._field.setAnswer(values) - - def get_help_message(self): - return '←, →: Switch between the value. Space: select or unselect a value' - -class ListSingleWin(FieldInput, Win): - def __init__(self, field): - FieldInput.__init__(self, field) - Win.__init__(self) - # the option list never changes - self.options = field.getOptions() - # val_pos is the position of the currently selected option - self.val_pos = 0 - for i, option in enumerate(self.options): - if field.getValue() == option['value']: - self.val_pos = i - - def do_command(self, key): - if key == 'KEY_LEFT': - if self.val_pos > 0: - self.val_pos -= 1 - elif key == 'KEY_RIGHT': - if self.val_pos < len(self.options)-1: - self.val_pos += 1 - else: - return - self.refresh() - - def refresh(self): - self._win.erase() - self._win.attron(to_curses_attr(self.color)) - self.addnstr(0, 0, ' '*self.width, self.width) - if self.val_pos > 0: - self.addstr(0, 0, '←') - if self.val_pos < len(self.options)-1: - self.addstr(0, self.width-1, '→') - if self.options: - option = self.options[self.val_pos]['label'] - self.addstr(0, self.width//2-len(option)//2, option) - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def reply(self): - self._field['label'] = '' - self._field.delOptions() - self._field.setAnswer(self.options[self.val_pos]['value']) - - def get_help_message(self): - return '←, →: Select a value amongst the others' - -class TextSingleWin(FieldInput, Input): - def __init__(self, field): - FieldInput.__init__(self, field) - Input.__init__(self) - self.text = field.getValue() if isinstance(field.getValue(), str)\ - else "" - self.pos = len(self.text) - self.color = get_theme().COLOR_NORMAL_TEXT - - def reply(self): - self._field['label'] = '' - self._field.setAnswer(self.get_text()) - - def get_help_message(self): - return 'Edit the text' - -class TextPrivateWin(TextSingleWin): - def __init__(self, field): - TextSingleWin.__init__(self, field) - - def rewrite_text(self): - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - self.addstr('*'*len(self.text[self.view_pos:self.view_pos+self.width-1])) - if self.color: - (y, x) = self._win.getyx() - size = self.width-x - self.addnstr(' '*size, size, to_curses_attr(self.color)) - self.addstr(0, self.pos, '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - self._refresh() - - def get_help_message(self): - return 'Edit the secret text' - -class FormWin(object): - """ - A window, with some subwins (the various inputs). - On init, create all the subwins. - On resize, move and resize all the subwin and define how the text will be written - On refresh, write all the text, and refresh all the subwins - """ - input_classes = {'boolean': BooleanWin, - 'fixed': DummyInput, - 'jid-multi': TextMultiWin, - 'jid-single': TextSingleWin, - 'list-multi': ListMultiWin, - 'list-single': ListSingleWin, - 'text-multi': TextMultiWin, - 'text-private': TextPrivateWin, - 'text-single': TextSingleWin, - } - def __init__(self, form, height, width, y, x): - self._form = form - self._win = Win._tab_win.derwin(height, width, y, x) - self.scroll_pos = 0 - self.current_input = 0 - self.inputs = [] # dict list - for (name, field) in self._form.getFields().items(): - if field['type'] == 'hidden': - continue - try: - input_class = self.input_classes[field['type']] - except IndexError: - continue - label = field['label'] - desc = field['desc'] - if field['type'] == 'fixed': - label = field.getValue() - inp = input_class(field) - self.inputs.append({'label':ColoredLabel(label), - 'description': desc, - 'input':inp}) - - def resize(self, height, width, y, x): - self.height = height - self.width = width - self._win = Win._tab_win.derwin(height, width, y, x) - # Adjust the scroll position, if resizing made the window too small - # for the cursor to be visible - while self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - - def reply(self): - """ - Set the response values in the form, for each field - from the corresponding input - """ - for inp in self.inputs: - if inp['input'].is_dummy(): - continue - else: - inp['input'].reply() - self._form['title'] = '' - self._form['instructions'] = '' - - def go_to_next_input(self): - if not self.inputs: - return - if self.current_input == len(self.inputs) - 1: - return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input += 1 - jump = 0 - while self.current_input+jump != len(self.inputs) - 1 and self.inputs[self.current_input+jump]['input'].is_dummy(): - jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): - return - self.current_input += jump - # If moving made the current input out of the visible screen, we - # adjust the scroll position and we redraw the whole thing. We don’t - # call refresh() if this is not the case, because - # refresh_current_input() is always called anyway, so this is not - # needed - if self.current_input - self.scroll_pos > self.height-1: - self.scroll_pos += 1 - self.refresh() - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - - def go_to_previous_input(self): - if not self.inputs: - return - if self.current_input == 0: - return - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_NORMAL_TEXT) - self.current_input -= 1 - jump = 0 - while self.current_input-jump > 0 and self.inputs[self.current_input+jump]['input'].is_dummy(): - jump += 1 - if self.inputs[self.current_input+jump]['input'].is_dummy(): - return - # Adjust the scroll position if the current_input would be outside - # of the visible area - if self.current_input < self.scroll_pos: - self.scroll_pos = self.current_input - self.refresh() - self.current_input -= jump - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - - def on_input(self, key): - if not self.inputs: - return - self.inputs[self.current_input]['input'].do_command(key) - - def refresh(self): - self._win.erase() - y = -self.scroll_pos - i = 0 - for name, field in self._form.getFields().items(): - if field['type'] == 'hidden': - continue - self.inputs[i]['label'].resize(1, self.width//2, y + 1, 0) - self.inputs[i]['input'].resize(1, self.width//2, y+1, self.width//2) - # TODO: display the field description - y += 1 - i += 1 - self._win.refresh() - for i, inp in enumerate(self.inputs): - if i < self.scroll_pos: - continue - if i >= self.height + self.scroll_pos: - break - inp['label'].refresh() - inp['input'].refresh() - inp['label'].refresh() - if self.inputs and self.current_input < self.height-1: - self.inputs[self.current_input]['input'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['input'].refresh() - self.inputs[self.current_input]['label'].set_color(get_theme().COLOR_SELECTED_ROW) - self.inputs[self.current_input]['label'].refresh() - - def refresh_current_input(self): - self.inputs[self.current_input]['input'].refresh() - - def get_help_message(self): - if self.inputs and self.current_input < self.height-1 and self.inputs[self.current_input]['input']: - return self.inputs[self.current_input]['input'].get_help_message() - return '' - diff --git a/src/windows/funcs.py b/src/windows/funcs.py deleted file mode 100644 index f1401628..00000000 --- a/src/windows/funcs.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Standalone functions used by the modules -""" - -import string - -from . base_wins import FORMAT_CHAR, format_chars - -def find_first_format_char(text, chars=None): - if chars is None: - chars = format_chars - pos = -1 - for char in chars: - p = text.find(char) - if p == -1: - continue - if pos == -1 or p < pos: - pos = p - return pos - -def truncate_nick(nick, size=10): - if size < 1: - size = 1 - if nick and len(nick) > size: - return nick[:size]+'…' - return nick - -def parse_attrs(text, previous=None): - next_attr_char = text.find(FORMAT_CHAR) - if previous: - attrs = previous - else: - attrs = [] - while next_attr_char != -1 and text: - if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char+1].lower() - else: - attr_char = str() - if attr_char == 'o': - attrs = [] - elif attr_char == 'u': - attrs.append('u') - elif attr_char == 'b': - attrs.append('b') - if attr_char in string.digits and attr_char != '': - color_str = text[next_attr_char+1:text.find('}', next_attr_char)] - if color_str: - attrs.append(color_str + '}') - text = text[next_attr_char+len(color_str)+2:] - else: - text = text[next_attr_char+2:] - next_attr_char = text.find(FORMAT_CHAR) - return attrs - diff --git a/src/windows/info_bar.py b/src/windows/info_bar.py deleted file mode 100644 index abd956cd..00000000 --- a/src/windows/info_bar.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Module defining the global info bar - -This window is the one listing the current opened tabs in poezio. -The GlobalInfoBar can be either horizontal or vertical -(VerticalGlobalInfoBar). -""" -import logging -log = logging.getLogger(__name__) - -import curses - - -from config import config -from . import Win -from theming import get_theme, to_curses_attr - -class GlobalInfoBar(Win): - def __init__(self): - Win.__init__(self) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - create_gaps = config.get('create_gaps') - show_names = config.get('show_tab_names') - show_nums = config.get('show_tab_numbers') - use_nicks = config.get('use_tab_nicks') - show_inactive = config.get('show_inactive_tabs') - # ignore any remaining gap tabs if the feature is not enabled - if create_gaps: - sorted_tabs = self.core.tabs[:] - else: - sorted_tabs = [tab for tab in self.core.tabs if tab] - - for nb, tab in enumerate(sorted_tabs): - if not tab: continue - color = tab.color - if not show_inactive and color is get_theme().COLOR_TAB_NORMAL: - continue - try: - if show_nums or not show_names: - self.addstr("%s" % str(nb), to_curses_attr(color)) - if show_names: - self.addstr(' ', to_curses_attr(color)) - if show_names: - if use_nicks: - self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color)) - else: - self.addstr("%s" % tab.name, to_curses_attr(color)) - self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - except: # end of line - break - (y, x) = self._win.getyx() - self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - (y, x) = self._win.getyx() - remaining_size = self.width - x - self.addnstr(' '*remaining_size, remaining_size, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self._refresh() - -class VerticalGlobalInfoBar(Win): - def __init__(self, scr): - Win.__init__(self) - self._win = scr - - def refresh(self): - height, width = self._win.getmaxyx() - self._win.erase() - sorted_tabs = [tab for tab in self.core.tabs if tab] - if not config.get('show_inactive_tabs'): - sorted_tabs = [tab for tab in sorted_tabs if\ - tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] - nb_tabs = len(sorted_tabs) - use_nicks = config.get('use_tab_nicks') - if nb_tabs >= height: - for y, tab in enumerate(sorted_tabs): - if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT: - pos = y - break - # center the current tab as much as possible - if pos < height//2: - sorted_tabs = sorted_tabs[:height] - elif nb_tabs - pos <= height//2: - sorted_tabs = sorted_tabs[-height:] - else: - sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2] - asc_sort = (config.get('vertical_tab_list_sort') == 'asc') - for y, tab in enumerate(sorted_tabs): - color = tab.vertical_color - if asc_sort: - y = height - y - 1 - self.addstr(y, 0, "%2d" % tab.nb, - to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER)) - self.addstr('.') - if use_nicks: - self.addnstr("%s" % tab.get_nick(), width - 4, to_curses_attr(color)) - else: - self.addnstr("%s" % tab.name, width - 4, to_curses_attr(color)) - separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) - self._win.attron(separator) - self._win.vline(0, width-1, curses.ACS_VLINE, height) - self._win.attroff(separator) - self._refresh() diff --git a/src/windows/info_wins.py b/src/windows/info_wins.py deleted file mode 100644 index f6aebd35..00000000 --- a/src/windows/info_wins.py +++ /dev/null @@ -1,311 +0,0 @@ -""" -Module defining all the "info wins", ie the bar which is on top of the -info buffer in normal tabs -""" - -import logging -log = logging.getLogger(__name__) - -from common import safeJID -from config import config - -from . import Win -from . funcs import truncate_nick -from theming import get_theme, to_curses_attr - -class InfoWin(Win): - """ - Base class for all the *InfoWin, used in various tabs. For example - MucInfoWin, etc. Provides some useful methods. - """ - def __init__(self): - Win.__init__(self) - - def print_scroll_position(self, window): - """ - Print, like in Weechat, a -MORE(n)- where n - is the number of available lines to scroll - down - """ - if window.pos > 0: - plus = ' -MORE(%s)-' % window.pos - self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER)) - -class XMLInfoWin(InfoWin): - """ - Info about the latest xml filter used and the state of the buffer. - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, filter_t='', filter='', window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - bar = to_curses_attr(get_theme().COLOR_INFORMATION_BAR) - if not filter_t: - self.addstr('[No filter]', bar) - else: - info = '[%s] %s' % (filter_t, filter) - self.addstr(info, bar) - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - -class PrivateInfoWin(InfoWin): - """ - The line above the information window, displaying informations - about the MUC user we are talking to - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, name, window, chatstate, informations): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_room_name(name) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, name) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_additional_informations(self, informations, jid): - """ - Write all informations added by plugins by getting the - value returned by the callbacks. - """ - for key in informations: - self.addstr(informations[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_room_name(self, name): - jid = safeJID(name) - room_name, nick = jid.bare, jid.resource - self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME)) - txt = ' from room %s' % room_name - self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_chatstate(self, state): - if state: - self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class MucListInfoWin(InfoWin): - """ - The live above the information window, displaying informations - about the muc server being listed - """ - def __init__(self, message=''): - InfoWin.__init__(self) - self.message = message - - def refresh(self, name=None, window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - if name: - self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - else: - self.addstr(self.message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - if window: - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - -class ConversationInfoWin(InfoWin): - """ - The line above the information window, displaying informations - about the user we are talking to - """ - - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, jid, contact, window, chatstate, informations): - # contact can be None, if we receive a message - # from someone not in our roster. In this case, we display - # only the maximum information from the message we can get. - log.debug('Refresh: %s', self.__class__.__name__) - jid = safeJID(jid) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - # if contact is None, then resource is None too: - # user is not in the roster so we know almost nothing about it - # If contact is a Contact, then - # resource can now be a Resource: user is in the roster and online - # or resource is None: user is in the roster but offline - self._win.erase() - if config.get('show_jid_in_conversations'): - self.write_contact_jid(jid) - self.write_contact_informations(contact) - self.write_resource_information(resource) - self.print_scroll_position(window) - self.write_chatstate(chatstate) - self.write_additional_informations(informations, jid) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_additional_informations(self, informations, jid): - """ - Write all informations added by plugins by getting the - value returned by the callbacks. - """ - for key in informations: - self.addstr(informations[key](jid), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_resource_information(self, resource): - """ - Write the informations about the resource - """ - if not resource: - presence = "unavailable" - else: - presence = resource.presence - color = get_theme().color_show(presence) - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color)) - self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_contact_informations(self, contact): - """ - Write the informations about the contact - """ - if not contact: - self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - return - display_name = contact.name - if display_name: - self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_contact_jid(self, jid): - """ - Just write the jid that we are talking to - """ - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(jid.full, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) - self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_chatstate(self, state): - if state: - self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class DynamicConversationInfoWin(ConversationInfoWin): - def write_contact_jid(self, jid): - """ - Just displays the resource in an other color - """ - log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s", - jid.resource) - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) - if jid.resource: - self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE)) - self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class MucInfoWin(InfoWin): - """ - The line just above the information window, displaying informations - about the MUC we are viewing - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, room, window=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_room_name(room) - self.write_participants_number(room) - self.write_own_nick(room) - self.write_disconnected(room) - self.write_role(room) - if window: - self.print_scroll_position(window) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_room_name(self, room): - self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) - self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_participants_number(self, room): - self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.addstr(str(len(room.users)), to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME)) - self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_disconnected(self, room): - """ - Shows a message if the room is not joined - """ - if not room.joined: - self.addstr(' -!- Not connected ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_own_nick(self, room): - """ - Write our own nick in the info bar - """ - nick = room.own_nick - if not nick: - return - self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - - def write_role(self, room): - """ - Write our own role and affiliation - """ - own_user = None - for user in room.users: - if user.nick == room.own_nick: - own_user = user - break - if not own_user: - return - txt = ' (' - if own_user.affiliation != 'none': - txt += own_user.affiliation+', ' - txt += own_user.role+')' - self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class ConversationStatusMessageWin(InfoWin): - """ - The upper bar displaying the status message of the contact - """ - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, jid, contact): - log.debug('Refresh: %s', self.__class__.__name__) - jid = safeJID(jid) - if contact: - if jid.resource: - resource = contact[jid.full] - else: - resource = contact.get_highest_priority_resource() - else: - resource = None - self._win.erase() - if resource: - self.write_status_message(resource) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_status_message(self, resource): - self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - -class BookmarksInfoWin(InfoWin): - def __init__(self): - InfoWin.__init__(self) - - def refresh(self, preferred): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - self.write_remote_status(preferred) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def write_remote_status(self, preferred): - self.addstr('Remote storage: %s' % preferred, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - diff --git a/src/windows/input_placeholders.py b/src/windows/input_placeholders.py deleted file mode 100644 index 496417d1..00000000 --- a/src/windows/input_placeholders.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Classes used to replace the input in some tabs or special situations, -but which are not inputs. -""" - -import logging -log = logging.getLogger(__name__) - - -from . import Win -from theming import get_theme, to_curses_attr - - -class HelpText(Win): - """ - A Window just displaying a read-only message. - Usually used to replace an Input when the tab is in - command mode. - """ - def __init__(self, text=''): - Win.__init__(self) - self.txt = text - - def refresh(self, txt=None): - log.debug('Refresh: %s', self.__class__.__name__) - if txt: - self.txt = txt - self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - self._refresh() - - def do_command(self, key, raw=False): - return False - - def on_delete(self): - return - -class YesNoInput(Win): - """ - A Window just displaying a Yes/No input - Used to ask a confirmation - """ - def __init__(self, text='', callback=None): - Win.__init__(self) - self.key_func = { - 'y' : self.on_yes, - 'n' : self.on_no, - } - self.txt = text - self.value = None - self.callback = callback - - def on_yes(self): - self.value = True - - def on_no(self): - self.value = False - - def refresh(self, txt=None): - log.debug('Refresh: %s', self.__class__.__name__) - if txt: - self.txt = txt - self._win.erase() - self.addstr(0, 0, self.txt[:self.width-1], to_curses_attr(get_theme().COLOR_WARNING_PROMPT)) - self.finish_line(get_theme().COLOR_WARNING_PROMPT) - self._refresh() - - def do_command(self, key, raw=False): - if key.lower() in self.key_func: - self.key_func[key]() - if self.value is not None and self.callback is not None: - return self.callback() - - def on_delete(self): - return - diff --git a/src/windows/inputs.py b/src/windows/inputs.py deleted file mode 100644 index 80f0c900..00000000 --- a/src/windows/inputs.py +++ /dev/null @@ -1,768 +0,0 @@ -""" -Text inputs. -""" - -import logging -log = logging.getLogger(__name__) - -import curses -import string - -import keyboard -import common -import poopt -from . import Win -from . base_wins import format_chars -from . funcs import find_first_format_char -from config import config -from theming import to_curses_attr - - -class Input(Win): - """ - The simplest Input possible, provides just a way to edit a single line - of text. It also has a clipboard, common to all Inputs. - Doesn't have any history. - It doesn't do anything when enter is pressed either. - This should be herited for all kinds of Inputs, for example MessageInput - or the little inputs in dataforms, etc, adding specific features (completion etc) - It features two kinds of completion, but they have to be called from outside (the Tab), - passing the list of items that can be used to complete. The completion can be used - in a very flexible way. - """ - text_attributes = ['b', 'o', 'u', '1', '2', '3', '4', '5', '6', '7', 't'] - clipboard = '' # A common clipboard for all the inputs, this makes - # it easy cut and paste text between various input - def __init__(self): - self.key_func = { - "KEY_LEFT": self.key_left, - "KEY_RIGHT": self.key_right, - "KEY_END": self.key_end, - "KEY_HOME": self.key_home, - "KEY_DC": self.key_dc, - '^D': self.key_dc, - 'M-b': self.jump_word_left, - "M-[1;5D": self.jump_word_left, - "kRIT5": self.jump_word_right, - "kLFT5": self.jump_word_left, - '^W': self.delete_word, - 'M-d': self.delete_next_word, - '^K': self.delete_end_of_line, - '^U': self.delete_beginning_of_line, - '^Y': self.paste_clipboard, - '^A': self.key_home, - '^E': self.key_end, - 'M-f': self.jump_word_right, - "M-[1;5C": self.jump_word_right, - "KEY_BACKSPACE": self.key_backspace, - "M-KEY_BACKSPACE": self.delete_word, - '^?': self.key_backspace, - "M-^?": self.delete_word, - # '^J': self.add_line_break, - } - Win.__init__(self) - self.text = '' - self.pos = 0 # The position of the “cursor” in the text - # (not only in the view) - self.view_pos = 0 # The position (in the text) of the - # first character displayed on the - # screen - self.on_input = None # callback called on any key pressed - self.color = None # use this color on addstr - - def on_delete(self): - """ - Remove all references kept to a tab, so that the tab - can be garbage collected - """ - del self.key_func - - def set_color(self, color): - self.color = color - self.rewrite_text() - - def is_empty(self): - if self.text: - return False - return True - - def is_cursor_at_end(self): - """ - Whether or not the cursor is at the end of the text. - """ - assert len(self.text) >= self.pos - if len(self.text) == self.pos: - return True - return False - - def jump_word_left(self): - """ - Move the cursor one word to the left - """ - if self.pos == 0: - return True - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: - self.key_left() - while self.pos > 0 and self.text[self.pos-1] not in separators: - self.key_left() - return True - - def jump_word_right(self): - """ - Move the cursor one word to the right - """ - if self.is_cursor_at_end(): - return True - separators = string.punctuation+' ' - while not self.is_cursor_at_end() and self.text[self.pos] in separators: - self.key_right() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: - self.key_right() - return True - - def delete_word(self): - """ - Delete the word just before the cursor - """ - separators = string.punctuation+' ' - while self.pos > 0 and self.text[self.pos-1] in separators: - self.key_backspace() - while self.pos > 0 and self.text[self.pos-1] not in separators: - self.key_backspace() - return True - - def delete_next_word(self): - """ - Delete the word just after the cursor - """ - separators = string.punctuation+' ' - while not self.is_cursor_at_end() and self.text[self.pos] in separators: - self.key_dc() - while not self.is_cursor_at_end() and self.text[self.pos] not in separators: - self.key_dc() - return True - - def delete_end_of_line(self): - """ - Cut the text from cursor to the end of line - """ - if self.is_cursor_at_end(): - return False - Input.clipboard = self.text[self.pos:] - self.text = self.text[:self.pos] - self.key_end() - return True - - def delete_beginning_of_line(self): - """ - Cut the text from cursor to the beginning of line - """ - if self.pos == 0: - return True - Input.clipboard = self.text[:self.pos] - self.text = self.text[self.pos:] - self.key_home() - return True - - def paste_clipboard(self): - """ - Insert what is in the clipboard at the cursor position - """ - if not Input.clipboard: - return True - for letter in Input.clipboard: - self.do_command(letter, False) - self.rewrite_text() - return True - - def key_dc(self): - """ - delete char just after the cursor - """ - self.reset_completion() - if self.is_cursor_at_end(): - return True # end of line, nothing to delete - self.text = self.text[:self.pos]+self.text[self.pos+1:] - self.rewrite_text() - return True - - def key_home(self): - """ - Go to the beginning of line - """ - self.reset_completion() - self.pos = 0 - self.rewrite_text() - return True - - def key_end(self, reset=False): - """ - Go to the end of line - """ - if reset: - self.reset_completion() - self.pos = len(self.text) - assert self.is_cursor_at_end() - self.rewrite_text() - return True - - def key_left(self, jump=True, reset=True): - """ - Move the cursor one char to the left - """ - if reset: - self.reset_completion() - if self.pos == 0: - return True - self.pos -= 1 - if reset: - self.rewrite_text() - return True - - def key_right(self, jump=True, reset=True): - """ - Move the cursor one char to the right - """ - if reset: - self.reset_completion() - if self.is_cursor_at_end(): - return True - self.pos += 1 - if reset: - self.rewrite_text() - return True - - def key_backspace(self, reset=True): - """ - Delete the char just before the cursor - """ - self.reset_completion() - if self.pos == 0: - return - self.key_left() - self.key_dc() - return True - - def auto_completion(self, word_list, add_after='', quotify=True): - """ - Complete the input, from a list of words - if add_after is None, we use the value defined in completion - plus a space, after the completion. If it's a string, we use it after the - completion (with no additional space) - """ - if quotify: - for i, word in enumerate(word_list[:]): - word_list[i] = '"' + word + '"' - self.normal_completion(word_list, add_after) - return True - - def new_completion(self, word_list, argument_position=-1, add_after='', quotify=True, override=False): - """ - Complete the argument at position ``argument_postion`` in the input. - If ``quotify`` is ``True``, then the completion will operate on block of words - (e.g. "toto titi") whereas if it is ``False``, it will operate on words (e.g - "toto", "titi"). - - The completions may modify other parts of the input when completing an argument, - for example removing useless double quotes around single-words, or setting the - space between each argument to only one space. - - The case where we complete the first argument is special, because we complete - the command, and we do not want to modify anything else in the input. - - This method is the one that should be used if the command being completed - has several arguments. - """ - if argument_position == 0: - self._new_completion_first(word_list) - else: - self._new_completion_args(word_list, argument_position, add_after, quotify, override) - self.rewrite_text() - return True - - def _new_completion_args(self, word_list, argument_position=-1, add_after='', quoted=True, override=False): - """ - Case for completing arguments with position ≠ 0 - """ - if quoted: - words = common.shell_split(self.text) - else: - words = self.text.split() - if argument_position >= len(words): - current = '' - else: - current = words[argument_position] - - if quoted: - split_words = words[1:] - words = [words[0]] - for word in split_words: - if ' ' in word or '\\' in word: - words.append('"' + word + '"') - else: - words.append(word) - current_l = current.lower() - if self.last_completion is not None: - self.hit_list.append(self.hit_list.pop(0)) - else: - if override: - hit_list = word_list - else: - hit_list = [] - for word in word_list: - if word.lower().startswith(current_l): - hit_list.append(word) - if not hit_list: - return - self.hit_list = hit_list - - if argument_position >= len(words): - if quoted and ' ' in self.hit_list[0]: - words.append('"'+self.hit_list[0]+'"') - else: - words.append(self.hit_list[0]) - else: - if quoted and ' ' in self.hit_list[0]: - words[argument_position] = '"'+self.hit_list[0]+'"' - else: - words[argument_position] = self.hit_list[0] - - new_pos = -1 - for i, word in enumerate(words): - if argument_position >= i: - new_pos += len(word) + 1 - - self.last_completion = self.hit_list[0] - self.text = words[0] + ' ' + ' '.join(words[1:]) - self.pos = new_pos - - def _new_completion_first(self, word_list): - """ - Special case of completing the command itself: - we don’t want to change anything to the input doing that - """ - space_pos = self.text.find(' ') - if space_pos != -1: - current, follow = self.text[:space_pos], self.text[space_pos:] - else: - current, follow = self.text, '' - - if self.last_completion: - self.hit_list.append(self.hit_list.pop(0)) - else: - hit_list = [] - for word in word_list: - if word.lower().startswith(current): - hit_list.append(word) - if not hit_list: - return - self.hit_list = hit_list - - self.last_completion = self.hit_list[0] - self.text = self.hit_list[0] + follow - self.pos = len(self.hit_list[0]) - - def get_argument_position(self, quoted=True): - """ - Get the argument number at the current position - """ - command_stop = self.text.find(' ') - if command_stop == -1 or self.pos <= command_stop: - return 0 - text = self.text[command_stop+1:] - pos = self.pos - len(self.text) + len(text) - 1 - val = common.find_argument(pos, text, quoted=quoted) + 1 - return val - - def reset_completion(self): - """ - Reset the completion list (called on ALL keys except tab) - """ - self.hit_list = [] - self.last_completion = None - - def normal_completion(self, word_list, after): - """ - Normal completion - """ - pos = self.pos - if pos < len(self.text) and after.endswith(' ') and self.text[pos] == ' ': - after = after[:-1] # remove the last space if we are already on a space - if not self.last_completion: - space_before_cursor = self.text.rfind(' ', 0, pos) - if space_before_cursor != -1: - begin = self.text[space_before_cursor+1:pos] - else: - begin = self.text[:pos] - hit_list = [] # list of matching hits - for word in word_list: - if word.lower().startswith(begin.lower()): - hit_list.append(word) - elif word.startswith('"') and word.lower()[1:].startswith(begin.lower()): - hit_list.append(word) - if len(hit_list) == 0: - return - self.hit_list = hit_list - end = len(begin) - else: - begin = self.last_completion - end = len(begin) + len(after) - self.hit_list.append(self.hit_list.pop(0)) # rotate list - - self.text = self.text[:pos-end] + self.text[pos:] - pos -= end - hit = self.hit_list[0] # take the first hit - self.text = self.text[:pos] + hit + after + self.text[pos:] - for _ in range(end): - try: - self.key_left(reset=False) - except: - pass - for _ in range(len(hit) + len(after)): - self.key_right(reset=False) - - self.rewrite_text() - self.last_completion = hit - - def do_command(self, key, reset=True, raw=False): - if key in self.key_func: - res = self.key_func[key]() - if not raw and self.on_input: - self.on_input(self.get_text()) - return res - if not raw and (not key or len(key) > 1): - return False # ignore non-handled keyboard shortcuts - if reset: - self.reset_completion() - # Insert the char at the cursor position - self.text = self.text[:self.pos]+key+self.text[self.pos:] - self.pos += len(key) - if reset: - self.rewrite_text() - if self.on_input: - self.on_input(self.get_text()) - - return True - - def add_line_break(self): - """ - Add a (real) \n to the line - """ - self.do_command('\n') - - def get_text(self): - """ - Return the text entered so far - """ - return self.text - - def addstr_colored_lite(self, text, y=None, x=None): - """ - Just like addstr_colored, with the single-char attributes - (\x0E to \x19 instead of \x19 + attr). We do not use any } - char in this version - """ - chars = format_chars[:] - chars.append('\n') - if y is not None and x is not None: - self.move(y, x) - format_char = find_first_format_char(text, chars) - while format_char != -1: - if text[format_char] == '\n': - attr_char = '|' - else: - attr_char = self.text_attributes[ - format_chars.index(text[format_char])] - self.addstr(text[:format_char]) - self.addstr(attr_char, curses.A_REVERSE) - text = text[format_char+1:] - if attr_char == 'o': - self._win.attrset(0) - elif attr_char == 'u': - self._win.attron(curses.A_UNDERLINE) - elif attr_char == 'b': - self._win.attron(curses.A_BOLD) - elif attr_char in string.digits and attr_char != '': - self._win.attron(to_curses_attr((int(attr_char), -1))) - format_char = find_first_format_char(text, chars) - self.addstr(text) - - def rewrite_text(self): - """ - Refresh the line onscreen, but first, always adjust the - view_pos. Also, each FORMAT_CHAR+attr_char count only take - one screen column (this is done in addstr_colored_lite), we - have to do some special calculations to find the correct - length of text to display, and the position of the cursor. - """ - self.adjust_view_pos() - text = self.text - self._win.erase() - if self.color: - self._win.attron(to_curses_attr(self.color)) - displayed_text = text[self.view_pos:self.view_pos+self.width-1].replace('\t', '\x18') - self._win.attrset(0) - self.addstr_colored_lite(displayed_text) - # Fill the rest of the line with the input color - if self.color: - (_, x) = self._win.getyx() - size = self.width - x - self.addnstr(' ' * size, size, to_curses_attr(self.color)) - self.addstr(0, - poopt.wcswidth(displayed_text[:self.pos-self.view_pos]), '') - if self.color: - self._win.attroff(to_curses_attr(self.color)) - curses.curs_set(1) - self._refresh() - - def adjust_view_pos(self): - """ - Adjust the position of the View, if needed (for example if the - cursor moved and would now be out of the view, we adapt the - view_pos so that we can always see our cursor) - """ - # start of the input - if self.pos == 0: - self.view_pos = 0 - return - # cursor outside of the screen (left) - if self.pos <= self.view_pos: - self.view_pos = self.pos - max(1 * self.width // 3, 1) - # cursor outside of the screen (right) - elif self.pos >= self.view_pos + self.width - 1: - self.view_pos = self.pos - max(2 * self.width // 3, 2) - - if self.view_pos < 0: - self.view_pos = 0 - - # text small enough to fit inside the window entirely: - # remove scrolling if present - if poopt.wcswidth(self.text) < self.width: - self.view_pos = 0 - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self.rewrite_text() - - def clear_text(self): - self.text = '' - self.pos = 0 - self.rewrite_text() - - def key_enter(self): - txt = self.get_text() - self.clear_text() - return txt - -class HistoryInput(Input): - """ - An input with colors and stuff, plus an history - ^R allows to search inside the history (as in a shell) - """ - history = list() - - def __init__(self): - Input.__init__(self) - self.help_message = '' - self.current_completed = '' - self.key_func['^R'] = self.toggle_search - self.search = False - if config.get('separate_history'): - self.history = list() - - def toggle_search(self): - if self.help_message: - return - self.search = not self.search - self.refresh() - - def update_completed(self): - """ - Find a match for the current text - """ - if not self.text: - return - for i in self.history: - if self.text in i: - self.current_completed = i - return - self.current_completed = '' - - def history_enter(self): - """ - Enter was pressed, set the text to the - current completion and disable history - search - """ - if self.search: - self.search = False - if self.current_completed: - self.text = self.current_completed - self.current_completed = '' - self.refresh() - return True - self.refresh() - return False - - def key_up(self): - """ - Get the previous line in the history - """ - self.reset_completion() - if self.histo_pos == -1 and self.get_text(): - if not self.history or self.history[0] != self.get_text(): - # add the message to history, we do not want to lose it - self.history.insert(0, self.get_text()) - self.histo_pos += 1 - if self.histo_pos < len(self.history) - 1: - self.histo_pos += 1 - self.text = self.history[self.histo_pos] - self.key_end() - return True - - def key_down(self): - """ - Get the next line in the history - """ - self.reset_completion() - if self.histo_pos > 0: - self.histo_pos -= 1 - self.text = self.history[self.histo_pos] - elif self.histo_pos <= 0 and self.get_text(): - if not self.history or self.history[0] != self.get_text(): - # add the message to history, we do not want to lose it - self.history.insert(0, self.get_text()) - self.text = '' - self.histo_pos = -1 - self.key_end() - return True - -class MessageInput(HistoryInput): - """ - The input featuring history and that is being used in - Conversation, Muc and Private tabs - Also letting the user enter colors or other text markups - """ - history = list() # The history is common to all MessageInput - - def __init__(self): - HistoryInput.__init__(self) - self.last_completion = None - self.histo_pos = -1 - self.key_func["KEY_UP"] = self.key_up - self.key_func["M-A"] = self.key_up - self.key_func["KEY_DOWN"] = self.key_down - self.key_func["M-B"] = self.key_down - self.key_func['^C'] = self.enter_attrib - - def enter_attrib(self): - """ - Read one more char (c), add the corresponding char from formats_char to the text string - """ - def cb(attr_char): - if attr_char in self.text_attributes: - char = format_chars[self.text_attributes.index(attr_char)] - self.do_command(char, False) - self.rewrite_text() - keyboard.continuation_keys_callback = cb - - def key_enter(self): - if self.history_enter(): - return - - txt = self.get_text() - if len(txt) != 0: - if not self.history or self.history[0] != txt: - # add the message to history, but avoid duplicates - self.history.insert(0, txt) - self.histo_pos = -1 - self.clear_text() - return txt - -class CommandInput(HistoryInput): - """ - An input with an help message in the left, with three given callbacks: - one when when successfully 'execute' the command and when we abort it. - The last callback is optional and is called on any input key - This input is used, for example, in the RosterTab when, to replace the - HelpMessage when a command is started - The on_input callback - """ - history = list() - - def __init__(self, help_message, on_abort, on_success, on_input=None): - HistoryInput.__init__(self) - self.on_abort = on_abort - self.on_success = on_success - self.on_input = on_input - self.help_message = help_message - self.key_func['^M'] = self.success - self.key_func['^G'] = self.abort - self.key_func['^C'] = self.abort - self.key_func["KEY_UP"] = self.key_up - self.key_func["M-A"] = self.key_up - self.key_func["KEY_DOWN"] = self.key_down - self.key_func["M-B"] = self.key_down - self.histo_pos = -1 - - def do_command(self, key, refresh=True, raw=False): - res = Input.do_command(self, key, refresh, raw) - if self.on_input: - self.on_input(self.get_text()) - return res - - def disable_history(self): - """ - Disable the history (up/down) keys - """ - if 'KEY_UP' in self.key_func: - del self.key_func['KEY_UP'] - if 'KEY_DOWN' in self.key_func: - del self.key_func['KEY_DOWN'] - - @property - def history_disabled(self): - return 'KEY_UP' not in self.key_func and 'KEY_DOWN' not in self.key_func - - def success(self): - """ - call the success callback, passing the text as argument - """ - self.on_input = None - if self.search: - self.history_enter() - res = self.on_success(self.get_text()) - return res - - def abort(self): - """ - Call the abort callback, passing the text as argument - """ - self.on_input = None - return self.on_abort(self.get_text()) - - def on_delete(self): - """ - SERIOUSLY BIG WTF. - - I can do - self.key_func.clear() - - but not - del self.key_func - because that would raise an AttributeError exception. WTF. - """ - self.on_abort = None - self.on_success = None - self.on_input = None - self.key_func.clear() - - def key_enter(self): - txt = self.get_text() - if len(txt) != 0: - if not self.history or self.history[0] != txt: - # add the message to history, but avoid duplicates - self.history.insert(0, txt) - self.histo_pos = -1 - diff --git a/src/windows/list.py b/src/windows/list.py deleted file mode 100644 index 677df6ff..00000000 --- a/src/windows/list.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -Windows relevant for the listing tabs, not much else -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win -from theming import to_curses_attr, get_theme - - -class ListWin(Win): - """ - A list (with no depth, so not for the roster) that can be - scrolled up and down, with one selected line at a time - """ - def __init__(self, columns, with_headers=True): - Win.__init__(self) - self._columns = columns # a dict {'column_name': tuple_index} - self._columns_sizes = {} # a dict {'column_name': size} - self.sorted_by = (None, None) # for example: ('name', '↑') - self.lines = [] # a list of dicts - self._selected_row = 0 - self._starting_pos = 0 # The column number from which we start the refresh - - @property - def pos(self): - if len(self.lines) > self.height: - return len(self.lines) - else: - return 0 - - def empty(self): - """ - emtpy the list and reset some important values as well - """ - self.lines = [] - self._selected_row = 0 - self._starting_pos = 0 - - def resize_columns(self, dic): - """ - Resize the width of the columns - """ - self._columns_sizes = dic - - def sort_by_column(self, col_name, asc=True): - """ - Sort the list by the given column, ascendant or descendant - """ - if not col_name: - return - elif asc: - self.lines.sort(key=lambda x: x[self._columns[col_name]]) - else: - self.lines.sort(key=lambda x: x[self._columns[col_name]], - reverse=True) - self.refresh() - curses.doupdate() - - def add_lines(self, lines): - """ - Append some lines at the end of the list - """ - if not lines: - return - self.lines.extend(lines) - - def set_lines(self, lines): - """ - Set the lines to another list - """ - if not lines: - return - self.lines = lines - - def get_selected_row(self): - """ - Return the tuple representing the selected row - """ - if self._selected_row is not None and self.lines: - return self.lines[self._selected_row] - return None - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - lines = self.lines[self._starting_pos:self._starting_pos+self.height] - for y, line in enumerate(lines): - x = 0 - for col in self._columns.items(): - try: - txt = line[col[1]] or '' - except KeyError: - txt = '' - size = self._columns_sizes[col[0]] - txt += ' ' * (size-len(txt)) - if not txt: - continue - if line is self.lines[self._selected_row]: - self.addstr(y, x, txt[:size], - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - else: - self.addstr(y, x, txt[:size]) - x += size - self._refresh() - - def move_cursor_down(self): - """ - Move the cursor Down - """ - if not self.lines: - return - if self._selected_row < len(self.lines) - 1: - self._selected_row += 1 - while self._selected_row >= self._starting_pos + self.height: - self._starting_pos += self.height // 2 - if self._starting_pos < 0: - self._starting_pos = 0 - return True - - def move_cursor_up(self): - """ - Move the cursor Up - """ - if not self.lines: - return - if self._selected_row > 0: - self._selected_row -= 1 - while self._selected_row < self._starting_pos: - self._starting_pos -= self.height // 2 - return True - - def scroll_down(self): - if not self.lines: - return - self._selected_row += self.height - if self._selected_row > len(self.lines) - 1: - self._selected_row = len(self.lines) -1 - while self._selected_row >= self._starting_pos + self.height: - self._starting_pos += self.height // 2 - if self._starting_pos < 0: - self._starting_pos = 0 - return True - - def scroll_up(self): - if not self.lines: - return - self._selected_row -= self.height + 1 - if self._selected_row < 0: - self._selected_row = 0 - while self._selected_row < self._starting_pos: - self._starting_pos -= self.height // 2 - return True - -class ColumnHeaderWin(Win): - """ - A class displaying the column's names - """ - def __init__(self, columns): - Win.__init__(self) - self._columns = columns - self._columns_sizes = {} - self._column_sel = '' - self._column_order = '' - self._column_order_asc = False - - def resize_columns(self, dic): - self._columns_sizes = dic - - def get_columns(self): - return self._columns - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - x = 0 - for col in self._columns: - txt = col - if col in self._column_order: - if self._column_order_asc: - txt += get_theme().CHAR_COLUMN_ASC - else: - txt += get_theme().CHAR_COLUMN_DESC - #⇓⇑↑↓⇧⇩▲▼ - size = self._columns_sizes[col] - txt += ' ' * (size-len(txt)) - if col in self._column_sel: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER_SEL)) - else: - self.addstr(0, x, txt, to_curses_attr(get_theme().COLOR_COLUMN_HEADER)) - x += size - self._refresh() - - def sel_column(self, dic): - self._column_sel = dic - - def get_sel_column(self): - return self._column_sel - - def set_order(self, order): - self._column_order = self._column_sel - self._column_order_asc = order - - def get_order(self): - if self._column_sel == self._column_order: - return self._column_order_asc - else: - return False - - def sel_column_left(self): - if self._column_sel in self._columns: - index = self._columns.index(self._column_sel) - if index > 1: - index = index -1 - else: - index = 0 - else: - index = 0 - self._column_sel = self._columns[index] - self.refresh() - - def sel_column_right(self): - if self._column_sel in self._columns: - index = self._columns.index(self._column_sel) - if index < len(self._columns)-2: - index = index +1 - else: - index = len(self._columns) -1 - else: - index = len(self._columns) - 1 - self._column_sel = self._columns[index] - self.refresh() - diff --git a/src/windows/misc.py b/src/windows/misc.py deleted file mode 100644 index 07c91bbd..00000000 --- a/src/windows/misc.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Wins that don’t fit any category -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win -from theming import get_theme, to_curses_attr - -class VerticalSeparator(Win): - """ - Just a one-column window, with just a line in it, that is - refreshed only on resize, but never on refresh, for efficiency - """ - def __init__(self): - Win.__init__(self) - - def rewrite_line(self): - self._win.vline(0, 0, curses.ACS_VLINE, self.height, - to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)) - self._refresh() - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self.rewrite_line() - - -class SimpleTextWin(Win): - def __init__(self, text): - Win.__init__(self) - self._text = text - self.built_lines = [] - - def rebuild_text(self): - """ - Transform the text in lines than can then be - displayed without any calculation or anything - at refresh() time - It is basically called on each resize - """ - self.built_lines = [] - for line in self._text.split('\n'): - while len(line) >= self.width: - limit = line[:self.width].rfind(' ') - if limit <= 0: - limit = self.width - self.built_lines.append(line[:limit]) - line = line[limit:] - self.built_lines.append(line) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - for y, line in enumerate(self.built_lines): - self.addstr_colored(line, y, 0) - self._refresh() - diff --git a/src/windows/muc.py b/src/windows/muc.py deleted file mode 100644 index 84775787..00000000 --- a/src/windows/muc.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Windows specific to a MUC -""" - -import logging -log = logging.getLogger(__name__) - -import curses - -from . import Win - -import poopt -from config import config -from theming import to_curses_attr, get_theme - -def userlist_to_cache(userlist): - result = [] - for user in userlist: - result.append((user.nick, user.status, user.chatstate, user.affiliation, user.role)) - return result - -class UserList(Win): - def __init__(self): - Win.__init__(self) - self.pos = 0 - self.cache = [] - - def scroll_up(self): - self.pos += self.height-1 - return True - - def scroll_down(self): - pos = self.pos - self.pos -= self.height-1 - if self.pos < 0: - self.pos = 0 - return self.pos != pos - - def draw_plus(self, y): - self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) - - - def refresh_if_changed(self, users): - old = self.cache - new = userlist_to_cache(users[self.pos:self.pos+self.height]) - if len(old) != len(new): - self.cache = new - self.refresh(users) - return - for i in range(len(old)): - if old[i] != new[i]: - self.cache = new - self.refresh(users) - - def refresh(self, users): - log.debug('Refresh: %s', self.__class__.__name__) - if config.get('hide_user_list'): - return # do not refresh if this win is hidden. - if len(users) < self.height: - self.pos = 0 - elif self.pos >= len(users) - self.height and self.pos != 0: - self.pos = len(users) - self.height - self._win.erase() - asc_sort = (config.get('user_list_sort').lower() == 'asc') - if asc_sort: - y, x = self._win.getmaxyx() - y -= 1 - else: - y = 0 - - for user in users[self.pos:self.pos+self.height]: - self.draw_role_affiliation(y, user) - self.draw_status_chatstate(y, user) - self.addstr(y, 2, - poopt.cut_by_columns(user.nick, self.width - 2), - to_curses_attr(user.color)) - if asc_sort: - y -= 1 - else: - y += 1 - if y == self.height: - break - # draw indicators of position in the list - if self.pos > 0: - if asc_sort: - self.draw_plus(self.height-1) - else: - self.draw_plus(0) - if self.pos + self.height < len(users): - if asc_sort: - self.draw_plus(0) - else: - self.draw_plus(self.height-1) - self._refresh() - - def draw_role_affiliation(self, y, user): - theme = get_theme() - color = theme.color_role(user.role) - symbol = theme.char_affiliation(user.affiliation) - self.addstr(y, 1, symbol, to_curses_attr(color)) - - def draw_status_chatstate(self, y, user): - show_col = get_theme().color_show(user.show) - if user.chatstate == 'composing': - char = get_theme().CHAR_CHATSTATE_COMPOSING - elif user.chatstate == 'active': - char = get_theme().CHAR_CHATSTATE_ACTIVE - elif user.chatstate == 'paused': - char = get_theme().CHAR_CHATSTATE_PAUSED - else: - char = get_theme().CHAR_STATUS - self.addstr(y, 0, char, to_curses_attr(show_col)) - - def resize(self, height, width, y, x): - separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR) - self._resize(height, width, y, x) - self._win.attron(separator) - self._win.vline(0, 0, curses.ACS_VLINE, self.height) - self._win.attroff(separator) - -class Topic(Win): - def __init__(self): - Win.__init__(self) - self._message = '' - - def refresh(self, topic=None): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - if topic: - msg = topic[:self.width-1] - else: - msg = self._message[:self.width-1] - self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR)) - (y, x) = self._win.getyx() - remaining_size = self.width - x - if remaining_size: - self.addnstr(' '*remaining_size, remaining_size, - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self._refresh() - - def set_message(self, message): - self._message = message - diff --git a/src/windows/roster_win.py b/src/windows/roster_win.py deleted file mode 100644 index a2e2badd..00000000 --- a/src/windows/roster_win.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Windows used with the roster (window displaying the contacts, and the -one showing detailed info on the current selection) -""" -import logging -log = logging.getLogger(__name__) - -from datetime import datetime - -from . import Win - -import common -from config import config -from contact import Contact, Resource -from roster import RosterGroup -from theming import get_theme, to_curses_attr - - -class RosterWin(Win): - - def __init__(self): - Win.__init__(self) - self.pos = 0 # cursor position in the contact list - self.start_pos = 1 # position of the start of the display - self.selected_row = None - self.roster_cache = [] - - @property - def roster_len(self): - return len(self.roster_cache) - - def move_cursor_down(self, number=1): - """ - Return True if we scrolled, False otherwise - """ - pos = self.pos - if self.pos < self.roster_len-number: - self.pos += number - else: - self.pos = self.roster_len - 1 - if self.pos >= self.start_pos-1 + self.height-1: - if number == 1: - self.scroll_down(8) - else: - self.scroll_down(self.pos-self.start_pos - self.height // 2) - self.update_pos() - return pos != self.pos - - def move_cursor_up(self, number=1): - """ - Return True if we scrolled, False otherwise - """ - pos = self.pos - if self.pos-number >= 0: - self.pos -= number - else: - self.pos = 0 - if self.pos <= self.start_pos: - if number == 1: - self.scroll_up(8) - else: - self.scroll_up(self.start_pos-self.pos + self.height // 2) - self.update_pos() - return pos != self.pos - - def update_pos(self): - if len(self.roster_cache) > self.pos and self.pos >= 0: - self.selected_row = self.roster_cache[self.pos] - elif self.roster_cache: - self.selected_row = self.roster_cache[-1] - - def scroll_down(self, number=8): - pos = self.start_pos - if self.start_pos + number <= self.roster_len-1: - self.start_pos += number - else: - self.start_pos = self.roster_len-1 - return self.start_pos != pos - - def scroll_up(self, number=8): - pos = self.start_pos - if self.start_pos - number > 0: - self.start_pos -= number - else: - self.start_pos = 1 - return self.start_pos != pos - - def build_roster_cache(self, roster): - """ - Regenerates the roster cache if needed - """ - if not roster.needs_rebuild: - return - log.debug('The roster has changed, rebuilding the cache…') - # This is a search - if roster.contact_filter: - self.roster_cache = [] - sort = config.get('roster_sort', 'jid:show') or 'jid:show' - for contact in roster.get_contacts_sorted_filtered(sort): - self.roster_cache.append(contact) - else: - show_offline = config.get('roster_show_offline') or roster.contact_filter - sort = config.get('roster_sort') or 'jid:show' - group_sort = config.get('roster_group_sort') or 'name' - self.roster_cache = [] - # build the cache - for group in roster.get_groups(group_sort): - contacts_filtered = group.get_contacts(roster.contact_filter) - if (not show_offline and group.get_nb_connected_contacts() == 0) or not contacts_filtered: - continue # Ignore empty groups - self.roster_cache.append(group) - if group.folded: - continue # ignore folded groups - for contact in group.get_contacts(roster.contact_filter, sort): - if not show_offline and len(contact) == 0: - continue # ignore offline contacts - self.roster_cache.append(contact) - if not contact.folded(group.name): - for resource in contact.get_resources(): - self.roster_cache.append(resource) - roster.last_built = datetime.now() - if self.selected_row in self.roster_cache: - if self.pos < self.roster_len and self.roster_cache[self.pos] != self.selected_row: - self.pos = self.roster_cache.index(self.selected_row) - - def refresh(self, roster): - """ - We display a number of lines from the roster cache - (and rebuild it if needed) - """ - log.debug('Refresh: %s', self.__class__.__name__) - self.build_roster_cache(roster) - # make sure we are within bounds - self.move_cursor_up((self.roster_len + self.pos) if self.pos >= self.roster_len else 0) - if not self.roster_cache: - self.selected_row = None - self._win.erase() - self._win.move(0, 0) - self.draw_roster_information(roster) - y = 1 - group = "none" - # scroll down if needed - if self.start_pos+self.height <= self.pos+2: - self.scroll_down(self.pos - self.start_pos - self.height + (self.height//2)) - # draw the roster from the cache - roster_view = self.roster_cache[self.start_pos-1:self.start_pos+self.height] - - options = { - 'show_roster_sub': config.get('show_roster_subscriptions'), - 'show_s2s_errors': config.get('show_s2s_errors'), - 'show_roster_jids': config.get('show_roster_jids') - } - - for item in roster_view: - draw_selected = False - if y -2 + self.start_pos == self.pos: - draw_selected = True - self.selected_row = item - - if isinstance(item, RosterGroup): - self.draw_group(y, item, draw_selected) - group = item.name - elif isinstance(item, Contact): - self.draw_contact_line(y, item, draw_selected, group, **options) - elif isinstance(item, Resource): - self.draw_resource_line(y, item, draw_selected) - - y += 1 - - if self.start_pos > 1: - self.draw_plus(1) - if self.start_pos + self.height-2 < self.roster_len: - self.draw_plus(self.height-1) - self._refresh() - - - def draw_plus(self, y): - """ - Draw the indicator that shows that - the list is longer than what is displayed - """ - self.addstr(y, self.width-5, '++++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR)) - - def draw_roster_information(self, roster): - """ - The header at the top - """ - self.addstr('Roster: %s/%s contacts' % ( - roster.get_nb_connected_contacts(), - len(roster)), - to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - - def draw_group(self, y, group, colored): - """ - Draw a groupname on a line - """ - if colored: - self._win.attron(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - if group.folded: - self.addstr(y, 0, '[+] ') - else: - self.addstr(y, 0, '[-] ') - contacts = " (%s/%s)" % (group.get_nb_connected_contacts(), len(group)) - self.addstr(y, 4, self.truncate_name(group.name, len(contacts)+4) + contacts) - if colored: - self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - self.finish_line() - - def truncate_name(self, name, added): - if len(name) + added <= self.width: - return name - return name[:self.width - added - 1] + '…' - - def draw_contact_line(self, y, contact, colored, group, show_roster_sub=False, - show_s2s_errors=True, show_roster_jids=False): - """ - Draw on a line all informations about one contact. - This is basically the highest priority resource's informations - Use 'color' to draw the jid/display_name to show what is - the currently selected contact in the list - """ - - theme = get_theme() - resource = contact.get_highest_priority_resource() - if not resource: - # There's no online resource - presence = 'unavailable' - nb = '' - else: - presence = resource.presence - nb = ' (%s)' % len(contact) - color = theme.color_show(presence) - added = 2 + len(theme.CHAR_STATUS) + len(nb) - - self.addstr(y, 0, ' ') - self.addstr(theme.CHAR_STATUS, to_curses_attr(color)) - - self.addstr(' ') - if resource: - self.addstr('[+] ' if contact.folded(group) else '[-] ') - added += 4 - if contact.ask: - added += len(get_theme().CHAR_ROSTER_ASKED) - if show_s2s_errors and contact.error: - added += len(get_theme().CHAR_ROSTER_ERROR) - if contact.tune: - added += len(get_theme().CHAR_ROSTER_TUNE) - if contact.mood: - added += len(get_theme().CHAR_ROSTER_MOOD) - if contact.activity: - added += len(get_theme().CHAR_ROSTER_ACTIVITY) - if contact.gaming: - added += len(get_theme().CHAR_ROSTER_GAMING) - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - added += len(theme.char_subscription(contact.subscription, keep=show_roster_sub)) - - if not show_roster_jids and contact.name: - display_name = '%s' % contact.name - elif contact.name and contact.name != contact.bare_jid: - display_name = '%s (%s)' % (contact.name, contact.bare_jid) - else: - display_name = '%s' % (contact.bare_jid,) - - display_name = self.truncate_name(display_name, added) + nb - - if colored: - self.addstr(display_name, to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - else: - self.addstr(display_name) - - if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both', 'none'): - self.addstr(theme.char_subscription(contact.subscription, keep=show_roster_sub), to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION)) - if contact.ask: - self.addstr(get_theme().CHAR_ROSTER_ASKED, to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) - if show_s2s_errors and contact.error: - self.addstr(get_theme().CHAR_ROSTER_ERROR, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) - if contact.tune: - self.addstr(get_theme().CHAR_ROSTER_TUNE, to_curses_attr(get_theme().COLOR_ROSTER_TUNE)) - if contact.activity: - self.addstr(get_theme().CHAR_ROSTER_ACTIVITY, to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY)) - if contact.mood: - self.addstr(get_theme().CHAR_ROSTER_MOOD, to_curses_attr(get_theme().COLOR_ROSTER_MOOD)) - if contact.gaming: - self.addstr(get_theme().CHAR_ROSTER_GAMING, to_curses_attr(get_theme().COLOR_ROSTER_GAMING)) - self.finish_line() - - def draw_resource_line(self, y, resource, colored): - """ - Draw a specific resource line - """ - color = get_theme().color_show(resource.presence) - self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color)) - if colored: - self.addstr(y, 6, self.truncate_name(str(resource.jid), 6), to_curses_attr(get_theme().COLOR_SELECTED_ROW)) - else: - self.addstr(y, 6, self.truncate_name(str(resource.jid), 6)) - self.finish_line() - - def get_selected_row(self): - if self.pos >= len(self.roster_cache): - return self.selected_row - if len(self.roster_cache) > 0: - self.selected_row = self.roster_cache[self.pos] - return self.roster_cache[self.pos] - return None - -class ContactInfoWin(Win): - def __init__(self): - Win.__init__(self) - - def draw_contact_info(self, contact): - """ - draw the contact information - """ - resource = contact.get_highest_priority_resource() - if contact: - jid = contact.bare_jid - elif resource: - jid = resource.jid - else: - jid = 'example@example.com' # should never happen - if resource: - presence = resource.presence - else: - presence = 'unavailable' - i = 0 - self.addstr(0, 0, '%s (%s)'%(jid, presence,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - i += 1 - self.addstr(i, 0, 'Subscription: %s' % (contact.subscription,)) - self.finish_line() - i += 1 - if contact.ask: - if contact.ask == 'asked': - self.addstr(i, 0, 'Ask: %s' % (contact.ask,), to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT)) - else: - self.addstr(i, 0, 'Ask: %s' % (contact.ask,)) - self.finish_line() - i += 1 - if resource: - self.addstr(i, 0, 'Status: %s' % (resource.status)) - self.finish_line() - i += 1 - - if contact.error: - self.addstr(i, 0, 'Error: %s' % contact.error, to_curses_attr(get_theme().COLOR_ROSTER_ERROR)) - self.finish_line() - i += 1 - - if contact.tune: - self.addstr(i, 0, 'Tune: %s' % common.format_tune_string(contact.tune), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.mood: - self.addstr(i, 0, 'Mood: %s' % contact.mood, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.activity: - self.addstr(i, 0, 'Activity: %s' % contact.activity, to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - if contact.gaming: - self.addstr(i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming), to_curses_attr(get_theme().COLOR_NORMAL_TEXT)) - self.finish_line() - i += 1 - - def draw_group_info(self, group): - """ - draw the group information - """ - self.addstr(0, 0, group.name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) - self.finish_line(get_theme().COLOR_INFORMATION_BAR) - - def refresh(self, selected_row): - log.debug('Refresh: %s', self.__class__.__name__) - self._win.erase() - if isinstance(selected_row, RosterGroup): - self.draw_group_info(selected_row) - elif isinstance(selected_row, Contact): - self.draw_contact_info(selected_row) - # elif isinstance(selected_row, Resource): - # self.draw_contact_info(None, selected_row) - self._refresh() diff --git a/src/windows/text_win.py b/src/windows/text_win.py deleted file mode 100644 index fd1fe546..00000000 --- a/src/windows/text_win.py +++ /dev/null @@ -1,597 +0,0 @@ -""" -TextWin, the window showing the text messages and info messages in poezio. -Can be locked, scrolled, has a separator, etc… -""" - -import logging -log = logging.getLogger(__name__) - -import curses -from math import ceil, log10 - -from . import Win -from . base_wins import FORMAT_CHAR, Line -from . funcs import truncate_nick, parse_attrs - -import poopt -from config import config -from theming import to_curses_attr, get_theme, dump_tuple - - -class BaseTextWin(Win): - def __init__(self, lines_nb_limit=None): - if lines_nb_limit is None: - lines_nb_limit = config.get('max_lines_in_memory') - Win.__init__(self) - self.lines_nb_limit = lines_nb_limit - self.pos = 0 - self.built_lines = [] # Each new message is built and kept here. - # on resize, we rebuild all the messages - - self.lock = False - self.lock_buffer = [] - self.separator_after = None - - def toggle_lock(self): - if self.lock: - self.release_lock() - else: - self.acquire_lock() - return self.lock - - def acquire_lock(self): - self.lock = True - - def release_lock(self): - for line in self.lock_buffer: - self.built_lines.append(line) - self.lock = False - - def scroll_up(self, dist=14): - pos = self.pos - self.pos += dist - if self.pos + self.height > len(self.built_lines): - self.pos = len(self.built_lines) - self.height - if self.pos < 0: - self.pos = 0 - return self.pos != pos - - def scroll_down(self, dist=14): - pos = self.pos - self.pos -= dist - if self.pos <= 0: - self.pos = 0 - return self.pos != pos - - def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10): - """ - Take one message, build it and add it to the list - Return the number of lines that are built for the given - message. - """ - lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size) - if self.lock: - self.lock_buffer.extend(lines) - else: - self.built_lines.extend(lines) - if not lines or not lines[0]: - return 0 - if clean: - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - return len(lines) - - def build_message(self, message, timestamp=False, nick_size=10): - """ - Build a list of lines from a message, without adding it - to a list - """ - pass - - def refresh(self): - pass - - def write_text(self, y, x, txt): - """ - write the text of a line. - """ - self.addstr_colored(txt, y, x) - - def write_time(self, time): - """ - Write the date on the yth line of the window - """ - if time: - color = get_theme().COLOR_TIME_STRING - curses_color = to_curses_attr(color) - self._win.attron(curses_color) - self.addstr(time) - self._win.attroff(curses_color) - self.addstr(' ') - return poopt.wcswidth(time) + 1 - return 0 - - def resize(self, height, width, y, x, room=None): - if hasattr(self, 'width'): - old_width = self.width - else: - old_width = None - self._resize(height, width, y, x) - if room and self.width != old_width: - self.rebuild_everything(room) - - # reposition the scrolling after resize - # (see #2450) - buf_size = len(self.built_lines) - if buf_size - self.pos < self.height: - self.pos = buf_size - self.height - if self.pos < 0: - self.pos = 0 - - def rebuild_everything(self, room): - self.built_lines = [] - with_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') - for message in room.messages: - self.build_new_message(message, clean=False, timestamp=with_timestamps, nick_size=nick_size) - if self.separator_after is message: - self.build_new_message(None) - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - - def __del__(self): - log.debug('** TextWin: deleting %s built lines', (len(self.built_lines))) - del self.built_lines - -class TextWin(BaseTextWin): - def __init__(self, lines_nb_limit=None): - BaseTextWin.__init__(self, lines_nb_limit) - - # the Lines of the highlights in that buffer - self.highlights = [] - # the current HL position in that list NaN means that we’re not on - # an hl. -1 is a valid position (it's before the first hl of the - # list. i.e the separator, in the case where there’s no hl before - # it.) - self.hl_pos = float('nan') - - # Keep track of the number of hl after the separator. - # This is useful to make “go to next highlight“ work after a “move to separator”. - self.nb_of_highlights_after_separator = 0 - - self.separator_after = None - - def next_highlight(self): - """ - Go to the next highlight in the buffer. - (depending on which highlight was selected before) - if the buffer is already positionned on the last, of if there are no - highlights, scroll to the end of the buffer. - """ - log.debug('Going to the next highlight…') - if (not self.highlights or self.hl_pos != self.hl_pos or - self.hl_pos >= len(self.highlights) - 1): - self.hl_pos = float('nan') - self.pos = 0 - return - hl_size = len(self.highlights) - 1 - if self.hl_pos < hl_size: - self.hl_pos += 1 - else: - self.hl_pos = hl_size - log.debug("self.hl_pos = %s", self.hl_pos) - hl = self.highlights[self.hl_pos] - pos = None - while not pos: - try: - pos = self.built_lines.index(hl) - except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] - if not self.highlights: - self.hl_pos = float('nan') - self.pos = 0 - return - self.hl_pos = 0 - hl = self.highlights[0] - self.pos = len(self.built_lines) - pos - self.height - if self.pos < 0 or self.pos >= len(self.built_lines): - self.pos = 0 - - def previous_highlight(self): - """ - Go to the previous highlight in the buffer. - (depending on which highlight was selected before) - if the buffer is already positionned on the first, or if there are no - highlights, scroll to the end of the buffer. - """ - log.debug('Going to the previous highlight…') - if not self.highlights or self.hl_pos <= 0: - self.hl_pos = float('nan') - self.pos = 0 - return - if self.hl_pos != self.hl_pos: - self.hl_pos = len(self.highlights) - 1 - else: - self.hl_pos -= 1 - log.debug("self.hl_pos = %s", self.hl_pos) - hl = self.highlights[self.hl_pos] - pos = None - while not pos: - try: - pos = self.built_lines.index(hl) - except ValueError: - self.highlights = self.highlights[self.hl_pos+1:] - if not self.highlights: - self.hl_pos = float('nan') - self.pos = 0 - return - self.hl_pos = 0 - hl = self.highlights[0] - self.pos = len(self.built_lines) - pos - self.height - if self.pos < 0 or self.pos >= len(self.built_lines): - self.pos = 0 - - def scroll_to_separator(self): - """ - Scroll until separator is centered. If no separator is - present, scroll at the top of the window - """ - if None in self.built_lines: - self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1 - if self.pos < 0: - self.pos = 0 - else: - self.pos = len(self.built_lines) - self.height + 1 - # Chose a proper position (not too high) - self.scroll_up(0) - # Make “next highlight” work afterwards. This makes it easy to - # review all the highlights since the separator was placed, in - # the correct order. - self.hl_pos = len(self.highlights) - self.nb_of_highlights_after_separator - 1 - log.debug("self.hl_pos = %s", self.hl_pos) - - def remove_line_separator(self): - """ - Remove the line separator - """ - log.debug('remove_line_separator') - if None in self.built_lines: - self.built_lines.remove(None) - self.separator_after = None - - def add_line_separator(self, room=None): - """ - add a line separator at the end of messages list - room is a textbuffer that is needed to get the previous message - (in case of resize) - """ - if None not in self.built_lines: - self.built_lines.append(None) - self.nb_of_highlights_after_separator = 0 - log.debug("Reseting number of highlights after separator") - if room and room.messages: - self.separator_after = room.messages[-1] - - def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False, nick_size=10): - """ - Take one message, build it and add it to the list - Return the number of lines that are built for the given - message. - """ - lines = self.build_message(message, timestamp=timestamp, nick_size=nick_size) - if self.lock: - self.lock_buffer.extend(lines) - else: - self.built_lines.extend(lines) - if not lines or not lines[0]: - return 0 - if highlight: - self.highlights.append(lines[0]) - self.nb_of_highlights_after_separator += 1 - log.debug("Number of highlights after separator is now %s", - self.nb_of_highlights_after_separator) - if clean: - while len(self.built_lines) > self.lines_nb_limit: - self.built_lines.pop(0) - return len(lines) - - def build_message(self, message, timestamp=False, nick_size=10): - """ - Build a list of lines from a message, without adding it - to a list - """ - if message is None: # line separator - return [None] - txt = message.txt - if not txt: - return [] - if len(message.str_time) > 8: - default_color = (FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) - + '}') - else: - default_color = None - ret = [] - nick = truncate_nick(message.nickname, nick_size) - offset = 0 - if message.ack: - if message.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 - if nick: - offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length - if message.revisions > 0: - offset += ceil(log10(message.revisions + 1)) - if message.me: - offset += 1 # '* ' before and ' ' after - if timestamp: - if message.str_time: - offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: - offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: - offset += 1 - lines = poopt.cut_text(txt, self.width-offset-1) - prepend = default_color if default_color else '' - attrs = [] - for line in lines: - saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend) - attrs = parse_attrs(message.txt[line[0]:line[1]], attrs) - if attrs: - prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) - else: - if default_color: - prepend = default_color - else: - prepend = '' - ret.append(saved) - return ret - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - if self.height <= 0: - return - if self.pos == 0: - lines = self.built_lines[-self.height:] - else: - lines = self.built_lines[-self.height-self.pos:-self.pos] - with_timestamps = config.get("show_timestamps") - nick_size = config.get("max_nick_length") - self._win.move(0, 0) - self._win.erase() - offset = 0 - for y, line in enumerate(lines): - if line: - msg = line.msg - if line.start_pos == 0: - offset = self.write_pre_msg(msg, with_timestamps, nick_size) - elif y == 0: - offset = self.compute_offset(msg, with_timestamps, nick_size) - self.write_text(y, offset, line.prepend - + line.msg.txt[line.start_pos:line.end_pos]) - else: - self.write_line_separator(y) - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - self._refresh() - - def compute_offset(self, msg, with_timestamps, nick_size): - offset = 0 - if with_timestamps and msg.str_time: - offset += poopt.wcswidth(msg.str_time) + 1 - - if not msg.nickname: # not a message, nothing to do afterwards - return offset - - nick = truncate_nick(msg.nickname, nick_size) - offset += poopt.wcswidth(nick) - if msg.ack: - if msg.ack > 0: - offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - else: - offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1 - if msg.me: - offset += 3 - else: - offset += 2 - if msg.revisions: - offset += ceil(log10(msg.revisions + 1)) - offset += self.write_revisions(msg) - return offset - - - def write_pre_msg(self, msg, with_timestamps, nick_size): - offset = 0 - if with_timestamps: - offset += self.write_time(msg.str_time) - - if not msg.nickname: # not a message, nothing to do afterwards - return offset - - nick = truncate_nick(msg.nickname, nick_size) - offset += poopt.wcswidth(nick) - if msg.nick_color: - color = msg.nick_color - elif msg.user: - color = msg.user.color - else: - color = None - if msg.ack: - if msg.ack > 0: - offset += self.write_ack() - else: - offset += self.write_nack() - if msg.me: - self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE)) - self.addstr('* ') - self.write_nickname(nick, color, msg.highlight) - offset += self.write_revisions(msg) - self.addstr(' ') - offset += 3 - else: - self.write_nickname(nick, color, msg.highlight) - offset += self.write_revisions(msg) - self.addstr('> ') - offset += 2 - return offset - - def write_revisions(self, msg): - if msg.revisions: - self._win.attron(to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE)) - self.addstr('%d' % msg.revisions) - self._win.attrset(0) - return ceil(log10(msg.revisions + 1)) - return 0 - - def write_line_separator(self, y): - char = get_theme().CHAR_NEW_TEXT_SEPARATOR - self.addnstr(y, 0, - char * (self.width // len(char) - 1), - self.width, - to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR)) - - def write_ack(self): - color = get_theme().COLOR_CHAR_ACK - self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_ACK_RECEIVED) - self._win.attroff(to_curses_attr(color)) - self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 - - def write_nack(self): - color = get_theme().COLOR_CHAR_NACK - self._win.attron(to_curses_attr(color)) - self.addstr(get_theme().CHAR_NACK) - self._win.attroff(to_curses_attr(color)) - self.addstr(' ') - return poopt.wcswidth(get_theme().CHAR_NACK) + 1 - - def write_nickname(self, nickname, color, highlight=False): - """ - Write the nickname, using the user's color - and return the number of written characters - """ - if not nickname: - return - if highlight: - hl_color = get_theme().COLOR_HIGHLIGHT_NICK - if hl_color == "reverse": - self._win.attron(curses.A_REVERSE) - else: - color = hl_color - if color: - self._win.attron(to_curses_attr(color)) - self.addstr(nickname) - if color: - self._win.attroff(to_curses_attr(color)) - if highlight and hl_color == "reverse": - self._win.attroff(curses.A_REVERSE) - - def modify_message(self, old_id, message): - """ - Find a message, and replace it with a new one - (instead of rebuilding everything in order to correct a message) - """ - with_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') - for i in range(len(self.built_lines)-1, -1, -1): - if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id: - index = i - while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id: - self.built_lines.pop(index) - index -= 1 - index += 1 - lines = self.build_message(message, timestamp=with_timestamps, nick_size=nick_size) - for line in lines: - self.built_lines.insert(index, line) - index += 1 - break - - def __del__(self): - log.debug('** TextWin: deleting %s built lines', (len(self.built_lines))) - del self.built_lines - -class XMLTextWin(BaseTextWin): - def __init__(self): - BaseTextWin.__init__(self) - - def refresh(self): - log.debug('Refresh: %s', self.__class__.__name__) - theme = get_theme() - if self.height <= 0: - return - if self.pos == 0: - lines = self.built_lines[-self.height:] - else: - lines = self.built_lines[-self.height-self.pos:-self.pos] - self._win.move(0, 0) - self._win.erase() - for y, line in enumerate(lines): - if line: - msg = line.msg - if line.start_pos == 0: - if msg.nickname == theme.CHAR_XML_OUT: - color = theme.COLOR_XML_OUT - elif msg.nickname == theme.CHAR_XML_IN: - color = theme.COLOR_XML_IN - self.write_time(msg.str_time) - self.write_prefix(msg.nickname, color) - self.addstr(' ') - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - for y, line in enumerate(lines): - offset = 0 - # Offset for the timestamp (if any) plus a space after it - offset += len(line.msg.str_time) - # space - offset += 1 - - # Offset for the prefix - offset += poopt.wcswidth(truncate_nick(line.msg.nickname)) - # space - offset += 1 - - self.write_text(y, offset, line.prepend - + line.msg.txt[line.start_pos:line.end_pos]) - if y != self.height-1: - self.addstr('\n') - self._win.attrset(0) - self._refresh() - - def build_message(self, message, timestamp=False, nick_size=10): - txt = message.txt - ret = [] - default_color = None - nick = truncate_nick(message.nickname, nick_size) - offset = 0 - if nick: - offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length - if message.str_time: - offset += 1 + len(message.str_time) - if get_theme().CHAR_TIME_LEFT and message.str_time: - offset += 1 - if get_theme().CHAR_TIME_RIGHT and message.str_time: - offset += 1 - lines = poopt.cut_text(txt, self.width-offset-1) - prepend = default_color if default_color else '' - attrs = [] - for line in lines: - saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend) - attrs = parse_attrs(message.txt[line[0]:line[1]], attrs) - if attrs: - prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) - else: - if default_color: - prepend = default_color - else: - prepend = '' - ret.append(saved) - return ret - - def write_prefix(self, nickname, color): - self._win.attron(to_curses_attr(color)) - self.addstr(truncate_nick(nickname)) - self._win.attroff(to_curses_attr(color)) - diff --git a/src/xhtml.py b/src/xhtml.py deleted file mode 100644 index b84ce943..00000000 --- a/src/xhtml.py +++ /dev/null @@ -1,543 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org> -# -# 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 methods to convert -shell colors to poezio colors, -xhtml code to shell colors, -poezio colors to xhtml code -""" - -import base64 -import curses -import hashlib -import re -from os import path -from slixmpp.xmlstream import ET -from urllib.parse import unquote - -from io import BytesIO -from xml import sax -from xml.sax import saxutils - -digits = '0123456789' # never trust the modules - -XHTML_NS = 'http://www.w3.org/1999/xhtml' - -# HTML named colors -colors = { - 'aliceblue': 231, - 'antiquewhite': 231, - 'aqua': 51, - 'aquamarine': 122, - 'azure': 231, - 'beige': 231, - 'bisque': 230, - 'black': 232, - 'blanchedalmond': 230, - 'blue': 21, - 'blueviolet': 135, - 'brown': 124, - 'burlywood': 223, - 'cadetblue': 109, - 'chartreuse': 118, - 'chocolate': 172, - 'coral': 209, - 'cornflowerblue': 111, - 'cornsilk': 231, - 'crimson': 197, - 'cyan': 51, - 'darkblue': 19, - 'darkcyan': 37, - 'darkgoldenrod': 178, - 'darkgray': 247, - 'darkgreen': 28, - 'darkgrey': 247, - 'darkkhaki': 186, - 'darkmagenta': 127, - 'darkolivegreen': 65, - 'darkorange': 214, - 'darkorchid': 134, - 'darkred': 124, - 'darksalmon': 216, - 'darkseagreen': 151, - 'darkslateblue': 61, - 'darkslategray': 59, - 'darkslategrey': 59, - 'darkturquoise': 44, - 'darkviolet': 128, - 'deeppink': 199, - 'deepskyblue': 45, - 'dimgray': 241, - 'dimgrey': 241, - 'dodgerblue': 39, - 'firebrick': 160, - 'floralwhite': 231, - 'forestgreen': 34, - 'fuchsia': 201, - 'gainsboro': 252, - 'ghostwhite': 231, - 'gold': 226, - 'goldenrod': 214, - 'gray': 244, - 'green': 34, - 'greenyellow': 191, - 'grey': 244, - 'honeydew': 231, - 'hotpink': 212, - 'indianred': 174, - 'indigo': 55, - 'ivory': 231, - 'khaki': 229, - 'lavender': 231, - 'lavenderblush': 231, - 'lawngreen': 118, - 'lemonchiffon': 230, - 'lightblue': 195, - 'lightcoral': 217, - 'lightcyan': 231, - 'lightgoldenrodyellow': 230, - 'lightgray': 251, - 'lightgreen': 157, - 'lightgrey': 251, - 'lightpink': 224, - 'lightsalmon': 216, - 'lightseagreen': 43, - 'lightskyblue': 153, - 'lightslategray': 109, - 'lightslategrey': 109, - 'lightsteelblue': 189, - 'lightyellow': 231, - 'lime': 46, - 'limegreen': 77, - 'linen': 231, - 'magenta': 201, - 'maroon': 124, - 'mediumaquamarine': 115, - 'mediumblue': 20, - 'mediumorchid': 170, - 'mediumpurple': 141, - 'mediumseagreen': 78, - 'mediumslateblue': 105, - 'mediumspringgreen': 49, - 'mediumturquoise': 80, - 'mediumvioletred': 163, - 'midnightblue': 18, - 'mintcream': 231, - 'mistyrose': 231, - 'moccasin': 230, - 'navajowhite': 230, - 'navy': 19, - 'oldlace': 231, - 'olive': 142, - 'olivedrab': 106, - 'orange': 214, - 'orangered': 202, - 'orchid': 213, - 'palegoldenrod': 229, - 'palegreen': 157, - 'paleturquoise': 195, - 'palevioletred': 211, - 'papayawhip': 231, - 'peachpuff': 230, - 'peru': 179, - 'pink': 224, - 'plum': 219, - 'powderblue': 195, - 'purple': 127, - 'red': 196, - 'rosybrown': 181, - 'royalblue': 69, - 'saddlebrown': 130, - 'salmon': 216, - 'sandybrown': 216, - 'seagreen': 72, - 'seashell': 231, - 'sienna': 131, - 'silver': 250, - 'skyblue': 153, - 'slateblue': 104, - 'slategray': 109, - 'slategrey': 109, - 'snow': 231, - 'springgreen': 48, - 'steelblue': 74, - 'tan': 187, - 'teal': 37, - 'thistle': 225, - 'tomato': 209, - 'turquoise': 86, - 'violet': 219, - 'wheat': 230, - 'white': 255, - 'whitesmoke': 255, - 'yellow': 226, - 'yellowgreen': 149 -} - -whitespace_re = re.compile(r'\s+') - -xhtml_attr_re = re.compile(r'\x19-?\d[^}]*}|\x19[buaio]') -xhtml_data_re = re.compile(r'data:image/([a-z]+);base64,(.+)') -poezio_color_double = re.compile(r'(?:\x19\d+}|\x19\d)+(\x19\d|\x19\d+})') -poezio_format_trim = re.compile(r'(\x19\d+}|\x19\d|\x19[buaio]|\x19o)+\x19o') - -xhtml_simple_attr_re = re.compile(r'\x19\d') - -def get_body_from_message_stanza(message, use_xhtml=False, - tmp_dir=None, extract_images=False): - """ - Returns a string with xhtml markups converted to - poezio colors if there's an xhtml_im element, or - the body (without any color) otherwise - """ - if use_xhtml: - xhtml = message['html'].xml - xhtml_body = xhtml.find('{http://www.w3.org/1999/xhtml}body') - if xhtml_body: - content = xhtml_to_poezio_colors(xhtml_body, tmp_dir=tmp_dir, - extract_images=extract_images) - content = content if content else message['body'] - return content or " " - return message['body'] - -def ncurses_color_to_html(color): - """ - Takes an int between 0 and 256 and returns - a string of the form #XXXXXX representing an - html color. - """ - if color <= 15: - try: - (r, g, b) = curses.color_content(color) - except: # fallback in faulty terminals (e.g. xterm) - (r, g, b) = curses.color_content(color%8) - r = r / 1000 * 6 - 0.01 - g = g / 1000 * 6 - 0.01 - b = b / 1000 * 6 - 0.01 - elif color <= 231: - color = color - 16 - r = color % 6 - color = color / 6 - g = color % 6 - color = color / 6 - b = color % 6 - else: - color -= 232 - r = g = b = color / 24 * 6 - return '#%02X%02X%02X' % (r*256/6, g*256/6, b*256/6) - -def parse_css(css): - def get_color(value): - if value[0] == '#': - value = value[1:] - length = len(value) - if length != 3 and length != 6: - return -1 - value = int(value, 16) - if length == 6: - r = int(value >> 16) - g = int((value >> 8) & 0xff) - b = int(value & 0xff) - if r == g == b: - return 232 + int(r/10.6251) - div = 42.51 - else: - r = int(value >> 8) - g = int((value >> 4) & 0xf) - b = int(value & 0xf) - if r == g == b: - return 232 + int(1.54*r) - div = 2.51 - return 6*6*int(r/div) + 6*int(g/div) + int(b/div) + 16 - if value in colors: - return colors[value] - return -1 - shell = '' - rules = css.split(';') - for rule in rules: - if ':' not in rule: - continue - key, value = rule.split(':', 1) - key = key.strip() - value = value.strip() - if key == 'background-color': - pass#shell += '\x191' - elif key == 'color': - color = get_color(value) - if color != -1: - shell += '\x19%d}' % color - elif key == 'font-style': - shell += '\x19i' - elif key == 'font-weight': - shell += '\x19b' - elif key == 'margin-left': - shell += ' ' - elif key == 'text-align': - pass - elif key == 'text-decoration': - if value == 'underline': - shell += '\x19u' - elif value == 'blink': - shell += '\x19a' - return shell - -def trim(string): - return re.sub(whitespace_re, ' ', string) - -class XHTMLHandler(sax.ContentHandler): - def __init__(self, force_ns=False, tmp_dir=None, extract_images=False): - self.builder = [] - self.formatting = [] - self.attrs = [] - self.list_state = [] - self.is_pre = False - self.a_start = 0 - # do not care about xhtml-in namespace - self.force_ns = force_ns - - self.tmp_dir = tmp_dir - self.extract_images = extract_images - - @property - def result(self): - sanitized = re.sub(poezio_color_double, r'\1', ''.join(self.builder).strip()) - return re.sub(poezio_format_trim, '\x19o', sanitized) - - def append_formatting(self, formatting): - self.formatting.append(formatting) - self.builder.append(formatting) - - def pop_formatting(self): - self.formatting.pop() - self.builder.append('\x19o' + ''.join(self.formatting)) - - def characters(self, characters): - self.builder.append(characters if self.is_pre else trim(characters)) - - def startElementNS(self, name, _, attrs): - if name[0] != XHTML_NS and not self.force_ns: - return - - builder = self.builder - attrs = {name: value for ((ns, name), value) in attrs.items() if ns is None} - self.attrs.append(attrs) - - if 'style' in attrs: - style = parse_css(attrs['style']) - self.append_formatting(style) - - name = name[1] - if name == 'a': - self.append_formatting('\x19u') - self.a_start = len(self.builder) - elif name == 'blockquote': - builder.append('“') - elif name == 'br': - builder.append('\n') - elif name == 'cite': - self.append_formatting('\x19u') - elif name == 'em': - self.append_formatting('\x19i') - elif name == 'img': - if re.match(xhtml_data_re, attrs['src']) and self.extract_images: - type_, data = [i for i in re.split(xhtml_data_re, attrs['src']) if i] - bin_data = base64.b64decode(unquote(data)) - filename = hashlib.sha1(bin_data).hexdigest() + '.' + type_ - filepath = path.join(self.tmp_dir, filename) - if not path.exists(filepath): - try: - with open(filepath, 'wb') as fd: - fd.write(bin_data) - builder.append('file://%s' % filepath) - except Exception as e: - builder.append('[Error while saving image: %s]' % e) - else: - builder.append('file://%s' % filepath) - else: - builder.append(trim(attrs['src'])) - if 'alt' in attrs: - builder.append(' (%s)' % trim(attrs['alt'])) - elif name == 'ul': - self.list_state.append('ul') - elif name == 'ol': - self.list_state.append(1) - elif name == 'li': - try: - state = self.list_state[-1] - except IndexError: - state = 'ul' - if state == 'ul': - builder.append('\n• ') - else: - builder.append('\n%d) ' % state) - state += 1 - self.list_state[-1] = state - elif name == 'p': - builder.append('\n') - elif name == 'pre': - builder.append('\n') - self.is_pre = True - elif name == 'strong': - self.append_formatting('\x19b') - - def endElementNS(self, name, _): - if name[0] != XHTML_NS and not self.force_ns: - return - - builder = self.builder - attrs = self.attrs.pop() - name = name[1] - - if name == 'a': - self.pop_formatting() - # do not display the link twice - text_elements = filter(lambda x: not x.startswith('\x19'), - self.builder[self.a_start:]) - link_text = ''.join(text_elements).strip() - if 'href' in attrs and attrs['href'] != link_text: - builder.append(' (%s)' % trim(attrs['href'])) - elif name == 'blockquote': - builder.append('”') - elif name in ('cite', 'em', 'strong'): - self.pop_formatting() - elif name in ('ol', 'p', 'ul'): - builder.append('\n') - elif name == 'pre': - builder.append('\n') - self.is_pre = False - - if 'style' in attrs: - self.pop_formatting() - - if 'title' in attrs: - builder.append(' [' + attrs['title'] + ']') - -def xhtml_to_poezio_colors(xml, force=False, tmp_dir=None, extract_images=None): - if isinstance(xml, str): - xml = xml.encode('utf8') - elif not isinstance(xml, bytes): - xml = ET.tostring(xml) - - handler = XHTMLHandler(force_ns=force, tmp_dir=tmp_dir, - extract_images=extract_images) - parser = sax.make_parser() - parser.setFeature(sax.handler.feature_namespaces, True) - parser.setContentHandler(handler) - parser.parse(BytesIO(xml)) - return handler.result - -def clean_text(s): - """ - Remove all xhtml-im attributes (\x19etc) from the string with the - complete color format, i.e \x19xxx} - """ - s = re.sub(xhtml_attr_re, "", s) - return s - -def clean_text_simple(string): - """ - Remove all \x19 from the string formatted with simple colors: - \x198 - """ - pos = string.find('\x19') - while pos != -1: - string = string[:pos] + string[pos+2:] - pos = string.find('\x19') - return string - -def convert_simple_to_full_colors(text): - """ - takes a \x19n formatted string and returns - a \x19n} formatted one. - """ - # TODO, have a single list of this. This is some sort of - # dusplicate from windows.format_chars - mapping = str.maketrans({'\x0E': '\x19b', '\x0F': '\x19o', '\x10': '\x19u', - '\x11': '\x191', '\x12': '\x192', '\x13': '\x193', - '\x14': '\x194', '\x15': '\x195', '\x16': '\x196', - '\x17': '\x197', '\x18': '\x198', '\x19': '\x199'}) - text = text.translate(mapping) - def add_curly_bracket(match): - return match.group(0) + '}' - return re.sub(xhtml_simple_attr_re, add_curly_bracket, text) - -number_to_color_names = { - 1: 'red', - 2: 'green', - 3: 'yellow', - 4: 'blue', - 5: 'violet', - 6: 'turquoise', - 7: 'white' -} - -def format_inline_css(_dict): - return ''.join(('%s: %s;' % (key, value) for key, value in _dict.items())) - -def poezio_colors_to_html(string): - """ - Convert poezio colors to html - (e.g. \x191}: <span style='color: red'>) - """ - # Maintain a list of the current css attributes used - # And check if a tag is open (by design, we only open - # spans tag, and they cannot be nested. - current_attrs = {} - tag_open = False - next_attr_char = string.find('\x19') - build = ["<body xmlns='http://www.w3.org/1999/xhtml'><p>"] - - def check_property(key, value): - nonlocal tag_open - if current_attrs.get(key, None) == value: - return - current_attrs[key] = value - if tag_open: - tag_open = False - build.append('</span>') - - while next_attr_char != -1: - attr_char = string[next_attr_char+1].lower() - - if next_attr_char != 0 and string[:next_attr_char]: - if current_attrs and not tag_open: - build.append('<span style="%s">' % format_inline_css(current_attrs)) - tag_open = True - build.append(saxutils.escape(string[:next_attr_char])) - - if attr_char == 'o': - if tag_open: - build.append('</span>') - tag_open = False - current_attrs = {} - elif attr_char == 'b': - check_property('font-weight', 'bold') - elif attr_char == 'u': - check_property('text-decoration', 'underline') - - if attr_char in digits: - number_str = string[next_attr_char+1:string.find('}', next_attr_char)] - number = int(number_str) - if number in number_to_color_names: - check_property('color', number_to_color_names.get(number, 'black')) - else: - check_property('color', ncurses_color_to_html(number)) - string = string[next_attr_char+len(number_str)+2:] - else: - string = string[next_attr_char+2:] - next_attr_char = string.find('\x19') - - if current_attrs and not tag_open and string: - build.append('<span style="%s">' % format_inline_css(current_attrs)) - tag_open = True - build.append(saxutils.escape(string)) - if tag_open: - build.append('</span>') - build.append("</p></body>") - text = ''.join(build) - return text.replace('\n', '<br />') |