# HG changeset patch # User Goffi # Date 1395655035 -3600 # Node ID d609581bf74acab602e88da9d4ba0339d6aa9c78 # Parent 5c78cefd233f194ae0e7a9fd797cd9e151f6f717 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 diff -r 5c78cefd233f -r d609581bf74a src/core/constants.py --- 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' diff -r 5c78cefd233f -r d609581bf74a src/plugins/plugin_exp_parrot.py --- 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 . 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 diff -r 5c78cefd233f -r d609581bf74a src/plugins/plugin_misc_text_commands.py --- 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 . 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) diff -r 5c78cefd233f -r d609581bf74a src/plugins/plugin_xep_0045.py --- 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) diff -r 5c78cefd233f -r d609581bf74a src/plugins/plugin_xep_0092.py --- 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 +