summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core.py130
-rwxr-xr-xsrc/daemon.py50
-rw-r--r--src/data_forms.py21
-rw-r--r--src/events.py70
-rw-r--r--src/fifo.py70
-rw-r--r--src/plugin.py92
-rw-r--r--src/plugin_manager.py137
-rw-r--r--src/tabs.py29
-rw-r--r--src/xhtml.py2
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: