changeset 926:d609581bf74a

plugin text commands: refactoring, text now only contain main commands, and other plugin can add commands themselve: - registerTextCommands can be called by a plugin in its __init__ method, it looks for methods starting with "cmd_" and register them as new commands - addWhoIsCb: add a callback to /whois command, the callback can complete whois informations - plugins parrot, XEP-0045 and XEP-0092 now manage their own commands
author Goffi <goffi@goffi.org>
date Mon, 24 Mar 2014 10:57:15 +0100
parents 5c78cefd233f
children cd150dd947e3
files src/core/constants.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_xep_0045.py src/plugins/plugin_xep_0092.py
diffstat 5 files changed, 217 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/src/core/constants.py	Sun Mar 23 10:02:50 2014 +0100
+++ b/src/core/constants.py	Mon Mar 24 10:57:15 2014 +0100
@@ -36,3 +36,6 @@
     PROF_KEY_NONE = '@NONE@'
     PROF_KEY_DEFAULT = '@DEFAULT@'
     IQ_SET = '/iq[@type="set"]'
+
+    # names of widely used plugins
+    TEXT_CMDS = 'TEXT-COMMANDS'
--- a/src/plugins/plugin_exp_parrot.py	Sun Mar 23 10:02:50 2014 +0100
+++ b/src/plugins/plugin_exp_parrot.py	Mon Mar 24 10:57:15 2014 +0100
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core.constants import Const as C
 from logging import debug, info, warning, error
 from twisted.words.protocols.jabber import jid
 
@@ -30,6 +31,7 @@
     "type": "EXP",
     "protocols": [],
     "dependencies": ["XEP-0045"],
+    "recommendations": [C.TEXT_CMDS],
     "main": "Exp_Parrot",
     "handler": "no",
     "description": _("""Implementation of parrot mode (repeat messages between 2 entities)""")
@@ -48,6 +50,10 @@
         self.host = host
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100)
         #host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100)
+        try:
+            self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
+        except KeyError:
+            info(_("Text commands not available"))
 
     #def sendMessageTrigger(self, mess_data, treatments, profile):
     #    """ Deactivate other triggers if recipient is in parrot links """
@@ -122,3 +128,50 @@
             del client.parrot_links[source_jid.userhostJID()]
         except (AttributeError, KeyError):
             pass
+
+    def cmd_parrot(self, mess_data, profile):
+        """activate Parrot mode between 2 entities, in both directions."""
+        #TODO: these commands must not be hardcoded, an interface should be made
+        #      to allow plugins to register simple commands like this.
+
+        debug("Catched parrot command")
+        txt_cmd = self.host.plugins[C.TEXT_CMDS]
+
+        try:
+            link_left_jid = jid.JID(mess_data["unparsed"].strip())
+            if not link_left_jid.user or not link_left_jid.host:
+                raise jid.InvalidFormat
+        except jid.InvalidFormat:
+            txt_cmd.feedBack("Can't activate Parrot mode for invalid jid", mess_data, profile)
+            return False
+
+        link_right_jid = mess_data['to']
+
+        self.addParrot(link_left_jid, link_right_jid, profile)
+        self.addParrot(link_right_jid, link_left_jid, profile)
+
+        txt_cmd.feedBack("Parrot mode activated for %s" % (unicode(link_left_jid), ), mess_data, profile)
+
+        return False
+
+    def cmd_unparrot(self, mess_data, profile):
+        """remove Parrot mode between 2 entities, in both directions."""
+        debug("Catched unparrot command")
+        txt_cmd = self.host.plugins[C.TEXT_CMDS]
+
+        try:
+            link_left_jid = jid.JID(mess_data["unparsed"].strip())
+            if not link_left_jid.user or not link_left_jid.host:
+                raise jid.InvalidFormat
+        except jid.InvalidFormat:
+            txt_cmd.feedBack("Can't deactivate Parrot mode for invalid jid", mess_data, profile)
+            return False
+
+        link_right_jid = mess_data['to']
+
+        self.removeParrot(link_left_jid, profile)
+        self.removeParrot(link_right_jid, profile)
+
+        txt_cmd.feedBack("Parrot mode deactivated for %s and %s" % (unicode(link_left_jid), unicode(link_right_jid)), mess_data, profile)
+
+        return False
--- a/src/plugins/plugin_misc_text_commands.py	Sun Mar 23 10:02:50 2014 +0100
+++ b/src/plugins/plugin_misc_text_commands.py	Mon Mar 24 10:57:15 2014 +0100
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core.constants import Const as C
 from sat.core.sat_main import MessageSentAndStored
 from twisted.words.protocols.jabber import jid
 from twisted.internet import defer
