diff options
author | mathieui <mathieui@mathieui.net> | 2014-04-05 17:50:50 +0200 |
---|---|---|
committer | mathieui <mathieui@mathieui.net> | 2014-04-05 17:50:50 +0200 |
commit | 673788bf46c71a9945d65b91bb1ba03e463ea31e (patch) | |
tree | 56179a6fd4d6ecfd0a94a8a77cf5867b2a654bda | |
parent | 38061a63977af659ec1e20ad2c23975ae0655c5b (diff) | |
download | poezio-673788bf46c71a9945d65b91bb1ba03e463ea31e.tar.gz poezio-673788bf46c71a9945d65b91bb1ba03e463ea31e.tar.bz2 poezio-673788bf46c71a9945d65b91bb1ba03e463ea31e.tar.xz poezio-673788bf46c71a9945d65b91bb1ba03e463ea31e.zip |
Split the Core class
Although the logic stays the same, and everything is put back together
in a single class.
-rw-r--r-- | src/core.py | 3870 | ||||
-rw-r--r-- | src/core/__init__.py | 8 | ||||
-rw-r--r-- | src/core/commands.py | 892 | ||||
-rw-r--r-- | src/core/completions.py | 381 | ||||
-rw-r--r-- | src/core/core.py | 1733 | ||||
-rw-r--r-- | src/core/handlers.py | 1022 | ||||
-rw-r--r-- | src/core/structs.py | 50 |
7 files changed, 4086 insertions, 3870 deletions
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 <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. - -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 <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.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 <command_name> - """ - 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 <command_name> 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 <key> - """ - 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 <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 <JID> [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 <theme name>""" - 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 <number> - """ - 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 <server> - 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 <jid> - """ - 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] <option> <value> - """ - args = common.shell_split(arg) - if len(args) != 2 and len(args) != 3: - self.command_help('set') - return - if len(args) == 2: - 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: - return - plugin = self.plugin_manager.plugins[plugin_name] - info = plugin.config.set_and_save(option, value, section) - else: - section = args[0] - option = args[1] - value = args[2] - info = config.set_and_save(option, value, section) - self.trigger_configuration_change(option, value) - self.call_for_resize() - self.information(*info) - - 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 = config.options('Poezio') - 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.auto_completion([''], n, quotify=True) - plugin = self.plugin_manager.plugins[plugin_name] - end_list = plugin.config.options(section or plugin_name) - elif not config.has_option('Poezio', args[1]): - if config.has_section(args[1]): - end_list = config.options(args[1]) - end_list.append('') - else: - end_list = [] - else: - end_list = [config.get(args[1], ''), ''] - elif n == 3: - if '|' in args[1]: - plugin_name, section = args[1].split('|')[:2] - if not plugin_name in self.plugin_manager.plugins: - return the_input.auto_completion([''], n, quotify=True) - plugin = self.plugin_manager.plugins[plugin_name] - end_list = [plugin.config.get(args[2], '', section or plugin_name), ''] - else: - if not config.has_section(args[1]): - end_list = [''] - else: - end_list = [config.get(args[2], '', args[1]), ''] - else: - return - return the_input.new_completion(end_list, n, quotify=True) - - def command_server_cycle(self, arg=''): - """ - Do a /cycle on each room of the given server. If none, do it on the current tab - """ - args = common.shell_split(arg) - tab = self.current_tab() - message = "" - if len(args): - domain = args[0] - if len(args) > 1: - message = args[1] - else: - if isinstance(tab, tabs.MucTab): - domain = safeJID(tab.get_name()).domain - else: - self.information(_("No server specified"), "Error") - return - for tab in self.get_tabs(tabs.MucTab): - if tab.get_name().endswith(domain): - if tab.joined: - muc.leave_groupchat(tab.core.xmpp, tab.get_name(), tab.own_nick, message) - tab.joined = False - if tab.get_name() == domain: - self.command_join('"@%s/%s"' %(tab.get_name(), tab.own_nick)) - else: - self.command_join('"%s/%s"' %(tab.get_name(), tab.own_nick)) - - 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.get_name()).server - serv_list.add(serv) - return the_input.new_completion(sorted(serv_list), 1, ' ') - - def command_last_activity(self, arg): - """ - /last_activity <jid> - """ - def callback(iq): - 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') - jid = safeJID(arg) - if jid == '': - return self.command_help('last_activity') - self.xmpp.plugin['xep_0012'].get_last_activity(jid, block=False, callback=callback) - - 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 command_mood(self, arg): - """ - /mood [<mood> [text]] - """ - args = common.shell_split(arg) - if not args: - return self.xmpp.plugin['xep_0107'].stop(block=False) - 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) > 1: - text = args[1] - else: - text = None - self.xmpp.plugin['xep_0107'].publish_mood(mood, text, callback=dumb_callback, block=False) - - 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 command_activity(self, arg): - """ - /activity [<general> [specific] [text]] - """ - args = common.shell_split(arg) - length = len(args) - if not length: - return self.xmpp.plugin['xep_0108'].stop(block=False) - 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, block=False) - - def command_gaming(self, arg): - """ - /gaming [<game name> [server address]] - """ - args = common.shell_split(arg) - if not args: - return self.xmpp.plugin['xep_0196'].stop(block=False) - 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, block=False) - - 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 command_invite(self, arg): - """/invite <to> <room> [reason]""" - args = common.shell_split(arg) - if len(args) < 2: - return - reason = args[2] if len(args) > 2 else '' - to = safeJID(args[0]) - room = safeJID(args[1]) - self.xmpp.plugin['xep_0045'].invite(room, str(to), 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(sorted(jid for jid in roster.jids()), n, quotify=True) - elif n == 2: - rooms = [] - for tab in self.get_tabs(tabs.MucTab): - if tab.joined: - rooms.append(tab.get_name()) - rooms.sort() - return the_input.new_completion(rooms, n, '', quotify=True) - - def command_decline(self, arg): - """/decline <room@server.tld> [reason]""" - args = common.shell_split(arg) - if not len(args): - return - jid = safeJID(args[0]) - if jid.bare not in self.pending_invites: - return - reason = args[1] if len(args) > 1 else '' - del self.pending_invites[jid.bare] - self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason) - - 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) - - ### Commands without a completion in this class ### - - def command_invitations(self, arg=''): - """/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') - - def command_quit(self, arg=''): - """ - /quit - """ - if len(arg.strip()) != 0: - msg = arg - else: - msg = None - 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.save_config() - self.plugin_manager.disable_plugins() - self.disconnect(msg) - self.running = False - self.reset_curses() - sys.exit() - - 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) - - - return the_input - - def command_bind(self, arg): - """ - Bind a key. - """ - args = common.shell_split(arg) - if len(args) < 1: - return self.command_help('bind') - elif len(args) < 2: - args.append("") - 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') - - def command_pubsub(self, args): - """ - Opens a pubsub browser on the given domain - """ - args = common.shell_split(args) - if len(args) != 1: - return self.command_help('pubsub') - domain = args[0] - tab = self.get_tab_by_name('%s@@pubsubbrowser' % (domain,), pubsub.PubsubBrowserTab) - if tab: - self.command_win('%s' % tab.nb) - else: - new_tab = pubsub.PubsubBrowserTab(domain) - self.add_tab(new_tab, True) - self.refresh_window() - - def command_rawxml(self, arg): - """ - /rawxml <xml stanza> - """ - if not arg: - return - - - try: - stanza = StanzaBase(self.xmpp, xml=ET.fromstring(arg)) - if stanza.xml.tag == 'iq' and \ - stanza.xml.attrib.get('type') in ('get', 'set') and \ - stanza.xml.attrib.get('id'): - iq_id = stanza.xml.attrib.get('id') - - def iqfunc(iq): - 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(arg), - exc_info=True) - - - def command_load(self, arg): - """ - /load <plugin> - """ - args = arg.split() - if len(args) != 1: - self.command_help('load') - return - filename = args[0] - self.plugin_manager.load(filename) - - def command_unload(self, arg): - """ - /unload <plugin> - """ - args = arg.split() - if len(args) != 1: - self.command_help('unload') - return - filename = args[0] - self.plugin_manager.unload(filename) - - def command_plugins(self, arg=''): - """ - /plugins - """ - self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info') - - def command_message(self, arg): - """ - /message <jid> [message] - """ - args = common.shell_split(arg) - if len(args) < 1: - self.command_help('message') - return - 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) - if not tab: - tab = self.open_conversation_window(jid.full, focus=True) - else: - self.focus_tab_named(tab.get_name()) - if len(args) > 1: - tab.command_say(args[1]) - - def completion_message(self, the_input): - """Completion for /message""" - 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])), []) - 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 = bares + comp + off - return the_input.new_completion(comp, 1, '', quotify=True) - - def command_xml_tab(self, arg=''): - """/xml_tab""" - self.xml_tab = True - xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab) - if not xml_tab: - tab = tabs.XMLTab() - self.add_tab(tab, True) - - def command_self(self, arg=None): - """ - /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') - - 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."), - shortdesc=_('Move a tab.'), - completion=self.completion_move_tab) - 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('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>'), - shortdesc=_('Load the specified plugin'), - completion=self.plugin_manager.completion_load) - self.register_command('unload', self.command_unload, - usage=_('<plugin>'), - shortdesc=_('Unload the specified plugin'), - 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) - - if config.get('enable_user_activity', True): - 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', True): - 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', True): - 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 ################################## - - 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') - rostertab.check_blocking(features) - if (config.get('enable_carbons', True) and - 'urn:xmpp:carbons:2' in features): - self.xmpp.plugin['xep_0280'].enable() - self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) - self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent) - features = self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, callback=callback, block=False) - - def on_carbon_received(self, message): - recv = message['carbon_received'] - if recv['from'].bare not in roster or roster[recv['from'].bare].subscription == 'none': - try: - if self.xmpp.plugin['xep_0030'].has_identity(jid=recv['from'].server, category="conference"): - return - except: - pass - else: - return - recv['to'] = self.xmpp.boundjid.full - self.on_normal_message(recv) - - def on_carbon_sent(self, message): - sent = message['carbon_sent'] - if sent['to'].bare not in roster or roster[sent['to'].bare].subscription == 'none': - try: - if self.xmpp.plugin['xep_0030'].has_identity(jid=sent['to'].server, category="conference"): - return - except: - pass - else: - return - sent['from'] = self.xmpp.boundjid.full - self.on_normal_message(sent) - - ### Invites ### - - def on_groupchat_invite(self, message): - 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', 'invite').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): - pass - - ### "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.get_name() == jid_from.bare: - if message['type'] == 'error': - return self.room_error(message, jid_from) - else: - return self.on_groupchat_private_message(message) - return self.on_normal_message(message) - - def on_normal_message(self, message): - """ - When receiving "normal" messages (from someone in our roster) - """ - if message['type'] == 'error': - return self.information(self.get_error_message(message, deprecated=True), 'Error') - elif message['type'] == 'headline' and message['body']: - return self.information('%s says: %s' % (message['from'], message['body']), 'Headline') - - use_xhtml = config.get('enable_xhtml_im', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) - 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', True): - if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None: - remote_nick = message['nick']['nick'] - 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): - conversation.lock(conv_jid.resource) - - if not remote_nick and conversation.nick: - remote_nick = conversation.nick - elif not remote_nick or own: - remote_nick = conv_jid.user - conversation.nick = remote_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) - delayed, date = common.find_delayed_tag(message) - - def try_modify(): - replaced_id = message['replace']['id'] - if replaced_id and (config.get_by_tabname('group_corrections', - True, conv_jid.bare)): - 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 'private' in config.get('beep_on', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, conv_jid.bare, False): - curses.beep() - if self.current_tab() is not conversation: - conversation.state = 'private' - 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', False, 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', False, 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', False, 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', False, 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', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) - 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', True, message['from'].bare)): - try: - if tab.modify_message(body, replaced_id, message['id'], time=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', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, room_from, False): - 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', True) - body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) - tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation - ignore = config.get_by_tabname('ignore_private', False, room_from) - if not tab: # It's the first message we receive: create the tab - if body and not ignore: - tab = self.open_private_window(room_from, nick_from, False) - if ignore: - self.events.trigger('ignored_private', message, tab) - msg = config.get_by_tabname('private_auto_response', None, room_from) - 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) - if not body or not tab: - return - replaced_id = message['replace']['id'] - replaced = False - user = tab.parent_muc.get_user_by_name(nick_from) - if replaced_id is not '' and (config.get_by_tabname( - 'group_corrections', True, room_from)): - 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', 'highlight private').split(): - if not config.get_by_tabname('disable_beep', False, jid.full, False): - 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() - 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() - return True - - def on_chatstate_groupchat_conversation(self, message, state): - """ - Chatstate received in a MUC - """ - nick = message['mucnick'] - room_from = message.getMucroom() - 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(): - tab.user_win.refresh(tab.users) - tab.input.refresh() - self.doupdate() - - ### 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) - 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' % 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. - if jid.resource: - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full)) - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) - self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), '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.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) - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full)) - 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})" % (safeJID(resource.jid).bare, presence['status']), "Roster") - else: - self.information("\x193}%s \x195}is \x194}online\x195}" % safeJID(resource.jid).bare, "Roster") - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare)) - 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): - """ - We cannot contact the remote server - """ - self.information(_("Connection to remote server failed")) - - def on_disconnected(self, event): - """ - When we are disconnected from remote server - """ - roster.modified() - for tab in self.get_tabs(tabs.MucTab): - tab.disconnect() - self.information(_("Disconnected from server.")) - - def on_failed_auth(self, event): - """ - Authentication failed - """ - self.information(_("Authentication failed (bad credentials?).")) - - def on_no_auth(self, event): - """ - Authentication failed (no mech) - """ - self.information(_("Authentication failed, no login method available.")) - - def on_connected(self, event): - """ - Remote host responded, but we are not yet authenticated - """ - self.information(_("Connected to server.")) - - 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.")) - self.information(_("Your JID is %s") % self.xmpp.boundjid.full) - if not self.xmpp.anon: - # request the roster - self.xmpp.get_roster() - # send initial presence - if config.get('send_initial_presence', True): - pres = self.xmpp.make_presence() - pres['show'] = self.status.show - pres['status'] = self.status.message - self.events.trigger('send_normal_presence', pres) - pres.send() - bookmark.get_local() - if not self.xmpp.anon and config.get('use_remote_bookmarks', True): - bookmark.get_remote(self.xmpp) - for bm in bookmark.bookmarks: - tab = self.get_tab_by_name(bm.jid, tabs.MucTab) - nick = bm.nick if bm.nick else self.own_nick - if not tab: - self.open_new_room(bm.jid, nick, False) - self.initial_joins.append(bm.jid) - histo_length = config.get('muc_history_length', 20) - if histo_length == -1: - histo_length= None - if histo_length is not None: - histo_length= str(histo_length) - # 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, - maxhistory=histo_length, - status=self.status.message, - show=self.status.show) - - if config.get('enable_user_nick', True): - self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback, block=False) - self.xmpp.plugin['xep_0115'].update_caps() - - ### 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.getMucroom() - tab = self.get_tab_by_name(room_from, tabs.MucTab) - subject = message['subject'] - if not subject or not tab: - return - if nick_from: - tab.add_message(_("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject}, - time=None, - typ=2) - else: - tab.add_message(_("\x19%(info_col)s}The subject is: %(subject)s") % - {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, - time=None, - typ=2) - tab.topic = subject - if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): - self.refresh_window() - - 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.get_name() == jid_from: - tab.state = 'attention' - self.refresh_tab_win() - return - for tab in self.tabs: - if tab.get_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) - 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: - self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza) - 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: - self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza) - if isinstance(self.current_tab(), tabs.XMLTab): - self.current_tab().refresh() - self.doupdate() - - def validate_ssl(self, pem): - """ - Check the server certificate using the sleekxmpp ssl_cert event - """ - if config.get('ignore_certificate', False): - 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) - digest = sha1(der).hexdigest().upper() - found_cert = ':'.join(i + j for i, j in zip(digest[::2], digest[1::2])) - if cert: - if found_cert == cert: - log.debug('Cert %s OK', found_cert) - return - else: - saved_input = self.current_tab().input - log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert) - input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n) (%s)" % found_cert) - self.current_tab().input = input - input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) - input.refresh() - self.doupdate() - self.paused = True - while input.value is None: - self.event.wait() - self.current_tab().input = saved_input - self.paused = False - if input.value: - self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info') - log.debug('Setting certificate to %s', found_cert) - if not config.silent_set('certificate', 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.xmpp.disconnect() - else: - log.debug('First time. Setting certificate to %s', found_cert) - if not config.silent_set('certificate', found_cert): - self.information(_('Unable to write in the config file'), 'Error') - - - -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): - bind = config.get(key, key, 'bindings') - if not bind: - bind = key - return bind - -def dumb_callback(*args, **kwargs): - pass - diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 00000000..6a82e2bb --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,8 @@ +""" +Core class, splitted into smaller chunks +""" + +from . core import Core +from . structs import Command, Status, possible_show, DEPRECATED_ERRORS, \ + ERROR_AND_STATUS_CODES + diff --git a/src/core/commands.py b/src/core/commands.py new file mode 100644 index 00000000..fb8dc2eb --- /dev/null +++ b/src/core/commands.py @@ -0,0 +1,892 @@ +""" +Global commands which are to be linked to the Core class +""" + +import logging + +log = logging.getLogger(__name__) + +import os +import sys +from datetime import datetime +from gettext import gettext as _ +from xml.etree import cElementTree as ET + +from sleekxmpp.xmlstream.stanzabase import StanzaBase +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath + +import bookmark +import common +import fixes +import pep +import pubsub +import tabs +import theming +from common import safeJID +from config import config, options as config_opts +import multiuserchat as muc +from roster import roster +from theming import dump_tuple, get_theme + +from . structs import Command, possible_show + + +def command_help(self, arg): + """ + /help <command_name> + """ + 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 <command_name> 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 command_runkey(self, arg): + """ + /runkey <key> + """ + 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 command_status(self, arg): + """ + /status <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 command_presence(self, arg): + """ + /presence <JID> [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 command_theme(self, arg=''): + """/theme <theme name>""" + 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 command_win(self, arg): + """ + /win <number> + """ + 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 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 command_list(self, arg): + """ + /list <server> + 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 command_version(self, arg): + """ + /version <jid> + """ + 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 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 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 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 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 command_set(self, arg): + """ + /set [module|][section] <option> <value> + """ + args = common.shell_split(arg) + if len(args) != 2 and len(args) != 3: + self.command_help('set') + return + if len(args) == 2: + 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: + return + plugin = self.plugin_manager.plugins[plugin_name] + info = plugin.config.set_and_save(option, value, section) + else: + section = args[0] + option = args[1] + value = args[2] + info = config.set_and_save(option, value, section) + self.trigger_configuration_change(option, value) + self.call_for_resize() + self.information(*info) + +def command_server_cycle(self, arg=''): + """ + Do a /cycle on each room of the given server. If none, do it on the current tab + """ + args = common.shell_split(arg) + tab = self.current_tab() + message = "" + if len(args): + domain = args[0] + if len(args) > 1: + message = args[1] + else: + if isinstance(tab, tabs.MucTab): + domain = safeJID(tab.get_name()).domain + else: + self.information(_("No server specified"), "Error") + return + for tab in self.get_tabs(tabs.MucTab): + if tab.get_name().endswith(domain): + if tab.joined: + muc.leave_groupchat(tab.core.xmpp, tab.get_name(), tab.own_nick, message) + tab.joined = False + if tab.get_name() == domain: + self.command_join('"@%s/%s"' %(tab.get_name(), tab.own_nick)) + else: + self.command_join('"%s/%s"' %(tab.get_name(), tab.own_nick)) + +def command_last_activity(self, arg): + """ + /last_activity <jid> + """ + def callback(iq): + 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') + jid = safeJID(arg) + if jid == '': + return self.command_help('last_activity') + self.xmpp.plugin['xep_0012'].get_last_activity(jid, block=False, callback=callback) + +def command_mood(self, arg): + """ + /mood [<mood> [text]] + """ + args = common.shell_split(arg) + if not args: + return self.xmpp.plugin['xep_0107'].stop(block=False) + 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) > 1: + text = args[1] + else: + text = None + self.xmpp.plugin['xep_0107'].publish_mood(mood, text, callback=dumb_callback, block=False) + +def command_activity(self, arg): + """ + /activity [<general> [specific] [text]] + """ + args = common.shell_split(arg) + length = len(args) + if not length: + return self.xmpp.plugin['xep_0108'].stop(block=False) + 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, block=False) + +def command_gaming(self, arg): + """ + /gaming [<game name> [server address]] + """ + args = common.shell_split(arg) + if not args: + return self.xmpp.plugin['xep_0196'].stop(block=False) + 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, block=False) + +def command_invite(self, arg): + """/invite <to> <room> [reason]""" + args = common.shell_split(arg) + if len(args) < 2: + return + reason = args[2] if len(args) > 2 else '' + to = safeJID(args[0]) + room = safeJID(args[1]) + self.xmpp.plugin['xep_0045'].invite(room, str(to), reason) + +def command_decline(self, arg): + """/decline <room@server.tld> [reason]""" + args = common.shell_split(arg) + if not len(args): + return + jid = safeJID(args[0]) + if jid.bare not in self.pending_invites: + return + reason = args[1] if len(args) > 1 else '' + 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 ### + +def command_invitations(self, arg=''): + """/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') + +def command_quit(self, arg=''): + """ + /quit + """ + if len(arg.strip()) != 0: + msg = arg + else: + msg = None + 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.save_config() + self.plugin_manager.disable_plugins() + self.disconnect(msg) + self.running = False + self.reset_curses() + sys.exit() + +def command_bind(self, arg): + """ + Bind a key. + """ + args = common.shell_split(arg) + if len(args) < 1: + return self.command_help('bind') + elif len(args) < 2: + args.append("") + 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') + +def command_pubsub(self, args): + """ + Opens a pubsub browser on the given domain + """ + args = common.shell_split(args) + if len(args) != 1: + return self.command_help('pubsub') + domain = args[0] + tab = self.get_tab_by_name('%s@@pubsubbrowser' % (domain,), pubsub.PubsubBrowserTab) + if tab: + self.command_win('%s' % tab.nb) + else: + new_tab = pubsub.PubsubBrowserTab(domain) + self.add_tab(new_tab, True) + self.refresh_window() + +def command_rawxml(self, arg): + """ + /rawxml <xml stanza> + """ + if not arg: + return + + + try: + stanza = StanzaBase(self.xmpp, xml=ET.fromstring(arg)) + if stanza.xml.tag == 'iq' and \ + stanza.xml.attrib.get('type') in ('get', 'set') and \ + stanza.xml.attrib.get('id'): + iq_id = stanza.xml.attrib.get('id') + + def iqfunc(iq): + 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(arg), + exc_info=True) + + +def command_load(self, arg): + """ + /load <plugin> + """ + args = arg.split() + if len(args) != 1: + self.command_help('load') + return + filename = args[0] + self.plugin_manager.load(filename) + +def command_unload(self, arg): + """ + /unload <plugin> + """ + args = arg.split() + if len(args) != 1: + self.command_help('unload') + return + filename = args[0] + self.plugin_manager.unload(filename) + +def command_plugins(self, arg=''): + """ + /plugins + """ + self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info') + +def command_message(self, arg): + """ + /message <jid> [message] + """ + args = common.shell_split(arg) + if len(args) < 1: + self.command_help('message') + return + 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) + if not tab: + tab = self.open_conversation_window(jid.full, focus=True) + else: + self.focus_tab_named(tab.get_name()) + if len(args) > 1: + tab.command_say(args[1]) + +def command_xml_tab(self, arg=''): + """/xml_tab""" + self.xml_tab = True + xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab) + if not xml_tab: + tab = tabs.XMLTab() + self.add_tab(tab, True) + +def command_self(self, arg=None): + """ + /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') + +def dumb_callback(*args, **kwargs): + pass diff --git a/src/core/completions.py b/src/core/completions.py new file mode 100644 index 00000000..cc3dd381 --- /dev/null +++ b/src/core/completions.py @@ -0,0 +1,381 @@ +""" +Completions for the global commands +""" +import logging + +log = logging.getLogger(__name__) + +import os +from functools import reduce + +import bookmark +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')] + 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 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 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 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 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 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.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 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 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) + + + return the_input + + +def completion_message(self, the_input): + """Completion for /message""" + 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])), []) + 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 = bares + comp + off + return the_input.new_completion(comp, 1, '', quotify=True) + + + +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(sorted(jid for jid in roster.jids()), n, quotify=True) + elif n == 2: + rooms = [] + for tab in self.get_tabs(tabs.MucTab): + if tab.joined: + rooms.append(tab.get_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.get_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 = config.options('Poezio') + 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.auto_completion([''], n, quotify=True) + plugin = self.plugin_manager.plugins[plugin_name] + end_list = plugin.config.options(section or plugin_name) + elif not config.has_option('Poezio', args[1]): + if config.has_section(args[1]): + end_list = config.options(args[1]) + end_list.append('') + else: + end_list = [] + else: + end_list = [config.get(args[1], ''), ''] + elif n == 3: + if '|' in args[1]: + plugin_name, section = args[1].split('|')[:2] + if not plugin_name in self.plugin_manager.plugins: + return the_input.auto_completion([''], n, quotify=True) + plugin = self.plugin_manager.plugins[plugin_name] + end_list = [plugin.config.get(args[2], '', section or plugin_name), ''] + else: + if not config.has_section(args[1]): + end_list = [''] + else: + end_list = [config.get(args[2], '', args[1]), ''] + else: + return + return the_input.new_completion(end_list, n, quotify=True) + + + + +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) + + diff --git a/src/core/core.py b/src/core/core.py new file mode 100644 index 00000000..2ef6642d --- /dev/null +++ b/src/core/core.py @@ -0,0 +1,1733 @@ +""" +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 collections +import curses +import os +import pipes +import sys +import time +from threading import Event +from datetime import datetime +from gettext import gettext as _ + +from sleekxmpp.xmlstream.handler import Callback + +import bookmark +import connection +import decorators +import events +import singleton +import tabs +import theming +import timed_events +import windows + +from common import safeJID +from config import config, firstrun +from contact import Contact, Resource +from daemon import Executor +from data_forms import DataFormsTab +from fifo import Fifo +from keyboard import keyboard +from logger import logger +from plugin_manager import PluginManager +from roster import roster +from text_buffer import TextBuffer +from theming import get_theme +from windows import g_lock + +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() + 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 <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.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 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."), + shortdesc=_('Move a tab.'), + completion=self.completion_move_tab) + 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('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>'), + shortdesc=_('Load the specified plugin'), + completion=self.plugin_manager.completion_load) + self.register_command('unload', self.command_unload, + usage=_('<plugin>'), + shortdesc=_('Unload the specified plugin'), + 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) + + if config.get('enable_user_activity', True): + 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', True): + 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', True): + 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_invite = handlers.on_groupchat_invite + on_groupchat_decline = handlers.on_groupchat_decline + on_message = handlers.on_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_failed_auth = handlers.on_failed_auth + on_no_auth = handlers.on_no_auth + on_connected = handlers.on_connected + 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_attention = handlers.on_attention + room_error = handlers.room_error + outgoing_stanza = handlers.outgoing_stanza + incoming_stanza = handlers.incoming_stanza + validate_ssl = handlers.validate_ssl + 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_remove_bookmark = commands.command_remove_bookmark + command_set = commands.command_set + 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_pubsub = commands.command_pubsub + 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_self = commands.command_self + 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_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): + bind = config.get(key, key, 'bindings') + if not bind: + bind = key + return bind + + diff --git a/src/core/handlers.py b/src/core/handlers.py new file mode 100644 index 00000000..5b5cc6af --- /dev/null +++ b/src/core/handlers.py @@ -0,0 +1,1022 @@ +""" +XMPP-related handlers for the Core class +""" + +import logging +log = logging.getLogger(__name__) + +import curses +import ssl +import time +from hashlib import sha1 +from gettext import gettext as _ + +from sleekxmpp import InvalidJID +from sleekxmpp.xmlstream.stanzabase import StanzaBase + +import common +import xhtml +import pep +import tabs +import bookmark +import windows +import multiuserchat as muc +from common import safeJID +from config import config +from contact import Resource +from logger import logger +from roster import roster +from text_buffer import CorrectionError +from theming import dump_tuple, get_theme + +from . commands import dumb_callback + +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') + rostertab.check_blocking(features) + if (config.get('enable_carbons', True) and + 'urn:xmpp:carbons:2' in features): + self.xmpp.plugin['xep_0280'].enable() + self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) + self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent) + features = self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, callback=callback, block=False) + +def on_carbon_received(self, message): + recv = message['carbon_received'] + if recv['from'].bare not in roster or roster[recv['from'].bare].subscription == 'none': + try: + if self.xmpp.plugin['xep_0030'].has_identity(jid=recv['from'].server, category="conference"): + return + except: + pass + else: + return + recv['to'] = self.xmpp.boundjid.full + self.on_normal_message(recv) + +def on_carbon_sent(self, message): + sent = message['carbon_sent'] + if sent['to'].bare not in roster or roster[sent['to'].bare].subscription == 'none': + try: + if self.xmpp.plugin['xep_0030'].has_identity(jid=sent['to'].server, category="conference"): + return + except: + pass + else: + return + sent['from'] = self.xmpp.boundjid.full + self.on_normal_message(sent) + +### Invites ### + +def on_groupchat_invite(self, message): + 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', 'invite').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): + pass + +### "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.get_name() == jid_from.bare: + if message['type'] == 'error': + return self.room_error(message, jid_from) + else: + return self.on_groupchat_private_message(message) + return self.on_normal_message(message) + +def on_normal_message(self, message): + """ + When receiving "normal" messages (from someone in our roster) + """ + if message['type'] == 'error': + return self.information(self.get_error_message(message, deprecated=True), 'Error') + elif message['type'] == 'headline' and message['body']: + return self.information('%s says: %s' % (message['from'], message['body']), 'Headline') + + use_xhtml = config.get('enable_xhtml_im', True) + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + 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', True): + if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None: + remote_nick = message['nick']['nick'] + 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): + conversation.lock(conv_jid.resource) + + if not remote_nick and conversation.nick: + remote_nick = conversation.nick + elif not remote_nick or own: + remote_nick = conv_jid.user + conversation.nick = remote_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) + delayed, date = common.find_delayed_tag(message) + + def try_modify(): + replaced_id = message['replace']['id'] + if replaced_id and (config.get_by_tabname('group_corrections', + True, conv_jid.bare)): + 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 'private' in config.get('beep_on', 'highlight private').split(): + if not config.get_by_tabname('disable_beep', False, conv_jid.bare, False): + curses.beep() + if self.current_tab() is not conversation: + conversation.state = 'private' + 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', False, 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', False, 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', False, 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', False, 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', True) + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + 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', True, message['from'].bare)): + try: + if tab.modify_message(body, replaced_id, message['id'], time=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', 'highlight private').split(): + if not config.get_by_tabname('disable_beep', False, room_from, False): + 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', True) + body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml) + tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation + ignore = config.get_by_tabname('ignore_private', False, room_from) + if not tab: # It's the first message we receive: create the tab + if body and not ignore: + tab = self.open_private_window(room_from, nick_from, False) + if ignore: + self.events.trigger('ignored_private', message, tab) + msg = config.get_by_tabname('private_auto_response', None, room_from) + 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) + if not body or not tab: + return + replaced_id = message['replace']['id'] + replaced = False + user = tab.parent_muc.get_user_by_name(nick_from) + if replaced_id is not '' and (config.get_by_tabname( + 'group_corrections', True, room_from)): + 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', 'highlight private').split(): + if not config.get_by_tabname('disable_beep', False, jid.full, False): + 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() + 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() + return True + +def on_chatstate_groupchat_conversation(self, message, state): + """ + Chatstate received in a MUC + """ + nick = message['mucnick'] + room_from = message.getMucroom() + 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(): + tab.user_win.refresh(tab.users) + tab.input.refresh() + self.doupdate() + +### 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) + 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' % 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. + if jid.resource: + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full)) + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) + self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), '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.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) + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full)) + 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})" % (safeJID(resource.jid).bare, presence['status']), "Roster") + else: + self.information("\x193}%s \x195}is \x194}online\x195}" % safeJID(resource.jid).bare, "Roster") + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare)) + 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): + """ + We cannot contact the remote server + """ + self.information(_("Connection to remote server failed")) + +def on_disconnected(self, event): + """ + When we are disconnected from remote server + """ + roster.modified() + for tab in self.get_tabs(tabs.MucTab): + tab.disconnect() + self.information(_("Disconnected from server.")) + +def on_failed_auth(self, event): + """ + Authentication failed + """ + self.information(_("Authentication failed (bad credentials?).")) + +def on_no_auth(self, event): + """ + Authentication failed (no mech) + """ + self.information(_("Authentication failed, no login method available.")) + +def on_connected(self, event): + """ + Remote host responded, but we are not yet authenticated + """ + self.information(_("Connected to server.")) + +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.")) + self.information(_("Your JID is %s") % self.xmpp.boundjid.full) + if not self.xmpp.anon: + # request the roster + self.xmpp.get_roster() + # send initial presence + if config.get('send_initial_presence', True): + pres = self.xmpp.make_presence() + pres['show'] = self.status.show + pres['status'] = self.status.message + self.events.trigger('send_normal_presence', pres) + pres.send() + bookmark.get_local() + if not self.xmpp.anon and config.get('use_remote_bookmarks', True): + bookmark.get_remote(self.xmpp) + for bm in bookmark.bookmarks: + tab = self.get_tab_by_name(bm.jid, tabs.MucTab) + nick = bm.nick if bm.nick else self.own_nick + if not tab: + self.open_new_room(bm.jid, nick, False) + self.initial_joins.append(bm.jid) + histo_length = config.get('muc_history_length', 20) + if histo_length == -1: + histo_length= None + if histo_length is not None: + histo_length= str(histo_length) + # 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, + maxhistory=histo_length, + status=self.status.message, + show=self.status.show) + + if config.get('enable_user_nick', True): + self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback, block=False) + self.xmpp.plugin['xep_0115'].update_caps() + +### 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.getMucroom() + tab = self.get_tab_by_name(room_from, tabs.MucTab) + subject = message['subject'] + if not subject or not tab: + return + if nick_from: + tab.add_message(_("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % + {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject}, + time=None, + typ=2) + else: + tab.add_message(_("\x19%(info_col)s}The subject is: %(subject)s") % + {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, + time=None, + typ=2) + tab.topic = subject + if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): + self.refresh_window() + +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.get_name() == jid_from: + tab.state = 'attention' + self.refresh_tab_win() + return + for tab in self.tabs: + if tab.get_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) + 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: + self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza) + 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: + self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza) + if isinstance(self.current_tab(), tabs.XMLTab): + self.current_tab().refresh() + self.doupdate() + +def validate_ssl(self, pem): + """ + Check the server certificate using the sleekxmpp ssl_cert event + """ + if config.get('ignore_certificate', False): + 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) + digest = sha1(der).hexdigest().upper() + found_cert = ':'.join(i + j for i, j in zip(digest[::2], digest[1::2])) + if cert: + if found_cert == cert: + log.debug('Cert %s OK', found_cert) + return + else: + saved_input = self.current_tab().input + log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert) + input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n) (%s)" % found_cert) + self.current_tab().input = input + input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) + input.refresh() + self.doupdate() + self.paused = True + while input.value is None: + self.event.wait() + self.current_tab().input = saved_input + self.paused = False + if input.value: + self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info') + log.debug('Setting certificate to %s', found_cert) + if not config.silent_set('certificate', 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.xmpp.disconnect() + else: + log.debug('First time. Setting certificate to %s', found_cert) + if not config.silent_set('certificate', found_cert): + self.information(_('Unable to write in the config file'), 'Error') + + diff --git a/src/core/structs.py b/src/core/structs.py new file mode 100644 index 00000000..d97acd9f --- /dev/null +++ b/src/core/structs.py @@ -0,0 +1,50 @@ +""" +Module defining structures useful to the core class and related methods +""" +import collections +from gettext import gettext as _ + +# 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') |