diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/core.py | 130 | ||||
-rwxr-xr-x | src/daemon.py | 50 | ||||
-rw-r--r-- | src/data_forms.py | 21 | ||||
-rw-r--r-- | src/events.py | 70 | ||||
-rw-r--r-- | src/fifo.py | 70 | ||||
-rw-r--r-- | src/plugin.py | 92 | ||||
-rw-r--r-- | src/plugin_manager.py | 137 | ||||
-rw-r--r-- | src/tabs.py | 29 | ||||
-rw-r--r-- | src/xhtml.py | 2 |
9 files changed, 575 insertions, 26 deletions
diff --git a/src/core.py b/src/core.py index 2bea288c..e2ba8ce1 100644 --- a/src/core.py +++ b/src/core.py @@ -16,6 +16,8 @@ import traceback from datetime import datetime +from inspect import getargspec + import common import theming import logging @@ -30,11 +32,14 @@ import multiuserchat as muc import tabs import xhtml +import events import pubsub import windows import connection import timed_events +from plugin_manager import PluginManager + from data_forms import DataFormsTab from config import config, options from logger import logger @@ -43,6 +48,7 @@ from contact import Contact, Resource from text_buffer import TextBuffer from keyboard import read_char from theming import get_theme +from fifo import Fifo from windows import g_lock # http://xmpp.org/extensions/xep-0045.html#errorstatus @@ -72,6 +78,7 @@ class Core(object): """ User interface using ncurses """ + def __init__(self): # All uncaught exception are given to this callback, instead # of being displayed on the screen and exiting the program. @@ -79,7 +86,9 @@ class Core(object): self.status = Status(show=None, message='') sys.excepthook = self.on_exception self.running = True + self.events = events.EventHandler() self.xmpp = singleton.Singleton(connection.Connection) + self.remote_fifo = None # a unique buffer used to store global informations # that are displayed in almost all tabs, in an # information window. @@ -98,6 +107,7 @@ class Core(object): # 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.plugin_manager = PluginManager(self) self.commands = { 'help': (self.command_help, '\_o< KOIN KOIN KOIN', self.completion_help), 'join': (self.command_join, _("Usage: /join [room_name][@server][/nick] [password]\nJoin: 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"), self.completion_join), @@ -109,10 +119,7 @@ class Core(object): 'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), self.completion_win), 'show': (self.command_status, _('Usage: /show <availability> [status message]\nShow: 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.'), self.completion_status), 'status': (self.command_status, _('Usage: /status <availability> [status message]\nStatus: 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.'), self.completion_status), - 'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optionally) your status message. This is equivalent to '/status away [message]'"), None), - 'busy': (self.command_busy, _("Usage: /busy [message]\nBusy: Sets your availability to busy and (optionally) your status message. This is equivalent to '/status busy [message]'"), None), - 'available': (self.command_avail, _("Usage: /available [message]\nAvailable: Sets your availability to available and (optionally) your status message. This is equivalent to '/status available [message]'"), None), - 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark 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 syntaxe 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)"), None), + 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark 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 syntaxe 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)"), None), 'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Set the value of the 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 an empty value (nothing) by providing no [value] after <option>."), None), 'theme': (self.command_theme, _('Usage: /theme [theme_name]\nTheme: Reload the theme defined in the config file. If theme_name is provided, set that theme before reloading it.'), None), 'list': (self.command_list, _('Usage: /list\nList: Get the list of public chatrooms on the specified server.'), self.completion_list), @@ -121,6 +128,9 @@ class Core(object): 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: Disconnect from the remote server if you are currently connected and then connect to it again.'), None), 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: Disconnect and reconnect in all the rooms in domain.'), None), 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: Bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same as the Up key.'), None), + 'load': (self.command_load, _('Usage: /load <plugin>\nLoad: Load the specified plugin'), self.plugin_manager.completion_load), + 'unload': (self.command_unload, _('Usage: /unload <plugin>\nUnload: Unload the specified plugin'), self.plugin_manager.completion_unload), + 'plugins': (self.command_plugins, _('Usage: /plugins\nPlugins: Show the plugins in use.'), None), } self.key_func = { @@ -165,6 +175,15 @@ class Core(object): self.timed_events = set() + self.connected_events = {} + + self.autoload_plugins() + + def autoload_plugins(self): + plugins = config.get('plugins_autoload', '') + for plugin in plugins.split(): + self.plugin_manager.load(plugin) + def start(self): """ Init curses, create the first tab, etc @@ -184,8 +203,7 @@ class Core(object): if config.get('firstrun', ''): self.information(_( 'It seems that it is the first time you start poezio.\n' + \ - 'The configuration help is here: http://dev.louiz.org/project/poezio/doc/HowToConfigure\n' + \ - 'And the documentation for users is here: http://dev.louiz.org/project/poezio/doc/HowToUse\n' + \ + 'The online help is here http://poezio.eu/en/documentation.php.\n' + \ 'By default, you are in poezio’s chatroom, where you can ask for help or tell us how great it is.\n' + \ 'Just press Ctrl-n.' \ )) @@ -547,6 +565,7 @@ class Core(object): tab = self.open_private_window(room_from, nick_from, False) if not tab: return + self.events.trigger('private_msg', message) body = xhtml.get_body_from_message_stanza(message) if not body: return @@ -596,6 +615,7 @@ class Core(object): When receiving "normal" messages (from someone in our roster) """ jid = message['from'] + self.events.trigger('conversation_msg', message) body = xhtml.get_body_from_message_stanza(message) if not body: if message['type'] == 'error': @@ -1036,6 +1056,7 @@ class Core(object): if tab.get_user_by_name(nick_from) and\ tab.get_user_by_name(nick_from) in tab.ignores: return + self.events.trigger('muc_msg', message) body = xhtml.get_body_from_message_stanza(message) if body: date = date if delayed == True else None @@ -1117,6 +1138,34 @@ class Core(object): def completion_status(self, the_input): return the_input.auto_completion([status for status in possible_show], ' ') + 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] @@ -1393,24 +1442,6 @@ class Core(object): msg = "%s=%s" % (option, value) self.information(msg, 'Info') - def command_away(self, arg): - """ - /away [msg] - """ - self.command_status("away "+arg) - - def command_busy(self, arg): - """ - /busy [msg] - """ - self.command_status("busy "+arg) - - def command_avail(self, arg): - """ - /avail [msg] - """ - self.command_status("available "+arg) - def close_tab(self, tab=None): """ Close the given tab. If None, close the current one @@ -1602,3 +1633,54 @@ class Core(object): if not self.running or self.background is True: return curses.doupdate() + + 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 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 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 reads on that fifo, + and executes any command that is read in it. + """ + command = '%s\n' % (command,) + if config.get('exec_remote', 'false') == 'true': + # 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: + self.information('Could not open fifo file for writing: %s' % (e,), 'Error') + return + try: + self.remote_fifo.write(command) + except (IOError) as e: + self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error') + self.remote_fifo = None + else: + pass + + 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() diff --git a/src/daemon.py b/src/daemon.py new file mode 100755 index 00000000..f23d6b5f --- /dev/null +++ b/src/daemon.py @@ -0,0 +1,50 @@ +#/usr/bin/env python3 +# Copyright 2011 Florent Le Coz <louiz@louiz.org> +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +This file is a standalone program that reads commands on +stdin and executes them (each line should be a command). + +Usage: cat some_fifo | ./daemon.py + +Poezio writes commands in the fifo, and this daemon executes them on the +local machine. +Note that you should not start this daemon if you do not trust the remote +machine that is running poezio, since this could make it run any (dangerous) +command on your local machine. +""" + +import sys +import threading +import subprocess + +class Executor(threading.Thread): + """ + Just a class to execute commands in a thread. + This way, the execution can totally fail, we don’t care, + and we can start commands without having to wait for them + to return + """ + def __init__(self, command): + threading.Thread.__init__(self) + self.command = command + + def run(self): + print('executing %s' % (self.command.strip(),)) + subprocess.call(self.command.split()) + +def main(): + while True: + line = sys.stdin.readline() + if line == '': + break + e = Executor(line) + e.start() + +if __name__ == '__main__': + main() diff --git a/src/data_forms.py b/src/data_forms.py index a7bbe97f..8445d3d2 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -152,6 +152,26 @@ class DummyInput(FieldInput, windows.Win): def is_dummy(self): return True +class ColoredLabel(windows.Win): + def __init__(self, text): + self.text = text + self.color = 14 + windows.Win.__init__(self) + + def resize(self, height, width, y, x): + self._resize(height, width, y, x) + + def set_color(self, color): + self.color = color + self.refresh() + + def refresh(self): + with g_lock: + self._win.attron(curses.color_pair(self.color)) + self.addstr(0, 0, self.text) + self._win.attroff(curses.color_pair(self.color)) + self._refresh() + class BooleanWin(FieldInput, windows.Win): def __init__(self, field): FieldInput.__init__(self, field) @@ -502,6 +522,7 @@ class FormWin(object): for i, inp in enumerate(self.inputs): if i >= self.height: break + inp['label'].refresh() inp['input'].refresh() inp['label'].refresh() if self.current_input < self.height-1: diff --git a/src/events.py b/src/events.py new file mode 100644 index 00000000..22d60ddf --- /dev/null +++ b/src/events.py @@ -0,0 +1,70 @@ +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Defines the EventHandler class +""" + +import logging +log = logging.getLogger(__name__) + +class EventHandler(object): + """ + A class keeping a list of possible events that are triggered + by poezio. You (a plugin for example) can add an event handler + associated with an event name, and whenever that event is triggered, + the callback is called + """ + def __init__(self): + self.events = { + # when you are highlighted in a muc tab + 'highlight': [], + 'muc_say': [], + 'conversation_say': [], + 'private_say': [], + 'conversation_msg': [], + 'private_msg': [], + 'muc_msg': [], + } + + def add_event_handler(self, name, callback, position=0): + """ + Add a callback to a given event. + Note that if that event name doesn’t exist, it just returns False. + If it was successfully added, it returns True + position: 0 means insert a the beginning, -1 means end + """ + if name not in self.events: + return False + + if position >= 0: + self.events[name].insert(position, callback) + else: + self.events[name].append(callback) + + return True + + def trigger(self, name, *args, **kwargs): + """ + Call all the callbacks associated to the given event name + """ + callbacks = self.events[name] + for callback in callbacks: + callback(*args, **kwargs) + + def del_event_handler(self, name, callback): + """ + Remove the callback from the list of callbacks of the given event + """ + if not name: + for event in self.events: + while callback in self.events[event]: + self.events[event].remove(callback) + return True + else: + if callback in self.events[name]: + self.events[name].remove(callback) + diff --git a/src/fifo.py b/src/fifo.py new file mode 100644 index 00000000..8306e24b --- /dev/null +++ b/src/fifo.py @@ -0,0 +1,70 @@ +# Copyright 2011 Florent Le Coz <louiz@louiz.org> +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +Defines the Fifo class +""" + +import logging +log = logging.getLogger(__name__) + +import os +import threading + +class OpenTrick(threading.Thread): + """ + A threaded trick to make the open for writing succeed. + A fifo cannot be opened for writing if it has not been + yet opened by the other hand for reading. + So, we just open the fifo for reading and close it + immediately afterwards. + Once that is done, we can freely keep the fifo open for + writing and write things in it. The writing can fail if + there’s still nothing reading that fifo, but we just yell + an error in that case. + """ + def __init__(self, path): + threading.Thread.__init__(self) + self.path = path + + def run(self): + open(self.path, 'r').close() + + +class Fifo(object): + """ + Just a simple file handler, writing and reading in a fifo. + Mode is either 'r' or 'w', just like the mode for the open() + function. + """ + def __init__(self, path, mode): + self.trick = None + if not os.path.exists(path): + os.mkfifo(path) + if mode == 'w': + self.trick = OpenTrick(path) + # that thread will wait until we open it for writing + self.trick.start() + self.fd = open(path, mode) + + def write(self, data): + """ + Try to write on the fifo. If that fails, this means + that nothing has that fifo opened, so the writing is useless, + so we just return (and display an error telling that, somewhere). + """ + self.fd.write(data) + self.fd.flush() + + def readline(self): + return self.fd.readline() + + def __del__(self): + try: + self.fd.close() + except: + pass diff --git a/src/plugin.py b/src/plugin.py new file mode 100644 index 00000000..80bc4dfc --- /dev/null +++ b/src/plugin.py @@ -0,0 +1,92 @@ +import os +from configparser import ConfigParser +import config +import inspect +import traceback + +class PluginConfig(config.Config): + def __init__(self, filename): + ConfigParser.__init__(self) + self.__config_file__ = filename + self.read() + + def read(self): + """Read the config file""" + ConfigParser.read(self, self.__config_file__) + + def write(self): + """Write the config to the disk""" + try: + fp = open(self.__config_file__, 'w') + ConfigParser.write(self, fp) + fp.close() + return True + except IOError: + return False + + +class SafetyMetaclass(type): + # A hack + core = None + + @staticmethod + def safe_func(f): + def helper(*args, **kwargs): + try: + return f(*args, **kwargs) + except: + if inspect.stack()[1][1] == inspect.getfile(f): + raise + elif SafetyMetaclass.core: + SafetyMetaclass.core.information(traceback.format_exc()) + return None + return helper + + def __new__(meta, name, bases, class_dict): + for k, v in class_dict.items(): + if inspect.isfunction(v): + class_dict[k] = SafetyMetaclass.safe_func(v) + return type.__new__(meta, name, bases, class_dict) + +class BasePlugin(object, metaclass=SafetyMetaclass): + """ + Class that all plugins derive from. Any methods beginning with command_ + are interpreted as a command and beginning with on_ are interpreted as + event handlers + """ + + def __init__(self, plugin_manager, core, plugins_conf_dir): + self.core = core + # More hack; luckily we'll never have more than one core object + SafetyMetaclass.core = core + self.plugin_manager = plugin_manager + conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg') + self.config = PluginConfig(conf) + self.init() + + def init(self): + pass + + def cleanup(self): + pass + + def unload(self): + self.cleanup() + + def add_command(self, name, handler, help, completion=None): + return self.plugin_manager.add_command(self.__module__, name, handler, help, completion) + + def del_command(self, name): + return self.plugin_manager.del_command(self.__module__, name) + + def add_event_handler(self, event_name, handler): + return self.plugin_manager.add_event_handler(self.__module__, event_name, handler) + + def del_event_handler(self, event_name, handler): + return self.plugin_manager.del_event_handler(self.__module__, event_name, handler) + + def add_poezio_event_handler(self, event_name, handler, position=0): + return self.plugin_manager.add_poezio_event_handler(self.__module__, event_name, handler, position) + + def del_poezio_event_handler(self, event_name, handler): + return self.plugin_manager.del_poezio_event_handler(self.__module__, event_name, handler) diff --git a/src/plugin_manager.py b/src/plugin_manager.py new file mode 100644 index 00000000..bdf94a5b --- /dev/null +++ b/src/plugin_manager.py @@ -0,0 +1,137 @@ +import imp +import os +import sys +from config import config +from gettext import gettext as _ + +plugins_dir = config.get('plugins_dir', '') +plugins_dir = plugins_dir or\ + os.path.join(os.environ.get('XDG_DATA_HOME') or\ + os.path.join(os.environ.get('HOME'), '.local', 'share'), + 'poezio', 'plugins') + +config_home = os.environ.get("XDG_CONFIG_HOME") +if not config_home: + config_home = os.path.join(os.environ.get('HOME'), '.config') +plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins') + +try: + os.makedirs(plugins_dir) +except OSError: + pass + +try: + os.makedirs(plugins_conf_dir) +except OSError: + pass + +sys.path.append(plugins_dir) + +class PluginManager(object): + def __init__(self, core): + self.core = core + self.modules = {} # module name -> module object + self.plugins = {} # module name -> plugin object + self.commands = {} # module name -> dict of commands loaded for the module + self.event_handlers = {} # module name -> list of event_name/handler pairs loaded for the module + self.poezio_event_handlers = {} + + def load(self, name): + if name in self.plugins: + self.unload(name) + + try: + if name in self.modules: + imp.acquire_lock() + module = imp.reload(self.modules[name]) + imp.release_lock() + else: + file, filename, info = imp.find_module(name, [plugins_dir]) + imp.acquire_lock() + module = imp.load_module(name, file, filename, info) + imp.release_lock() + except Exception as e: + import traceback + self.core.information(_("Could not load plugin: ") + traceback.format_exc(), 'Error') + return + finally: + if imp.lock_held(): + imp.release_lock() + + self.modules[name] = module + self.commands[name] = {} + self.event_handlers[name] = [] + self.poezio_event_handlers[name] = [] + self.plugins[name] = module.Plugin(self, self.core, plugins_conf_dir) + + def unload(self, name): + if name in self.plugins: + try: + for command in self.commands[name].keys(): + del self.core.commands[command] + for event_name, handler in self.event_handlers[name]: + self.core.xmpp.del_event_handler(event_name, handler) + for handler in self.poezio_event_handlers[name]: + self.core.events.del_event_handler(None, handler) + + self.plugins[name].unload() + del self.plugins[name] + del self.commands[name] + del self.event_handlers[name] + del self.poezio_event_handlers[name] + except Exception as e: + import traceback + self.core.information(_("Could not unload plugin (may not be safe to try again): ") + traceback.format_exc()) + + def del_command(self, module_name, name): + if name in self.commands[module_name]: + del self.commands[module_name][name] + if name in self.core.commands: + del self.core.commands[name] + + def add_command(self, module_name, name, handler, help, completion=None): + if name in self.core.commands: + raise Exception(_("Command '%s' already exists") % (name,)) + + commands = self.commands[module_name] + commands[name] = (handler, help, completion) + self.core.commands[name] = (handler, help, completion) + + def add_event_handler(self, module_name, event_name, handler): + eh = self.event_handlers[module_name] + eh.append((event_name, handler)) + self.core.xmpp.add_event_handler(event_name, handler) + + def del_event_handler(self, module_name, event_name, handler): + self.core.xmpp.del_event_handler(event_name, handler) + eh = self.event_handlers[module_name] + eh = list(filter(lambda e : e != (event_name, handler), eh)) + + def add_poezio_event_handler(self, module_name, event_name, handler, position): + eh = self.poezio_event_handlers[module_name] + eh.append(handler) + self.core.events.add_event_handler(event_name, handler, position) + + def del_poezio_event_handler(self, module_name, event_name, handler): + self.core.events.del_event_handler(None, handler) + eh = self.poezio_event_handlers[module_name] + eh = list(filter(lambda e : e != handler, eh)) + + def completion_load(self, the_input): + """ + completion function that completes the name of the plugins, from + all .py files in plugins_dir + """ + try: + names = os.listdir(plugins_dir) + except OSError as e: + self.core.information(_('Completion failed: %s' % e), 'Error') + return + plugins_files = [name[:-3] for name in names if name.endswith('.py')] + return the_input.auto_completion(plugins_files, '') + + def completion_unload(self, the_input): + """ + completion function that completes the name of the plugins that are loaded + """ + return the_input.auto_completion(list(self.plugins.keys()), '') diff --git a/src/tabs.py b/src/tabs.py index 611202e4..6f100741 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -390,6 +390,9 @@ class ChatTab(Tab): self.text_win.refresh() self.input.refresh() + def get_conversation_messages(self): + return self._text_buffer.messages + def command_say(self, line): raise NotImplementedError @@ -716,6 +719,10 @@ class MucTab(ChatTab): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'groupchat' msg['body'] = line + # trigger the event BEFORE looking for colors. + # This lets a plugin insert \x19xxx} colors, that will + # be converted in xhtml. + self.core.events.trigger('muc_say', msg) if msg['body'].find('\x19') != -1: msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) @@ -1207,6 +1214,10 @@ class PrivateTab(ChatTab): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' msg['body'] = line + # trigger the event BEFORE looking for colors. + # This lets a plugin insert \x19xxx} colors, that will + # be converted in xhtml. + self.core.events.trigger('private_say', msg) self.core.add_message_to_text_buffer(self._text_buffer, msg['body'], None, self.core.own_nick or self.own_nick) if msg['body'].find('\x19') != -1: msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body']) @@ -1402,6 +1413,7 @@ class RosterInfoTab(Tab): self.commands['remove'] = (self.command_remove, _("Usage: /remove [jid]\nRemove: Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster."), self.completion_remove) self.commands['export'] = (self.command_export, _("Usage: /export [/path/to/file]\nExport: Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not."), None) self.commands['import'] = (self.command_import, _("Usage: /import [/path/to/file]\nImport: Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not."), None) + self.commands['clear_infos'] = (self.command_clear_infos, _("Usage: /clear_infos\nClear Infos: Use this command to clear the info buffer."), None) self.resize() def resize(self): @@ -1422,6 +1434,15 @@ class RosterInfoTab(Tab): not self.input.help_message: self.complete_commands(self.input) + def command_clear_infos(self, arg): + """ + /clear_infos + """ + self.core.information_buffer.messages = [] + self.information_win.rebuild_everything(self.core.information_buffer) + self.core.information_win.rebuild_everything(self.core.information_buffer) + self.refresh() + def command_deny(self, args): """ Denies a JID from our roster @@ -1869,6 +1890,11 @@ class ConversationTab(ChatTab): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' msg['body'] = line + # trigger the event BEFORE looking for colors. + # and before displaying the message in the window + # This lets a plugin insert \x19xxx} colors, that will + # be converted in xhtml. + self.core.events.trigger('conversation_say', msg) self.core.add_message_to_text_buffer(self._text_buffer, msg['body'], None, self.core.own_nick) if msg['body'].find('\x19') != -1: msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body']) @@ -1982,6 +2008,9 @@ class ConversationTab(ChatTab): if config.get('send_chat_states', 'true') == 'true': self.send_chat_state('gone') + def add_message(self, txt, time=None, nickname=None, forced_user=None): + self._text_buffer.add_message(txt, time, nickname, None, None, forced_user) + class MucListTab(Tab): """ A tab listing rooms from a specific server, displaying various information, diff --git a/src/xhtml.py b/src/xhtml.py index 44195f90..99e0bf01 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -255,7 +255,6 @@ def xhtml_to_poezio_colors(text): key, value = rule.split(':', 1) key = key.strip() value = value.strip() - log.debug(value) if key == 'background-color': pass#shell += '\x191' elif key == 'color': @@ -278,7 +277,6 @@ def xhtml_to_poezio_colors(text): def trim(string): return re.sub(whitespace_re, ' ', string) - log.debug(text) xml = ET.fromstring(text) message = '' if version_info[1] == 2: |