@@ -26,10 +27,10 @@
 
 PLUGIN_INFO = {
     "name": "Text commands",
-    "import_name": "TEXT-COMMANDS",
+    "import_name": C.TEXT_CMDS,
     "type": "Misc",
     "protocols": [],
-    "dependencies": ["XEP-0045", "EXP-PARROT", "XEP-0092"],
+    "dependencies": [],
     "main": "TextCommands",
     "handler": "no",
     "description": _("""IRC like text commands""")
@@ -45,6 +46,45 @@
         info(_("Text commands initialization"))
         self.host = host
         host.trigger.add("sendMessage", self.sendMessageTrigger)
+        self._commands = {}
+        self._whois = []
+        self.registerTextCommands(self)
+
+    def registerTextCommands(self, instance):
+        """ Add a text command
+        @param instance: instance of a class containing text commands
+
+        """
+        for attr in dir(instance):
+            if attr.startswith('cmd_'):
+                cmd = getattr(instance, attr)
+                if not callable(cmd):
+                    warning(_("Skipping not callable [%s] attribute") % attr)
+                    continue
+                cmd_name = attr[4:]
+                if not cmd_name:
+                    warning(_("Skipping cmd_ method"))
+                if cmd_name in self._commands:
+                    suff=2
+                    while (cmd_name + suff) in self._commands:
+                        suff+=1
+                    new_name = cmd_name + suff
+                    warning(_("Conflict for command [%(old_name)s], renaming it to [%(new_name)s]") % {'old_name': cmd_name, 'new_name': new_name})
+                    cmd_name = new_name
+                self._commands[cmd_name] = cmd
+                info(_("Registered text command [%s]") % cmd_name)
+
+    def addWhoIsCb(self, callback, priority=0):
+        """Add a callback which give information to the /whois command
+        @param callback: a callback which will be called with the following arguments
+            - whois_msg: list of information strings to display, callback need to append its own strings to it
+            - target_jid: full jid from who we want informations
+            - profile: %(doc_profile)s
+        @param priority: priority of the information to show (the highest priority will be displayed first)
+
+        """
+        self._whois.append((priority, callback))
+        self._whois.sort(key=lambda item: item[0])
 
     def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
         """ Install SendMessage command hook """
@@ -89,14 +129,14 @@
 
             try:
                 mess_data["unparsed"] = msg[1 + len(command):]  # part not yet parsed of the message
-                d = defer.maybeDeferred(getattr(self, "cmd_%s" % command), mess_data, profile)
+                d = defer.maybeDeferred(self._commands[command], mess_data, profile)
                 d.addCallback(retHandling)
-            except AttributeError:
+            except KeyError:
                 pass
 
         return d or mess_data # if a command is detected, we should have a deferred, else be send the message normally
 
-    def _getRoomJID(self, arg, service_jid):
+    def getRoomJID(self, arg, service_jid):
         """Return a room jid with a shortcut
         @param arg: argument: can be a full room jid (e.g.: sat@chat.jabberfr.org)
                     or a shortcut (e.g.: sat or sat@ for sat on current service)
@@ -109,7 +149,7 @@
             return jid.JID(arg + service_jid)
         return jid.JID(u"%s@%s" % (arg, service_jid))
 
-    def _feedBack(self, message, mess_data, profile):
+    def feedBack(self, message, mess_data, profile):
         """Give a message back to the user"""
         if mess_data["type"] == 'groupchat':
             _from = mess_data["to"].userhostJID()
@@ -118,128 +158,6 @@
 
         self.host.bridge.newMessage(unicode(mess_data["to"]), message, mess_data['type'], unicode(_from), {}, profile=profile)
 
-    def cmd_nick(self, mess_data, profile):
-        """change nickname"""
-        debug("Catched nick command")
-
-        if mess_data['type'] != "groupchat":
-            #/nick command does nothing if we are not on a group chat
-            info("Ignoring /nick command on a non groupchat message")
-
-            return True
-
-        nick = mess_data["unparsed"].strip()
-        room = mess_data["to"]
-
-        self.host.plugins["XEP-0045"].nick(room, nick, profile)
-
-        return False
-
-    def cmd_join(self, mess_data, profile):
-        """join a new room (on the same service if full jid is not specified)"""
-        debug("Catched join command")
-
-        if mess_data['type'] != "groupchat":
-            #/leave command does nothing if we are not on a group chat
-            info("Ignoring /join command on a non groupchat message")
-            return True
-
-        if mess_data["unparsed"].strip():
-            room = self._getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
-            nick = (self.host.plugins["XEP-0045"].getRoomNick(mess_data["to"].userhost(), profile) or
-                    self.host.getClient(profile).jid.user)
-            self.host.plugins["XEP-0045"].join(room, nick, {}, profile)
-
-        return False
-
-    def cmd_leave(self, mess_data, profile):
-        """quit a room"""
-        debug("Catched leave command")
-
-        if mess_data['type'] != "groupchat":
-            #/leave command does nothing if we are not on a group chat
-            info("Ignoring /leave command on a non groupchat message")
-            return True
-
-        if mess_data["unparsed"].strip():
-            room = self._getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
-        else:
-            room = mess_data["to"]
-
-        self.host.plugins["XEP-0045"].leave(room, profile)
-
-        return False
-
-    def cmd_part(self, mess_data, profile):
-        """just a synonym of /leave"""
-        return self.cmd_leave(mess_data, profile)
-
-    def cmd_title(self, mess_data, profile):
-        """change room's subject"""
-        debug("Catched title command")
-
-        if mess_data['type'] != "groupchat":
-            #/leave command does nothing if we are not on a group chat
-            info("Ignoring /title command on a non groupchat message")
-            return True
-
-        subject = mess_data["unparsed"].strip()
-
-        if subject:
-            room = mess_data["to"]
-            self.host.plugins["XEP-0045"].subject(room, subject, profile)
-
-        return False
-
-    def cmd_topic(self, mess_data, profile):
-        """just a synonym of /title"""
-        return self.cmd_title(mess_data, profile)
-
-    def cmd_parrot(self, mess_data, profile):
-        """activate Parrot mode between 2 entities, in both directions."""
-        #TODO: these commands must not be hardcoded, an interface should be made
-        #      to allow plugins to register simple commands like this.
-
-        debug("Catched parrot command")
-
-        try:
-            link_left_jid = jid.JID(mess_data["unparsed"].strip())
-            if not link_left_jid.user or not link_left_jid.host:
-                raise jid.InvalidFormat
-        except jid.InvalidFormat:
-            self._feedBack("Can't activate Parrot mode for invalid jid", mess_data, profile)
-            return False
-
-        link_right_jid = mess_data['to']
-
-        self.host.plugins["EXP-PARROT"].addParrot(link_left_jid, link_right_jid, profile)
-        self.host.plugins["EXP-PARROT"].addParrot(link_right_jid, link_left_jid, profile)
-
-        self._feedBack("Parrot mode activated for %s" % (unicode(link_left_jid), ), mess_data, profile)
-
-        return False
-
-    def cmd_unparrot(self, mess_data, profile):
-        """remove Parrot mode between 2 entities, in both directions."""
-        debug("Catched unparrot command")
-
-        try:
-            link_left_jid = jid.JID(mess_data["unparsed"].strip())
-            if not link_left_jid.user or not link_left_jid.host:
-                raise jid.InvalidFormat
-        except jid.InvalidFormat:
-            self._feedBack("Can't deactivate Parrot mode for invalid jid", mess_data, profile)
-            return False
-
-        link_right_jid = mess_data['to']
-
-        self.host.plugins["EXP-PARROT"].removeParrot(link_left_jid, profile)
-        self.host.plugins["EXP-PARROT"].removeParrot(link_right_jid, profile)
-
-        self._feedBack("Parrot mode deactivated for %s and %s" % (unicode(link_left_jid), unicode(link_right_jid)), mess_data, profile)
-
-        return False
-
     def cmd_whois(self, mess_data, profile):
         """show informations on entity"""
         debug("Catched whois command")
@@ -259,7 +177,7 @@
                 if not target_jid.user or not target_jid.host:
                     raise jid.InvalidFormat
             except (jid.InvalidFormat, RuntimeError):
-                self._feedBack(_("Invalid jid, can't whois"), mess_data, profile)
+                self.feedBack(_("Invalid jid, can't whois"), mess_data, profile)
                 return False
 
         if not target_jid.resource:
@@ -267,23 +185,12 @@
 
         whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}]
 
-        # version
-        def versionCb(version_data):
-            name, version, os = version_data
-            if name:
-                whois_msg.append(_("Client name: %s") % name)
-            if version:
-                whois_msg.append(_("Client version: %s") % version)
-            if os:
-                whois_msg.append(_("Operating system: %s") % os)
-
-        d = self.host.plugins['XEP-0092'].getVersion(target_jid, profile)
-        d.addCallback(versionCb)
-
-        #TODO: add informations here (vcard, etc)
+        d = defer.succeed(None)
+        for ignore, callback in self._whois:
+            d.addCallback(lambda ignore: callback(whois_msg, target_jid, profile))
 
         def feedBack(ignore):
-            self._feedBack(u"\n".join(whois_msg), mess_data, profile)
+            self.feedBack(u"\n".join(whois_msg), mess_data, profile)
             return False
 
         d.addCallback(feedBack)
@@ -295,14 +202,14 @@
         longuest = max([len(command) for command in commands])
         help_cmds = []
 
-        for command in commands:
-            method = getattr(self, command)
+        for command in self._commands:
+            method = self._commands[command]
             try:
                 help_str = method.__doc__.split('\n')[0]
             except AttributeError:
                 help_str = ''
             spaces = (longuest - len(command)) * ' '
-            help_cmds.append("    /%s: %s %s" % (command[4:], spaces, help_str))
+            help_cmds.append("    /%s: %s %s" % (command, spaces, help_str))
 
         help_mess = _(u"Text commands available:\n%s") % (u'\n'.join(help_cmds), )
