diff --git a/src/core/constants.py b/src/core/constants.py
index 8f5c7f96..ab95c9af 100644
--- a/src/core/constants.py
+++ b/src/core/constants.py
@@ -36,3 +36,6 @@ class Const(object):
PROF_KEY_NONE = '@NONE@'
PROF_KEY_DEFAULT = '@DEFAULT@'
IQ_SET = '/iq[@type="set"]'
+
+ # names of widely used plugins
+ TEXT_CMDS = 'TEXT-COMMANDS'
diff --git a/src/plugins/plugin_exp_parrot.py b/src/plugins/plugin_exp_parrot.py
index 96242533..64687513 100755
--- a/src/plugins/plugin_exp_parrot.py
+++ b/src/plugins/plugin_exp_parrot.py
@@ -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 @@ PLUGIN_INFO = {
"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 @@ class Exp_Parrot(object):
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 @@ class Exp_Parrot(object):
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 --git a/src/plugins/plugin_misc_text_commands.py b/src/plugins/plugin_misc_text_commands.py
index a2626b20..5e6cb2b3 100755
--- a/src/plugins/plugin_misc_text_commands.py
+++ b/src/plugins/plugin_misc_text_commands.py
@@ -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 @@ from logging import debug, info, warning, error
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 --git a/src/plugins/plugin_xep_0045.py b/src/plugins/plugin_xep_0045.py
index cb1bc8c5..9f8ba4b0 100755
--- a/src/plugins/plugin_xep_0045.py
+++ b/src/plugins/plugin_xep_0045.py
@@ -43,6 +43,7 @@ PLUGIN_INFO = {
"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 @@ class XEP_0045(object):
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 @@ class XEP_0045(object):
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 --git a/src/plugins/plugin_xep_0092.py b/src/plugins/plugin_xep_0092.py
index c41189d8..ed8c4386 100644
--- a/src/plugins/plugin_xep_0092.py
+++ b/src/plugins/plugin_xep_0092.py
@@ -31,6 +31,7 @@ PLUGIN_INFO = {
"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 @@ class XEP_0092(object):
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 @@ class XEP_0092(object):
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
+