From e3b933445fe4b18af6ec462fc40da5f482e447a0 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Fri, 23 Sep 2011 17:43:01 +0200 Subject: [teisenbe] first attempt at a plugin system. --- src/core.py | 27 +++++++++++++++++++++++++++ src/plugin.py | 33 +++++++++++++++++++++++++++++++++ src/plugin_manager.py | 25 +++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/plugin.py create mode 100644 src/plugin_manager.py (limited to 'src') diff --git a/src/core.py b/src/core.py index 3cdc7592..d644c19b 100644 --- a/src/core.py +++ b/src/core.py @@ -36,6 +36,8 @@ 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 @@ -125,6 +127,8 @@ class Core(object): 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None), 'bind': (self.command_bind, _('Usage: /bind \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 than the Up key.'), None), 'pubsub': (self.command_pubsub, _('Usage: /pubsub \nPubsub: Open a pubsub browser on the given domain'), None), + 'load': (self.command_load, _('Usage: /load \nLoad: Load the specified python script'), None), + 'unload': (self.command_unload, _('Usage: /unload \nUnload: Unload the specified python script'), None), } self.key_func = { @@ -169,6 +173,7 @@ class Core(object): self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) self.timed_events = set() + self.plugin_manager = PluginManager(self) def coucou(self): self.command_pubsub('pubsub.louiz.org') @@ -1121,6 +1126,28 @@ 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 + """ + 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 + """ + args = arg.split() + if len(args) != 1: + self.command_help('unload') + return + filename = args[0] + self.plugin_manager.unload(filename) + def command_message(self, arg): """ /message [message] diff --git a/src/plugin.py b/src/plugin.py new file mode 100644 index 00000000..e8386d16 --- /dev/null +++ b/src/plugin.py @@ -0,0 +1,33 @@ +import inspect + +class BasePlugin(object): + """ + 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, core): + self.core = core + for k, v in inspect.getmembers(self, inspect.ismethod): + if k.startswith('on_'): + core.xmpp.add_event_handler(k[3:], v) + elif k.startswith('command_'): + command = k[len('command_'):] + core.commands[command] = (v, v.__doc__, None) + self.init() + + def init(self): + pass + + def cleanup(self): + pass + + def unload(self): + for k, v in inspect.getmembers(self, inspect.ismethod): + if k.startswith('on_'): + self.core.xmpp.del_event_handler(k[3:], v) + elif k.startswith('command_'): + command = k[len('command_'):] + del self.core.commands[command] + self.cleanup() diff --git a/src/plugin_manager.py b/src/plugin_manager.py new file mode 100644 index 00000000..3f900e39 --- /dev/null +++ b/src/plugin_manager.py @@ -0,0 +1,25 @@ +class PluginManager(object): + def __init__(self, core): + self.core = core + self.plugins = {} + + def load(self, name): + if name in self.plugins: + self.plugins[name].unload() + + try: + code = compile(open(name).read(), name, 'exec') + from plugin import BasePlugin + globals = { 'BasePlugin' : BasePlugin } + exec(code, globals) + self.plugins[name] = globals['Plugin'](self.core) + except Exception as e: + self.core.information("Could not load plugin: %s" % (e,)) + + def unload(self, name): + if name in self.plugins: + try: + self.plugins[name].unload() + del self.plugins[name] + except Exception as e: + self.core.information("Could not unload plugin (may not be safe to try again): %s" % (e,)) -- cgit v1.2.3 From f27556747896aeb891ce71cfdd0ac349d68c5b3d Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 24 Sep 2011 22:26:31 +0200 Subject: [teisenbe] Use the imp module to import modules. Also add a simple translator module --- src/plugin.py | 26 ++++++++--------- src/plugin_manager.py | 77 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index e8386d16..728dfe21 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,5 +1,3 @@ -import inspect - class BasePlugin(object): """ Class that all plugins derive from. Any methods beginning with command_ @@ -7,14 +5,9 @@ class BasePlugin(object): event handlers """ - def __init__(self, core): + def __init__(self, plugin_manager, core): self.core = core - for k, v in inspect.getmembers(self, inspect.ismethod): - if k.startswith('on_'): - core.xmpp.add_event_handler(k[3:], v) - elif k.startswith('command_'): - command = k[len('command_'):] - core.commands[command] = (v, v.__doc__, None) + self.plugin_manager = plugin_manager self.init() def init(self): @@ -24,10 +17,13 @@ class BasePlugin(object): pass def unload(self): - for k, v in inspect.getmembers(self, inspect.ismethod): - if k.startswith('on_'): - self.core.xmpp.del_event_handler(k[3:], v) - elif k.startswith('command_'): - command = k[len('command_'):] - del self.core.commands[command] self.cleanup() + + def add_command(self, name, handler, help, completion=None): + return self.plugin_manager.add_command(self.__module__, name, handler, help, completion) + + 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) diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 3f900e39..8301f5f8 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -1,25 +1,86 @@ +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') +try: + os.makedirs(plugins_dir) +except OSError: + pass + +sys.path.append(plugins_dir) + class PluginManager(object): def __init__(self, core): self.core = core - self.plugins = {} + 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 def load(self, name): if name in self.plugins: self.plugins[name].unload() try: - code = compile(open(name).read(), name, 'exec') - from plugin import BasePlugin - globals = { 'BasePlugin' : BasePlugin } - exec(code, globals) - self.plugins[name] = globals['Plugin'](self.core) + 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: - self.core.information("Could not load plugin: %s" % (e,)) + import traceback + self.core.information(_("Could not load plugin: ") + traceback.format_exc()) + return + finally: + if imp.lock_held(): + imp.release_lock() + + self.modules[name] = module + self.commands[name] = {} + self.event_handlers[name] = [] + self.plugins[name] = module.Plugin(self, self.core) 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) + self.plugins[name].unload() del self.plugins[name] + del self.commands[name] + del self.event_handlers[name] except Exception as e: - self.core.information("Could not unload plugin (may not be safe to try again): %s" % (e,)) + import traceback + self.core.information(_("Could not unload plugin (may not be safe to try again): ") + traceback.format_exc()) + + 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)) -- cgit v1.2.3 From eb096892a9ab3429ba0dcb9654a356afcd01932d Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 24 Sep 2011 23:10:55 +0200 Subject: Completion for load and unload commands --- src/core.py | 6 +++--- src/plugin_manager.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index d644c19b..55026967 100644 --- a/src/core.py +++ b/src/core.py @@ -103,6 +103,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), @@ -127,8 +128,8 @@ class Core(object): 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None), 'bind': (self.command_bind, _('Usage: /bind \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 than the Up key.'), None), 'pubsub': (self.command_pubsub, _('Usage: /pubsub \nPubsub: Open a pubsub browser on the given domain'), None), - 'load': (self.command_load, _('Usage: /load \nLoad: Load the specified python script'), None), - 'unload': (self.command_unload, _('Usage: /unload \nUnload: Unload the specified python script'), None), + 'load': (self.command_load, _('Usage: /load \nLoad: Load the specified python script'), self.plugin_manager.completion_load), + 'unload': (self.command_unload, _('Usage: /unload \nUnload: Unload the specified python script'), self.plugin_manager.completion_unload), } self.key_func = { @@ -173,7 +174,6 @@ class Core(object): self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) self.timed_events = set() - self.plugin_manager = PluginManager(self) def coucou(self): self.command_pubsub('pubsub.louiz.org') diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 8301f5f8..2a7a116f 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -40,7 +40,7 @@ class PluginManager(object): imp.release_lock() except Exception as e: import traceback - self.core.information(_("Could not load plugin: ") + traceback.format_exc()) + self.core.information(_("Could not load plugin: ") + traceback.format_exc(), 'Error') return finally: if imp.lock_held(): @@ -84,3 +84,22 @@ class PluginManager(object): self.core.xmpp.del_event_handler(event_name, handler) eh = self.event_handlers[module_name] eh = list(filter(lambda e : e != (event_name, handler), eh)) + + def completion_load(self, the_input): + """ + completion function that completes the name of the plugins, from + all .py files in plugins_dir + """ + try: + names = 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()), '') -- cgit v1.2.3 From cac130e7543b30be7fbec6484b29191a9f8b1665 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 24 Sep 2011 23:44:52 +0200 Subject: Autoload plugins --- src/core.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/core.py b/src/core.py index 55026967..0b8442ff 100644 --- a/src/core.py +++ b/src/core.py @@ -174,6 +174,12 @@ class Core(object): self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) self.timed_events = set() + self.autoload_plugins() + + def autoload_plugins(self): + plugins = config.get('plugins_autoload', '') + for plugin in plugins.split(): + self.plugin_manager.load(plugin) def coucou(self): self.command_pubsub('pubsub.louiz.org') -- cgit v1.2.3 From 1a6d903e34d505005836f6b8aee3552073a2397e Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 25 Sep 2011 02:39:00 +0200 Subject: Add a config file to the plugins by default --- src/plugin.py | 28 +++++++++++++++++++++++++++- src/plugin_manager.py | 12 +++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index 728dfe21..d64679a1 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,3 +1,26 @@ +import os +from configparser import ConfigParser + +class PluginConfig(ConfigParser): + 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 BasePlugin(object): """ Class that all plugins derive from. Any methods beginning with command_ @@ -5,9 +28,11 @@ class BasePlugin(object): event handlers """ - def __init__(self, plugin_manager, core): + def __init__(self, plugin_manager, core, plugins_conf_dir): self.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): @@ -17,6 +42,7 @@ class BasePlugin(object): pass def unload(self): + self.cleanup() def add_command(self, name, handler, help, completion=None): diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 2a7a116f..0ffee7ee 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -9,11 +9,21 @@ 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') + +plugins_conf_dir = os.path.join(os.environ.get('XDG_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): @@ -49,7 +59,7 @@ class PluginManager(object): self.modules[name] = module self.commands[name] = {} self.event_handlers[name] = [] - self.plugins[name] = module.Plugin(self, self.core) + self.plugins[name] = module.Plugin(self, self.core, plugins_conf_dir) def unload(self, name): if name in self.plugins: -- cgit v1.2.3 From b63132d32d35fc4593d0a25fc95274891492a542 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 25 Sep 2011 02:43:52 +0200 Subject: remove yet another unneeded call for GlobalInfoBar --- src/data_forms.py | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/data_forms.py b/src/data_forms.py index 176c4669..f54e7493 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -32,7 +32,6 @@ class DataFormsTab(Tab): for field in self._form: self.fields.append(field) self.topic_win = windows.Topic() - self.tab_win = windows.GlobalInfoBar() self.form_win = FormWin(form, self.height-4, self.width, 1, 0) self.help_win = windows.HelpText("Ctrl+Y: send form, Ctrl+G: cancel") self.help_win_dyn = windows.HelpText() -- cgit v1.2.3 From fd99fb32bb6d696af9d792d2d6ea69bfe610501f Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 25 Sep 2011 02:50:03 +0200 Subject: [teisenbe] Make the data forms more usable (add color to the labels) --- src/data_forms.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data_forms.py b/src/data_forms.py index f54e7493..9510bdf8 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -130,6 +130,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) @@ -398,7 +418,7 @@ class FormWin(object): if field['type'] == 'fixed': label = field.getValue() inp = input_class(field) - self.inputs.append({'label':label, + self.inputs.append({'label':ColoredLabel(label), 'description': desc, 'input':inp}) @@ -427,6 +447,7 @@ class FormWin(object): return if self.current_input == len(self.inputs) - 1 or self.current_input >= self.height-1: return + self.inputs[self.current_input]['label'].set_color(14) self.inputs[self.current_input]['input'].set_color(14) self.current_input += 1 jump = 0 @@ -435,6 +456,7 @@ class FormWin(object): if self.inputs[self.current_input+jump]['input'].is_dummy(): return self.current_input += jump + self.inputs[self.current_input]['label'].set_color(13) self.inputs[self.current_input]['input'].set_color(13) def go_to_previous_input(self): @@ -442,6 +464,7 @@ class FormWin(object): return if self.current_input == 0: return + self.inputs[self.current_input]['label'].set_color(14) self.inputs[self.current_input]['input'].set_color(14) self.current_input -= 1 jump = 0 @@ -450,6 +473,7 @@ class FormWin(object): if self.inputs[self.current_input+jump]['input'].is_dummy(): return self.current_input -= jump + self.inputs[self.current_input]['label'].set_color(13) self.inputs[self.current_input]['input'].set_color(13) def on_input(self, key): @@ -465,8 +489,7 @@ class FormWin(object): for name, field in self._form.getFields(): if field['type'] == 'hidden': continue - label = self.inputs[i]['label'] - self._win.addstr(y, 0, label) + self.inputs[i]['label'].resize(1, self.width//3, y + 1, 0) self.inputs[i]['input'].resize(1, self.width//3, y+1, 2*self.width//3) # TODO: display the field description y += 1 @@ -477,10 +500,13 @@ class FormWin(object): for i, inp in enumerate(self.inputs): if i >= self.height: break + inp['label'].refresh() inp['input'].refresh() if self.current_input < self.height-1: self.inputs[self.current_input]['input'].set_color(13) self.inputs[self.current_input]['input'].refresh() + self.inputs[self.current_input]['label'].set_color(13) + self.inputs[self.current_input]['label'].refresh() def refresh_current_input(self): self.inputs[self.current_input]['input'].refresh() -- cgit v1.2.3 From 00ed9b4842169111238b86d0bfc1465176b7d2d8 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 25 Sep 2011 03:01:32 +0200 Subject: [teisenbe] Fix a bug in case of XDG_CONFIG_HOME not set --- src/plugin_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 0ffee7ee..1f0e89eb 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -10,8 +10,10 @@ plugins_dir = plugins_dir or\ os.path.join(os.environ.get('HOME'), '.local', 'share'), 'poezio', 'plugins') -plugins_conf_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME'), '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) @@ -23,7 +25,6 @@ try: except OSError: pass - sys.path.append(plugins_dir) class PluginManager(object): -- cgit v1.2.3 From 47c052acf47848a35987c6c41d9c20d390894f3e Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sun, 25 Sep 2011 21:15:00 +0200 Subject: Add a send_message() function for plugins --- src/core.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src') diff --git a/src/core.py b/src/core.py index 88c926a9..1f60e252 100644 --- a/src/core.py +++ b/src/core.py @@ -1637,3 +1637,13 @@ 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 -- cgit v1.2.3 From 55d624c0ee0d96ee5706ccbeec91cdc6ffc0b5a7 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sun, 25 Sep 2011 21:16:31 +0200 Subject: exec plugin --- src/plugin.py | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index d64679a1..d332ca01 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -42,7 +42,6 @@ class BasePlugin(object): pass def unload(self): - self.cleanup() def add_command(self, name, handler, help, completion=None): -- cgit v1.2.3 From 7b8a860de95f2af6855b1ba270e677a0eb4c044c Mon Sep 17 00:00:00 2001 From: Todd Eisenberger Date: Tue, 27 Sep 2011 10:14:18 -0700 Subject: Fix loading already loaded plugins --- src/core.py | 8 ++++---- src/plugin_manager.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index 1f60e252..1a4d1f2d 100644 --- a/src/core.py +++ b/src/core.py @@ -129,8 +129,8 @@ class Core(object): 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None), 'bind': (self.command_bind, _('Usage: /bind \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 than the Up key.'), None), 'pubsub': (self.command_pubsub, _('Usage: /pubsub \nPubsub: Open a pubsub browser on the given domain'), None), - 'load': (self.command_load, _('Usage: /load \nLoad: Load the specified python script'), self.plugin_manager.completion_load), - 'unload': (self.command_unload, _('Usage: /unload \nUnload: Unload the specified python script'), self.plugin_manager.completion_unload), + 'load': (self.command_load, _('Usage: /load \nLoad: Load the specified plugin'), self.plugin_manager.completion_load), + 'unload': (self.command_unload, _('Usage: /unload \nUnload: Unload the specified plugin'), self.plugin_manager.completion_unload), } self.key_func = { @@ -1137,7 +1137,7 @@ class Core(object): def command_load(self, arg): """ - /load + /load """ args = arg.split() if len(args) != 1: @@ -1148,7 +1148,7 @@ class Core(object): def command_unload(self, arg): """ - /unload + /unload """ args = arg.split() if len(args) != 1: diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 1f0e89eb..df96e9ab 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -37,7 +37,7 @@ class PluginManager(object): def load(self, name): if name in self.plugins: - self.plugins[name].unload() + self.unload(name) try: if name in self.modules: -- cgit v1.2.3 From e9e15058bd22ecc225bde40380b0fcc2b422d37f Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Thu, 29 Sep 2011 02:07:45 +0200 Subject: Mais putain --- src/tabs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tabs.py b/src/tabs.py index 63fd2c96..bc656cdd 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -884,7 +884,7 @@ class MucTab(ChatTab): if by: kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'by':by} else: - kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195 has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} if reason is not None and reason.text: kick_msg += _('\x195} Reason: \x196}%(reason)s\x195}') % {'reason': reason.text} room.add_message(kick_msg) -- cgit v1.2.3 From ed87f26db763432505072eb5a2875f30fc4061d1 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 1 Oct 2011 23:48:42 +0200 Subject: Added a connect() function to the plugins API, for internal event --- src/core.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin_manager.py | 3 +++ src/tabs.py | 1 + 3 files changed, 65 insertions(+) (limited to 'src') diff --git a/src/core.py b/src/core.py index 0e09cf04..41a54b23 100644 --- a/src/core.py +++ b/src/core.py @@ -16,6 +16,7 @@ import threading import traceback from datetime import datetime +from inspect import getargspec import common import theming @@ -78,6 +79,13 @@ class Core(object): """ User interface using ncurses """ + + # dict containing the name of the internal events + # used with the plugins, the key is the name of the event + # and the value is the number of arguments the handler must take + internal_events = { + 'enter': 2, + } def __init__(self): # All uncaught exception are given to this callback, instead # of being displayed on the screen and exiting the program. @@ -174,6 +182,8 @@ class Core(object): self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) self.timed_events = set() + + self.connected_events = {} self.autoload_plugins() def autoload_plugins(self): @@ -210,6 +220,57 @@ class Core(object): )) self.refresh_window() + def connect(self, event, handler): + """ + Connect an handler to an internal event of poezio + (eg "enter pressed in a chattab") + """ + # Fail if the method doesn’t take at least the good number of arguments + # or if the event is unknown + if not event in self.internal_events \ + or len(getargspec(handler).args) < self.internal_events[event]: + return False + + module_name = handler.__module__ + if not event in self.connected_events: + self.connected_events[event] = {} + if not module_name in self.connected_events[event]: + self.connected_events[event][module_name] = [] + + self.connected_events[event][module_name].append(handler) + return True + + def run_event(self, event, **kwargs): + """ + Call the handlers associated with an event + """ + if event in self.connected_events: + for module in self.connected_events[event]: + for handler in self.connected_events[event][module]: + try: + handler(**kwargs) + except: + import traceback + tp = traceback.format_exc() + module_name = handler.__name__ + log.debug('ERROR: in plugin %s, \n%s' % (module_name, tp)) + + def disconnect(self, event, handler): + """ + Disconnect a handler from an event + """ + if not event in self.internal_events: + return False + + module_name = getmodule(handler).__name__ + if not event in self.connected_events: + return False + if not module_name in self.connected_events[event]: + return False + + self.connected_events[event][module_name].remove(handler) + return True + def resize_global_information_win(self): """ Resize the global_information_win only once at each resize. diff --git a/src/plugin_manager.py b/src/plugin_manager.py index df96e9ab..82be8632 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -69,6 +69,9 @@ class PluginManager(object): del self.core.commands[command] for event_name, handler in self.event_handlers[name]: self.core.xmpp.del_event_handler(event_name, handler) + for event_name in self.core.internal_events: + if name in event_name: + del event_name[name] self.plugins[name].unload() del self.plugins[name] diff --git a/src/tabs.py b/src/tabs.py index bc656cdd..a349da0b 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -298,6 +298,7 @@ class ChatTab(Tab): if not self.execute_command(clean_text): if txt.startswith('//'): txt = txt[1:] + self.core.run_event('enter', line=txt) self.command_say(txt) self.cancel_paused_delay() -- cgit v1.2.3 From b7279678df346488c8a0454b7d5d372d2236ce65 Mon Sep 17 00:00:00 2001 From: Todd Eisenberger Date: Sun, 2 Oct 2011 00:09:50 -0700 Subject: Frumious hacks to make plugins less likely to kill a client --- src/plugin.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index d332ca01..a5aae1f6 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,5 +1,7 @@ import os from configparser import ConfigParser +import inspect +import traceback class PluginConfig(ConfigParser): def __init__(self, filename): @@ -21,7 +23,30 @@ class PluginConfig(ConfigParser): except IOError: return False -class BasePlugin(object): +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 @@ -30,6 +55,8 @@ class BasePlugin(object): 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) -- cgit v1.2.3 From d5898965993b63ee719df48cca5d48b0a0402f85 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 2 Oct 2011 13:21:51 +0200 Subject: Adds a way to delete the commands without reloading the plugin --- src/plugin.py | 3 +++ src/plugin_manager.py | 6 ++++++ 2 files changed, 9 insertions(+) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index a5aae1f6..8d873419 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -74,6 +74,9 @@ class BasePlugin(object, metaclass=SafetyMetaclass): 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) diff --git a/src/plugin_manager.py b/src/plugin_manager.py index 82be8632..5bd6d75b 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -81,6 +81,12 @@ class PluginManager(object): 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,)) -- cgit v1.2.3 From 7e16ffd9e0558b4895684c61f49059f7e603dc06 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 05:11:30 +0200 Subject: Remote execution. We can use a fifo to write command, and execute them on the local machine by running a simple daemon. --- src/core.py | 30 ++++++++++++++++++++++++++ src/fifo.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/fifo.py (limited to 'src') diff --git a/src/core.py b/src/core.py index 41a54b23..2b305f21 100644 --- a/src/core.py +++ b/src/core.py @@ -49,6 +49,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 # http://xmpp.org/extensions/xep-0045.html#errorstatus ERROR_AND_STATUS_CODES = { @@ -94,6 +95,7 @@ class Core(object): sys.excepthook = self.on_exception self.running = True 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. @@ -1707,3 +1709,31 @@ class Core(object): 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. + """ + 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, e,), 'Error') + self.remote_fifo = None + else: + pass 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 +# +# 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 -- cgit v1.2.3 From 1303919706c74352be28c55e0f4516b994ebdb5b Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 05:13:12 +0200 Subject: Add the daemon. --- src/daemon.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 src/daemon.py (limited to 'src') diff --git a/src/daemon.py b/src/daemon.py new file mode 100755 index 00000000..a9d888f1 --- /dev/null +++ b/src/daemon.py @@ -0,0 +1,63 @@ +# Copyright 2011 Florent Le Coz +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the zlib license. See the COPYING file. + +""" +This file is a standalone program that creates a fifo file (if it doesn’t exist +yet), opens it for reading, reads commands from it and executes them (each line +should be a command). + +Usage: ./daemon.py + +That fifo should be in a directory, shared through sshfs, with the remote +machine running poezio. Poezio then writes command in it, 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 + +from fifo import Fifo + +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,)) + subprocess.call(self.command.split()) + +def main(path): + while True: + fifo = Fifo(path, 'r') + while True: + line = fifo.readline() + if line == '': + del fifo + break + e = Executor(line) + e.start() + +def usage(): + print('Usage: %s ' % (sys.argv[0],)) + +if __name__ == '__main__': + argc = len(sys.argv) + if argc != 2: + usage() + else: + main(sys.argv[1]) -- cgit v1.2.3 From f8fcf6696d0044817d596e7d09dcbdcc8a52aac6 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 05:14:05 +0200 Subject: =?UTF-8?q?Remove=20any=20mention=20of=20=E2=80=9Ccoucou=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index 2b305f21..712eed62 100644 --- a/src/core.py +++ b/src/core.py @@ -159,7 +159,6 @@ class Core(object): 'M-z': self.go_to_previous_tab, '^L': self.full_screen_redraw, 'M-j': self.go_to_room_number, -# 'M-c': self.coucou, } # Add handlers @@ -193,9 +192,6 @@ class Core(object): for plugin in plugins.split(): self.plugin_manager.load(plugin) - def coucou(self): - self.command_pubsub('pubsub.louiz.org') - def start(self): """ Init curses, create the first tab, etc -- cgit v1.2.3 From cb2796abc5e3732dc199414bb1cbe166e0d57ae3 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 07:03:49 +0200 Subject: Add a \n when writing the command to the fifo --- src/core.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/core.py b/src/core.py index 712eed62..5f734142 100644 --- a/src/core.py +++ b/src/core.py @@ -1718,6 +1718,7 @@ class Core(object): 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: -- cgit v1.2.3 From a090b235d70c826ad823898800917c04e84a5535 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 07:04:32 +0200 Subject: Make the PluginConfig class heritate the poezio Config We can use the nice and safe get() method --- src/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/plugin.py b/src/plugin.py index 8d873419..a8eb5934 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,9 +1,10 @@ import os from configparser import ConfigParser +import config import inspect import traceback -class PluginConfig(ConfigParser): +class PluginConfig(config.Config): def __init__(self, filename): ConfigParser.__init__(self) self.__config_file__ = filename @@ -23,6 +24,7 @@ class PluginConfig(ConfigParser): except IOError: return False + class SafetyMetaclass(type): # A hack core = None -- cgit v1.2.3 From 881de6a7c02e4c11957a1bb0dd3b31b8884fa8b0 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 07:05:19 +0200 Subject: add get_conversation_messages for the plugin API --- src/core.py | 12 ++++++++++++ src/tabs.py | 3 +++ 2 files changed, 15 insertions(+) (limited to 'src') diff --git a/src/core.py b/src/core.py index 5f734142..b39bf904 100644 --- a/src/core.py +++ b/src/core.py @@ -1734,3 +1734,15 @@ class Core(object): 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/tabs.py b/src/tabs.py index a349da0b..8606c450 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -368,6 +368,9 @@ class ChatTab(Tab): self.text_win.refresh(self._room) self.input.refresh() + def get_conversation_messages(self): + return self._room.messages + def command_say(self, line): raise NotImplementedError -- cgit v1.2.3 From db1e84d34e910812d494673a1d78d85f38e2b725 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 16:58:36 +0200 Subject: Daemon now reads from a pipe and not in the fifo directly --- src/daemon.py | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/daemon.py b/src/daemon.py index a9d888f1..f23d6b5f 100755 --- a/src/daemon.py +++ b/src/daemon.py @@ -1,3 +1,4 @@ +#/usr/bin/env python3 # Copyright 2011 Florent Le Coz # # This file is part of Poezio. @@ -6,15 +7,13 @@ # it under the terms of the zlib license. See the COPYING file. """ -This file is a standalone program that creates a fifo file (if it doesn’t exist -yet), opens it for reading, reads commands from it and executes them (each line -should be a command). +This file is a standalone program that reads commands on +stdin and executes them (each line should be a command). -Usage: ./daemon.py +Usage: cat some_fifo | ./daemon.py -That fifo should be in a directory, shared through sshfs, with the remote -machine running poezio. Poezio then writes command in it, and this daemon -executes them on the local machine. +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. @@ -24,8 +23,6 @@ import sys import threading import subprocess -from fifo import Fifo - class Executor(threading.Thread): """ Just a class to execute commands in a thread. @@ -38,26 +35,16 @@ class Executor(threading.Thread): self.command = command def run(self): - print('executing %s' % (self.command,)) + print('executing %s' % (self.command.strip(),)) subprocess.call(self.command.split()) -def main(path): +def main(): while True: - fifo = Fifo(path, 'r') - while True: - line = fifo.readline() - if line == '': - del fifo - break - e = Executor(line) - e.start() - -def usage(): - print('Usage: %s ' % (sys.argv[0],)) + line = sys.stdin.readline() + if line == '': + break + e = Executor(line) + e.start() if __name__ == '__main__': - argc = len(sys.argv) - if argc != 2: - usage() - else: - main(sys.argv[1]) + main() -- cgit v1.2.3 From 21f0c8f3f348ca515b81208c73704d5fd7b10328 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 29 Oct 2011 17:20:18 +0200 Subject: Fix the url matching in the link plugin --- src/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index 1fb06b38..88a726ea 100644 --- a/src/core.py +++ b/src/core.py @@ -1730,7 +1730,7 @@ class Core(object): try: self.remote_fifo.write(command) except (IOError) as e: - self.information('Could not execute [%s]: %s' % (command, e,), 'Error') + self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error') self.remote_fifo = None else: pass -- cgit v1.2.3 From 9d64bd13983eba36b357d1681f38a1c0c41827e7 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 29 Oct 2011 17:46:45 +0200 Subject: Remove aliases to /status --- src/core.py | 22 ---------------------- 1 file changed, 22 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index 88a726ea..e1bae799 100644 --- a/src/core.py +++ b/src/core.py @@ -124,11 +124,7 @@ class Core(object): 'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room."), None), 'win': (self.command_win, _("Usage: /win \nWin: Go to the specified room."), self.completion_win), 'w': (self.command_win, _("Usage: /w \nW: Go to the specified room."), self.completion_win), - 'show': (self.command_status, _('Usage: /show [status message]\nShow: Sets your availability and (optionaly) your status message. The argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message.'), self.completion_status), 'status': (self.command_status, _('Usage: /status [status message]\nStatus: Sets your availability and (optionaly) your status message. The argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message.'), self.completion_status), - 'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optionaly) 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 (optionaly) 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 (optionaly) 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 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