-        self._feedBack(help_mess, mess_data, profile)
+        self.feedBack(help_mess, mess_data, profile)
--- a/src/plugins/plugin_xep_0045.py	Sun Mar 23 10:02:50 2014 +0100
+++ b/src/plugins/plugin_xep_0045.py	Mon Mar 24 10:57:15 2014 +0100
@@ -43,6 +43,7 @@
     "type": "XEP",
     "protocols": ["XEP-0045"],
     "dependencies": [],
+    "recommendations": [C.TEXT_CMDS],
     "main": "XEP_0045",
     "handler": "yes",
     "description": _("""Implementation of Multi-User Chat""")
@@ -74,6 +75,10 @@
         host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss')  # args: room_jid, old_nick, new_nick, profile
         host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss')  # args: room_jid, subject, profile
         self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True)
+        try:
+            self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
+        except KeyError:
+            info(_("Text commands not available"))
 
     def __check_profile(self, profile):
         """check if profile is used and connected
@@ -348,6 +353,85 @@
         self.clients[profile] = SatMUCClient(self)
         return self.clients[profile]
 
+    # Text commands #
+
+    def cmd_nick(self, mess_data, profile):
+        """change nickname"""
+        debug("Catched nick command")
+
+        if mess_data['type'] != "groupchat":
+            #/nick command does nothing if we are not on a group chat
+            info("Ignoring /nick command on a non groupchat message")
+
+            return True
+
+        nick = mess_data["unparsed"].strip()
+        room = mess_data["to"]
+
+        self.nick(room, nick, profile)
+
+        return False
+
+    def cmd_join(self, mess_data, profile):
+        """join a new room (on the same service if full jid is not specified)"""
+        debug("Catched join command")
+
+        if mess_data['type'] != "groupchat":
+            #/leave command does nothing if we are not on a group chat
+            info("Ignoring /join command on a non groupchat message")
+            return True
+
+        if mess_data["unparsed"].strip():
+            room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
+            nick = (self.getRoomNick(mess_data["to"].userhost(), profile) or
+                    self.host.getClient(profile).jid.user)
+            self.join(room, nick, {}, profile)
+
+        return False
+
+    def cmd_leave(self, mess_data, profile):
+        """quit a room"""
+        debug("Catched leave command")
+
+        if mess_data['type'] != "groupchat":
+            #/leave command does nothing if we are not on a group chat
+            info("Ignoring /leave command on a non groupchat message")
+            return True
+
+        if mess_data["unparsed"].strip():
+            room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
+        else:
+            room = mess_data["to"]
+
+        self.leave(room, profile)
+
+        return False
+
+    def cmd_part(self, mess_data, profile):
+        """just a synonym of /leave"""
+        return self.cmd_leave(mess_data, profile)
+
+    def cmd_title(self, mess_data, profile):
+        """change room's subject"""
+        debug("Catched title command")
+
+        if mess_data['type'] != "groupchat":
+            #/leave command does nothing if we are not on a group chat
+            info("Ignoring /title command on a non groupchat message")
+            return True
+
+        subject = mess_data["unparsed"].strip()
+
+        if subject:
+            room = mess_data["to"]
+            self.subject(room, subject, profile)
+
+        return False
+
+    def cmd_topic(self, mess_data, profile):
+        """just a synonym of /title"""
+        return self.cmd_title(mess_data, profile)
+
 
 class SatMUCClient (muc.MUCClient):
     #implements(iwokkel.IDisco)
