From 673788bf46c71a9945d65b91bb1ba03e463ea31e Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 5 Apr 2014 17:50:50 +0200 Subject: Split the Core class Although the logic stays the same, and everything is put back together in a single class. --- src/core.py | 3870 ----------------------------------------------- src/core/__init__.py | 8 + src/core/commands.py | 892 +++++++++++ src/core/completions.py | 381 +++++ src/core/core.py | 1733 +++++++++++++++++++++ src/core/handlers.py | 1022 +++++++++++++ src/core/structs.py | 50 + 7 files changed, 4086 insertions(+), 3870 deletions(-) delete mode 100644 src/core.py create mode 100644 src/core/__init__.py create mode 100644 src/core/commands.py create mode 100644 src/core/completions.py create mode 100644 src/core/core.py create mode 100644 src/core/handlers.py create mode 100644 src/core/structs.py diff --git a/src/core.py b/src/core.py deleted file mode 100644 index 053c03b0..00000000 --- a/src/core.py +++ /dev/null @@ -1,3870 +0,0 @@ -# Copyright 2010-2011 Florent Le Coz -# -# This file is part of Poezio. -# -# Poezio is free software: you can redistribute it and/or modify -# it under the terms of the zlib license. See the COPYING file. - -from gettext import gettext as _ - -import curses -import os -import sys -import time -import pipes -import ssl - -from functools import reduce -from hashlib import sha1 -from threading import Event -from datetime import datetime -from xml.etree import cElementTree as ET - -import pep -import common -import theming -import logging -import singleton -import collections - -from sleekxmpp import InvalidJID -from common import safeJID -from sleekxmpp.xmlstream.stanzabase import StanzaBase -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath - -log = logging.getLogger(__name__) - -import multiuserchat as muc -import tabs - -import fixes -import decorators -import xhtml -import events -import pubsub -import windows -import connection -import timed_events -import bookmark - -from plugin_manager import PluginManager - -from data_forms import DataFormsTab -from config import config, firstrun, options as config_opts -from logger import logger -from roster import roster -from contact import Contact, Resource -from text_buffer import TextBuffer, CorrectionError -from keyboard import keyboard -from theming import get_theme, dump_tuple -from fifo import Fifo -from windows import g_lock -from daemon import Executor - -# 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') - -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() - status = config.get('status', None) - 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 - roster.set_node(self.xmpp.client_roster) - decorators.refresh_wrapper.core = self - self.paused = False - self.event = Event() - 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', 2, 'var') - self.information_win = windows.TextWin(300) - self.information_buffer.add_window(self.information_win) - - self.tab_win = windows.GlobalInfoBar() - # Number of xml tabs opened, used to avoid useless memory consumption - self.xml_tab = False - self.xml_buffer = TextBuffer() - - self.tabs = [] - self._current_tab_nb = 0 - self.previous_tab_nb = 0 - - self.own_nick = config.get('default_nick', '') or self.xmpp.boundjid.user or os.environ.get('USER') or 'poezio' - - self.plugins_autoloaded = False - self.plugin_manager = PluginManager(self) - self.events = events.EventHandler() - - - # 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', True): - del self.commands['status'] - del self.commands['show'] - - 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_': lambda arg: self.try_execute(arg), - } - self.key_func.update(key_func) - - # Add handlers - self.xmpp.add_event_handler('connected', self.on_connected) - self.xmpp.add_event_handler('disconnected', self.on_disconnected) - self.xmpp.add_event_handler('failed_auth', self.on_failed_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_invite) - 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("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.all_stanzas = Callback('custom matcher', connection.MatchAll(None), self.incoming_stanza) - self.xmpp.register_handler(self.all_stanzas) - if config.get('enable_user_tune', True): - self.xmpp.add_event_handler("user_tune_publish", self.on_tune_event) - if config.get('enable_user_nick', True): - self.xmpp.add_event_handler("user_nick_publish", self.on_nick_received) - if config.get('enable_user_mood', True): - self.xmpp.add_event_handler("user_mood_publish", self.on_mood_event) - if config.get('enable_user_activity', True): - self.xmpp.add_event_handler("user_activity_publish", self.on_activity_event) - if config.get('enable_user_gaming', True): - self.xmpp.add_event_handler("user_gaming_publish", self.on_gaming_event) - - self.initial_joins = [] - - self.timed_events = set() - - 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("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("", 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_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(filter(lambda x: bool(x), self.tabs)) - - 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_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 sigusr_handler(self, num, stack): - """ - Handle SIGUSR1 (10) - When caught, reload all the possible files. - """ - log.debug("SIGUSR1 caught, reloading the files…") - # 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…") - self.command_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(config.file_name) - # 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, "", 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 exit_from_signal(self, *args, **kwargs): - """ - Quit when receiving SIGHUP or SIGTERM - - do not save the config because it is not a normal exit - (and only roster UI things are not yet saved) - """ - log.debug("Either SIGHUP or SIGTERM received. Exiting…") - if config.get('enable_user_mood', True): - self.xmpp.plugin['xep_0107'].stop(block=False) - if config.get('enable_user_activity', True): - self.xmpp.plugin['xep_0108'].stop(block=False) - if config.get('enable_user_gaming', True): - self.xmpp.plugin['xep_0196'].stop(block=False) - self.plugin_manager.disable_plugins() - self.disconnect('') - self.running = False - try: - self.reset_curses() - except: # too bad - pass - sys.exit() - - 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!')) - if firstrun: - self.information(_( - 'It seems that it is the first time you start poezio.\n' - 'The online help is here http://poezio.eu/doc/en/\n' - 'No room is joined by default, but you can join poezio’s chatroom ' - '(with /join poezio@muc.poezio.eu), where you can ask for help or tell us how great it is.' - ), 'Help') - self.refresh_window() - - 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 main_loop(self): - """ - main loop waiting for the user to press a key - """ - def replace_line_breaks(key): - 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(len(char) > 0) - # 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 - - while self.running: - self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) - big_char_list = [replace_key_with_bound(key)\ - for key in self.read_keyboard()] - # whether to refresh after ALL keys have been handled - for char_list in separate_chars_from_bindings(big_char_list): - if self.paused: - self.current_tab().input.do_command(char_list[0]) - self.current_tab().input.prompt() - self.event.set() - continue - # 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: - 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: - res = self.do_command(replace_line_breaks(char), False) - else: - self.do_command(''.join(char_list), True) - self.doupdate() - - def save_config(self): - """ - Save config in the file just before exit - """ - if not roster.save_to_config_file() or \ - not config.silent_set('info_win_height', self.information_win_size, 'var'): - self.information(_('Unable to write 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', False): - # We just write the command in the fifo - if not self.remote_fifo: - try: - self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w') - except (OSError, IOError) as e: - log.error('Could not open the fifo for writing (%s)', - os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), - exc_info=True) - self.information('Could not open fifo file for writing: %s' % (e,), 'Error') - return - command_str = ' '.join([pipes.quote(arg.replace('\n', ' ')) for arg in command]) + '\n' - try: - self.remote_fifo.write(command_str) - except (IOError) as e: - log.error('Could not write in the fifo (%s): %s', - os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), - repr(command), - exc_info=True) - self.information('Could not execute %s: %s' % (command, e,), 'Error') - self.remote_fifo = None - else: - e = Executor(command) - try: - e.start() - except ValueError as e: - log.error('Could not execute command (%s)', repr(command), exc_info=True) - self.information('%s' % (e,), 'Error') - - - def do_command(self, key, raw): - if not key: - return - return 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""" - if event and event in self.timed_events: - self.timed_events.remove(event) - - def add_timed_event(self, event): - """Add a new timed event""" - self.timed_events.add(event) - - def check_timed_events(self): - """Check for the execution of timed events""" - now = datetime.now() - for event in self.timed_events: - if event.has_timed_out(now): - res = event() - if not res: - self.timed_events.remove(event) - break - - -####################### 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', True): - if not config.silent_set('status', pres if pres else '') or \ - not config.silent_set('status_message', msg.replace('\n', '|') if msg else ''): - self.information(_('Unable to write in the config file'), 'Error') - - def get_bookmark_nickname(self, room_name): - """ - Returns the nickname associated with a bookmark - or the default nickname - """ - bm = bookmark.get_by_jid(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) - """ - msg = msg or '' - for tab in self.get_tabs(tabs.MucTab): - tab.command_part(msg) - self.xmpp.disconnect() - if reconnect: - self.xmpp.start() - - 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 get_error_message(self, stanza, deprecated=False): - """ - Takes a stanza of the form - 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.get_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): - self.tabs[new_pos], self.tabs[old_pos] = self.tabs[old_pos], 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', False): - 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 - """ - char = self.read_keyboard()[0] - try: - nb1 = int(char) - except ValueError: - return - char = self.read_keyboard()[0] - try: - nb2 = int(char) - except ValueError: - return - self.command_win('%s%s' % (nb1, nb2)) - - def go_to_roster(self): - self.command_win('0') - - def go_to_previous_tab(self): - 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.get_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): - return self._current_tab_nb - - @current_tab_nb.setter - def current_tab_nb(self, value): - 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 - - ### 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.get_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, focus=True): - """ - Open a new tab.MucTab containing a muc Room, using the specified nick - """ - new_tab = tabs.MucTab(room, nick) - self.add_tab(new_tab, focus) - self.refresh_window() - - 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 = 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='\x195}You left the chatroom\x193}'): - """ - Disable private tabs when leaving a room - """ - for tab in self.get_tabs(tabs.PrivateTab): - if tab.get_name().startswith(room_name): - tab.deactivate(reason=reason) - - def enable_private_tabs(self, room_name, reason='\x195}You joined the chatroom\x193}'): - """ - Enable private tabs when joining a room - """ - for tab in self.get_tabs(tabs.PrivateTab): - if tab.get_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) - 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 - """ - 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 config.get('create_gaps', False): - 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.get_name() in logger.fds: - logger.fds[tab.get_name()].close() - log.debug("Log file for %s closed.", tab.get_name()) - del logger.fds[tab.get_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 - 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): - if not self.running or self.background is True: - 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) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - elif typ != '' and typ.lower() in config.get('information_buffer_popup_on', - 'error roster warning help info').split(): - popup_time = config.get('popup_time', 4) + (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 - """ - self.background = False # Bool to know if curses can draw - # or be quiet while an other console app is running. - 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 - """ - self.current_tab().state = 'current' - self.current_tab().refresh() - self.doupdate() - - def refresh_tab_win(self): - """ - Refresh the window containing the tab list - """ - self.current_tab().refresh_tab_win() - 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): - if self.information_win_size >= self.current_tab().height -5 or \ - self.information_win_size+nb >= self.current_tab().height-4: - return 0 - if self.information_win_size == 14: - return 0 - self.information_win_size += nb - if self.information_win_size > 14: - nb = nb - (self.information_win_size - 14) - self.information_win_size = 14 - 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): - if self.information_win_size == 0: - 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): - 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): - 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', False) - 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. - """ - with g_lock: - self.information_win.resize(self.information_win_size, tabs.Tab.width, - tabs.Tab.height - 1 - self.information_win_size - tabs.Tab.tab_win_height(), 0) - - def resize_global_info_bar(self): - """ - Resize the GlobalInfoBar only once at each resize - """ - with g_lock: - self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) - if config.get('enable_vertical_tab_list', False): - height, width = self.stdscr.getmaxyx() - truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0) - self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) - else: - 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 the draw themself in the portion - # of the screen that the can occupy, and we draw the tab list - # on the left remaining space - if config.get('enable_vertical_tab_list', False): - with g_lock: - scr = self.stdscr.subwin(0, config.get('vertical_tab_list_size', 20)) - else: - scr = self.stdscr - tabs.Tab.resize(scr) - self.resize_global_info_bar() - self.resize_global_information_win() - with g_lock: - for tab in self.tabs: - if config.get('lazy_resize', True): - 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. - get_user_input() has a timeout: it returns None when the timeout - occurs. In that case we do not return (we loop until we get - a non-None value), but we check for timed events instead. - """ - res = keyboard.get_user_input(self.stdscr) - while res is None: - self.check_timed_events() - res = keyboard.get_user_input(self.stdscr) - return res - - 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 - """ - keyboard.escape_next_key() - -####################### Commands and completions ############################## - - def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''): - if name in self.commands: - return - if not desc and shortdesc: - desc = shortdesc - self.commands[name] = Command(func, desc, completion, shortdesc, usage) - - def command_help(self, arg): - """ - /help - """ - args = arg.split() - if not args: - color = dump_tuple(get_theme().COLOR_HELP_COMMANDS) - acc = [] - buff = ['Global commands:'] - for command in self.commands: - if isinstance(self.commands[command], Command): - acc.append(' \x19%s}%s\x19o - %s' % (color, command, self.commands[command].short)) - else: - acc.append(' \x19%s}%s\x19o' % (color, command)) - acc = sorted(acc) - buff.extend(acc) - acc = [] - buff.append('Tab-specific commands:') - commands = self.current_tab().commands - for command in commands: - if isinstance(commands[command], Command): - acc.append(' \x19%s}%s\x19o - %s' % (color, command, commands[command].short)) - else: - acc.append(' \x19%s}%s\x19o' % (color, command)) - acc = sorted(acc) - buff.extend(acc) - - msg = '\n'.join(buff) - msg += _("\nType /help to know what each command does") - if args: - 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') - - 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 command_runkey(self, arg): - """ - /runkey - """ - def replace_line_breaks(key): - if key == '^J': - return '\n' - return key - char = arg.strip() - 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() - - 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 command_status(self, arg): - """ - /status [msg] - """ - args = common.shell_split(arg) - if len(args) < 1: - return - if not args[0] in possible_show.keys(): - self.command_help('status') - return - show = possible_show[args[0]] - if len(args) == 2: - msg = args[1] - else: - msg = None - 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() - if isinstance(current, tabs.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 isinstance(current, tabs.MucTab) and current.joined and show not in ('away', 'xa'): - current.send_chat_state('active') - - 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 command_presence(self, arg): - """ - /presence [type] [status] - """ - args = common.shell_split(arg) - if len(args) == 1: - jid, type, status = args[0], None, None - elif len(args) == 2: - jid, type, status = args[0], args[1], None - elif len(args) == 3: - jid, type, status = args[0], args[1], args[2] - else: - return - if jid == '.' and isinstance(self.current_tab(), tabs.ChatTab): - jid = self.current_tab().get_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) - - 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 command_theme(self, arg=''): - """/theme """ - args = arg.split() - if args: - self.command_set('theme %s' % (args[0],)) - warning = theming.reload_theme() - if warning: - self.information(warning, 'Warning') - self.refresh_window() - - 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')] - if not 'default' in theme_files: - theme_files.append('default') - return the_input.new_completion(theme_files, 1, '', quotify=False) - - def command_win(self, arg): - """ - /win - """ - arg = arg.strip() - if not arg: - self.command_help('win') - return - try: - nb = int(arg.split()[0]) - except ValueError: - nb = arg - 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() - - 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 command_move_tab(self, arg): - """ - /move_tab old_pos new_pos - """ - args = common.shell_split(arg) - current_tab = self.current_tab() - if len(args) != 2: - return self.command_help('move_tab') - def get_nb_from_value(value): - ref = None - try: - ref = int(value) - except ValueError: - old_tab = None - for tab in self.tabs: - if not old_tab and value == tab.get_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() - - def completion_move_tab(self, the_input): - """Completion for /move_tab""" - n = the_input.get_argument_position(quoted=True) - if n == 1: - nodes = [tab.get_name() for tab in self.tabs if tab] - nodes.remove('Roster') - return the_input.new_completion(nodes, 1, ' ', quotify=True) - - def command_list(self, arg): - """ - /list - Opens a MucListTab containing the list of the room in the specified server - """ - arg = arg.split() - if len(arg) > 1: - return self.command_help('list') - elif arg: - server = safeJID(arg[0]).server - else: - if not isinstance(self.current_tab(), tabs.MucTab): - return self.information('Please provide a server', 'Error') - server = safeJID(self.current_tab().get_name()).server - list_tab = tabs.MucListTab(server) - self.add_tab(list_tab, True) - self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received) - - 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.get_name() not in muc_serv_list: - muc_serv_list.append(safeJID(tab.get_name()).server) - if muc_serv_list: - return the_input.new_completion(muc_serv_list, 1, quotify=False) - - def command_version(self, arg): - """ - /version - """ - def callback(res): - 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') - - args = common.shell_split(arg) - if len(args) < 1: - 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) - - def completion_version(self, the_input): - """Completion for /version""" - n = the_input.get_argument_position(quoted=True) - 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=True) - - def command_join(self, arg, histo_length=None): - """ - /join [room][/nick] [password] - """ - args = common.shell_split(arg) - password = None - if len(args) == 0: - tab = self.current_tab() - if not isinstance(tab, tabs.MucTab) and not isinstance(tab, tabs.PrivateTab): - return - room = safeJID(tab.get_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 == '': - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick', '') - if nick == '': - nick = default - 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.get_name() - if nick == '': - nick = tab.own_nick - else: - room = info.bare - if room.find('@') == -1 and not server_root: # 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 isinstance(self.current_tab(), tabs.MucTab) and\ - self.current_tab().get_name().find('@') != -1: - room += '@%s' % safeJID(self.current_tab().get_name()).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 len(args) == 2: # a password is provided - password = args[1] - if tab and tab.joined: # if we are already in the room - self.focus_tab_named(tab.name) - if tab.own_nick == nick: - self.information('/join: Nothing to do.', 'Info') - else: - tab.own_nick = nick - tab.command_cycle('') - return - - if room.startswith('@'): - room = room[1:] - current_status = self.get_status() - if not histo_length: - histo_length= config.get('muc_history_length', 20) - if histo_length == -1: - histo_length= None - if histo_length is not None: - histo_length= str(histo_length) - if password is None: # try to use a saved password - password = config.get_by_tabname('password', None, room, fallback=False) - if tab and not tab.joined: - if tab.last_connection: - delta = datetime.now() - tab.last_connection - seconds = delta.seconds + delta.days * 24 * 3600 if tab.last_connection is not None else 0 - seconds = int(seconds) - else: - seconds = 0 - muc.join_groupchat(self, room, nick, password, - histo_length, current_status.message, current_status.show, seconds=seconds) - if not tab: - self.open_new_room(room, nick) - muc.join_groupchat(self, room, nick, password, - histo_length, current_status.message, current_status.show) - else: - tab.own_nick = nick - tab.users = [] - if tab and tab.joined: - self.enable_private_tabs(room) - tab.state = "normal" - if tab == self.current_tab(): - tab.refresh() - self.doupdate() - - 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 bookmark.bookmarks} - for tab in self.get_tabs(tabs.MucTab): - name = tab.get_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.server and not jid.user: - # no room was given: complete the node - try: - response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) - except: - log.error('/join completion: Unable to get the list of rooms for %s', - jid.server, - exc_info=True) - response = None - if response: - items = response['disco_items'].get_items() - else: - return True - items = sorted('%s/%s' % (tup[0], jid.resource) for tup in items) - return the_input.new_completion(items, 1, quotify=True, override=True) - elif 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.get_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) - return True - - def command_bookmark_local(self, arg=''): - """ - /bookmark_local [room][/nick] [password] - """ - args = common.shell_split(arg) - nick = None - password = None - if not args and not isinstance(self.current_tab(), tabs.MucTab): - return - if not args: - tab = self.current_tab() - roomname = tab.get_name() - if tab.joined and tab.own_nick != self.own_nick: - nick = tab.own_nick - elif args[0] == '*': - new_bookmarks = [] - for tab in self.get_tabs(tabs.MucTab): - b = bookmark.get_by_jid(tab.get_name()) - if not b: - b = bookmark.Bookmark(tab.get_name(), autojoin=True, method="local") - new_bookmarks.append(b) - else: - b.method = "local" - new_bookmarks.append(b) - bookmark.bookmarks.remove(b) - new_bookmarks.extend(bookmark.bookmarks) - bookmark.bookmarks = new_bookmarks - bookmark.save_local() - bookmark.save_remote(self.xmpp) - self.information('Bookmarks added and saved.', 'Info') - return - else: - info = safeJID(args[0]) - if info.resource != '': - nick = info.resource - roomname = info.bare - if not roomname: - if not isinstance(self.current_tab(), tabs.MucTab): - return - roomname = self.current_tab().get_name() - if len(args) > 1: - password = args[1] - - bm = bookmark.get_by_jid(roomname) - if not bm: - bm = bookmark.Bookmark(jid=roomname) - bookmark.bookmarks.append(bm) - self.information('Bookmark added.', 'Info') - else: - self.information('Bookmark updated.', 'Info') - if nick: - bm.nick = nick - bm.autojoin = True - bm.password = password - bm.method = "local" - bookmark.save_local() - self.information(_('Your local bookmarks are now: %s') % - [b for b in bookmark.bookmarks if b.method == 'local'], 'Info') - - 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.get_name() for tab in self.get_tabs(tabs.MucTab)] - muc_list.append('*') - return the_input.new_completion(muc_list, 1, quotify=True) - - def command_bookmark(self, arg=''): - """ - /bookmark [room][/nick] [autojoin] [password] - """ - - if not config.get('use_remote_bookmarks', True): - self.command_bookmark_local(arg) - return - args = common.shell_split(arg) - nick = None - if not args and not isinstance(self.current_tab(), tabs.MucTab): - return - if not args: - tab = self.current_tab() - roomname = tab.get_name() - if tab.joined: - nick = tab.own_nick - autojoin = True - password = None - elif args[0] == '*': - if len(args) > 1: - autojoin = False if args[1].lower() != 'true' else True - else: - autojoin = True - new_bookmarks = [] - for tab in self.get_tabs(tabs.MucTab): - b = bookmark.get_by_jid(tab.get_name()) - if not b: - b = bookmark.Bookmark(tab.get_name(), autojoin=autojoin, - method=bookmark.preferred) - new_bookmarks.append(b) - else: - b.method = bookmark.preferred - bookmark.bookmarks.remove(b) - new_bookmarks.append(b) - new_bookmarks.extend(bookmark.bookmarks) - bookmark.bookmarks = new_bookmarks - - if bookmark.save_remote(self.xmpp): - bookmark.save_local() - self.information("Bookmarks added.", "Info") - else: - self.information("Could not add the bookmarks.", "Info") - return - else: - info = safeJID(args[0]) - if info.resource != '': - nick = info.resource - roomname = info.bare - if roomname == '': - if not isinstance(self.current_tab(), tabs.MucTab): - return - roomname = self.current_tab().get_name() - if len(args) > 1: - autojoin = False if args[1].lower() != 'true' else True - else: - autojoin = True - if len(args) > 2: - password = args[2] - else: - password = None - bm = bookmark.get_by_jid(roomname) - if not bm: - bm = bookmark.Bookmark(roomname) - bookmark.bookmarks.append(bm) - bm.method = config.get('use_bookmarks_method', 'pep') - if nick: - bm.nick = nick - if password: - bm.password = password - bm.autojoin = autojoin - if bookmark.save_remote(self.xmpp): - self.information('Bookmark added.', 'Info') - self.information(_('Your remote bookmarks are now: %s') % - [b for b in bookmark.bookmarks if b.method in ('pep', 'privatexml')], 'Info') - - 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.get_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 command_bookmarks(self, arg=''): - """/bookmarks""" - self.information(_('Your remote bookmarks are: %s') % - [b for b in bookmark.bookmarks if b.method in ('pep', 'privatexml')], 'Info') - self.information(_('Your local bookmarks are: %s') % - [b for b in bookmark.bookmarks if b.method is 'local'], 'Info') - - def command_remove_bookmark(self, arg=''): - """/remove_bookmark [jid]""" - args = common.shell_split(arg) - if not args: - tab = self.current_tab() - if isinstance(tab, tabs.MucTab) and bookmark.get_by_jid(tab.get_name()): - bookmark.remove(tab.get_name()) - bookmark.save(self.xmpp) - if bookmark.save(self.xmpp): - self.information('Bookmark deleted', 'Info') - else: - self.information('No bookmark to remove', 'Info') - else: - if bookmark.get_by_jid(args[0]): - bookmark.remove(args[0]) - if bookmark.save(self.xmpp): - self.information('Bookmark deleted', 'Info') - - else: - self.information('No bookmark to remove', 'Info') - - def completion_remove_bookmark(self, the_input): - """Completion for /remove_bookmark""" - return the_input.new_completion([bm.jid for bm in bookmark.bookmarks], 1, quotify=False) - - def command_set(self, arg): - """ - /set [module|][section]