--- a/src/plugins/plugin_xep_0092.py	Sun Mar 23 10:02:50 2014 +0100
+++ b/src/plugins/plugin_xep_0092.py	Mon Mar 24 10:57:15 2014 +0100
@@ -31,6 +31,7 @@
     "type": "XEP",
     "protocols": ["XEP-0092"],
     "dependencies": [],
+    "recommendations": [C.TEXT_CMDS],
     "main": "XEP_0092",
     "handler": "no", # version is already handler in core.xmpp module
     "description": _("""Implementation of Software Version""")
@@ -42,6 +43,10 @@
     def __init__(self, host):
         info(_("Plugin XEP_0092 initialization"))
         self.host = host
+        try:
+            self.host.plugins[C.TEXT_CMDS].addWhoIsCb(self._whois, 100)
+        except KeyError:
+            info(_("Text commands not available"))
 
     def getVersion(self, jid_, profile_key=C.PROF_KEY_NONE):
         """ Ask version of the client that jid_ is running
@@ -78,3 +83,20 @@
         return tuple(ret)
 
 
+    def _whois(self, whois_msg, target_jid, profile):
+        """ Add software/OS information to whois """
+        def versionCb(version_data):
+            name, version, os = version_data
+            if name:
+                whois_msg.append(_("Client name: %s") % name)
+            if version:
+                whois_msg.append(_("Client version: %s") % version)
+            if os:
+                whois_msg.append(_("Operating system: %s") % os)
+        def versionEb(failure):
+            whois_msg.append(_("Can't find software informations"))
+
+        d = self.getVersion(target_jid, profile)
+        d.addCallbacks(versionCb, versionEb)
+        return d
+