hi
gloox currently supplies a complete infrastructure to execute a command
on a remote entity. unfortunately the infrastructure to allow command
execution within one's own bot/client is lacking.
the attach files (not patches), provide such a framework. the idea is to
spark conversation/ideas/discussion around the framework. the files
currently contains (some) lines of other code not included, and will NOT
compile as is. this is fairly easy to fix/change...
how do one use the framework? that is very easy as well:
1. create an instance of gloox::adhoc
2. create an instance of AdhocCommandManager
3. derive your own command implementations from AdhocCommand
4. register commands with AdhocCommandManager
(AdhocCommandManager::addCommand)
the framework was taken from wild-/openfire:
http://svn.igniterealtime.org/svn/repos/wildfire/trunk/src/java/org/jivesoftware/wildfire/commands/{AdHocCommand,
AdHocCommandHandler,SessionData}.java
i've removed 3 features from the java implementation, i.e
1. session expiration (this is trivial to add)
2. limit of simultaneous commands executed (can be re-added)
3. the AdhocCommand will always allow a client to execute the command
(see AdHocCommand.cpp#hasPermission). a subclass can choose to override
the default behavior.
a slighty tighter integration with gloox can be achieved by allowing
AdhocCommandManager to handle disco#items. In such a case only commands
which the user is allowed to execute will be shown during service discovery.
comments welcome.
regards
roelof.
#include "AdhocCommand.h"
#include <gloox/tag.h>
bool AdhocCommand::hasPermission(const std::string& bareJid) const
{
return true;
}
void AdhocCommand::addNextStageInformation(SessionData& data, gloox::Tag*
command)
{
// Increment the stage number to the next stage
data.setStage(data.getStage() + 1);
// Return the data form of the current stage to the command requester. The
// requester will need to specify the action to follow (e.g. execute, prev,
// cancel, etc.) and complete the form is going "forward"
addStageInformation(data, command);
// Include the available actions at this stage
addStageActions(data, command);
}
void AdhocCommand::addPreviousStageInformation(SessionData& data, gloox::Tag*
command)
{
// Decrement the stage number to the previous stage
data.setStage(data.getStage() - 1);
// Return the data form of the current stage to the command requester. The
// requester will need to specify the action to follow (e.g. execute, prev,
// cancel, etc.) and complete the form is going "forward"
addStageInformation(data, command);
// Include the available actions at this stage
addStageActions(data, command);
}
void AdhocCommand::addStageActions(SessionData& data, gloox::Tag* command)
{
// Add allowed actions to the response
gloox::Tag* actions = new gloox::Tag(command, "actions");
gloox::StringList validActions = getActions(data);
gloox::StringList::const_iterator it = validActions.begin();
for ( ; it != validActions.end(); ++it) {
new gloox::Tag(actions, *it);
} //for
std::string executeAction = getExecuteAction(data);
// Add default execute action to the response
actions->addAttribute("execute", executeAction);
// Store the allowed actions that the user can follow from this stage
data.setAllowedActions(validActions);
// Store the default execute action to follow if the user does not specify an
// action in his command
data.setExecuteAction(executeAction);
}
#ifndef ADHOCCOMMAND_H
#define ADHOCCOMMAND_H
#include <string>
#include "SessionData.h"
#include <gloox/gloox.h>
#include <gloox/jid.h>
class gloox::Tag;
/**
* An ad-hoc command is a stateless object responsbile for executing the
provided service. Each
* subclass will only have one instance that will be shared across all users
sessions. T
* Therefore, it is important to not keep any information related to executions
as permanent
* data (i.e. as instance or static variables). Each command has a
<tt>code</tt> that should be
* unique within a given JID.<p>
*
* Commands may have zero or more stages. Each stage is usually used for
gathering information
* required for the command execution. Users are able to move forward or
backward across the
* different stages. Commands may not be cancelled while they are beig
executed. However, users
* may request the "cancel" action when submiting a stage response indicating
that the command
* execution should be aborted. Thus, releasing any collected information.
Commands that require
* user interaction (i.e. have more than one stage) will have to provide the
data forms the user
* must complete in each stage and the allowed actions the user might perform
during each stage
* (e.g. go to the previous stage or go to the next stage).<p>
*
* This implemetation is taken from wild-/openfire.
* \see
http://svn.igniterealtime.org/svn/repos/wildfire/trunk/src/java/org/jivesoftware/wildfire/commands/AdHocCommand.java
*/
class AdhocCommand
{
public:
/**
* Destructor
*/
virtual ~AdhocCommand() {};
/**
* \return The descriptive label of the command
*/
virtual std::string getLabel() const = 0;
/**
* Returns true if the requester is allowed to execute this command.
Subclasses may
* redefine this method with any specific logic.<p>
*
* Note: The default implementation always return \c true
*
* \param requester the JID of the user requesting to execute this command.
* \return true if the requester is allowed to execute this command.
*/
virtual bool hasPermission(const std::string& bareJid) const;
/**
* Returns the unique identifier for this command for the containing JID.
The code will
* be used as the node in the disco#items or the node when executing the
command.
*
* \return the unique identifier for this command for the containing JID.
*/
virtual std::string getCode() const = 0;
/**
* Returns the max number of stages for this command. The number of stages
may vary
* according to the collected data in previous stages. Therefore, a
SessionData object is
* passed as a parameter. When the max number of stages has been reached
then the command
* is ready to be executed.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages or the requester is requesting the
execution for the
* first time.
* \return the max number of stages for this command.
*/
virtual int getMaxStages() const = 0;
/**
* Executes the command with the specified session data.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages.
* \param command the command element to be sent to the command requester
with a reported
* data result or note element with the answer of the execution.
*/
virtual void execute(SessionData& data, gloox::Tag* command) = 0;
/**
* Increments the stage number by one and adds to the command element the
new data form and
* new allowed actions that the user might perform.
*
* \param data the gathered data through the command stages.
* \param command the command element to be sent to the command requester.
*/
void addNextStageInformation(SessionData& data, gloox::Tag* command);
/**
* Decrements the stage number by one and adds to the command the data form
and allowed
* actions that the user might perform of the previous stage.
*
* \param data the gathered data through the command stages.
* \param command the command element to be sent to the command requester.
*/
void addPreviousStageInformation(SessionData& data, gloox::Tag* command);
protected:
/**
* Adds to the command element the data form or notes required by the
current stage. The
* current stage is specified in the SessionData. This method will never be
invoked for
* commands that have no stages.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages or the requester is requesting the
execution for the
* first time.
* \param command the command element to be sent to the command requester.
*/
virtual void addStageInformation(SessionData& data, gloox::Tag* command) =
0;
/**
* Returns a collection with the allowed actions based on the current stage
as defined
* in the SessionData. Possible actions are: <tt>prev</tt>, <tt>next</tt>
and
* <tt>complete</tt>. This method will never be invoked for commands that
have no stages.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages or the requester is requesting the
execution for the
* first time.
* \return a collection with the allowed actions based on the current stage
as defined
* in the SessionData.
*/
virtual gloox::StringList getActions(SessionData& data) const = 0;
/**
* Returns which of the actions available for the current stage is
considered the equivalent
* to "execute". When the requester sends his reply, if no action was
defined in the command
* then the action will be assumed "execute" thus assuming the action
returned by this
* method. This method will never be invoked for commands that have no
stages.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages or the requester is requesting the
execution for the
* first time.
* \return which of the actions available for the current stage is
considered the equivalent
to "execute".
*/
virtual std::string getExecuteAction(SessionData& data) const = 0;
/**
* Adds the allowed actions to follow from the current stage. Possible
actions are:
* <tt>prev</tt>, <tt>next</tt> and <tt>complete</tt>.
*
* \param data the gathered data through the command stages or
<tt>null</tt> if the
* command does not have stages or the requester is requesting the
execution for the
* first time.
* \param command the command element to be sent to the command requester.
*/
private:
/**
* Add the list of valid actions to the \c command element
* \param data The session data
* \param command The command xml element
*/
void addStageActions(SessionData& data, gloox::Tag* command);
};
#endif
#include "AdhocCommandManager.h"
#include <gloox/dataform.h>
#include <map>
#include <string>
#include "Logger.h"
#include "XmppUtil.h"
AdhocCommandManager::AdhocCommandManager(gloox::Adhoc* adhoc,
gloox::ClientBase* parent)
: m_adhoc(adhoc),
m_parent(parent)
{
}
AdhocCommandManager::~AdhocCommandManager()
{
}
void AdhocCommandManager::addCommand(AdhocCommand* command)
{
m_adhoc->registerAdhocCommandProvider(this, command->getCode(),
command->getLabel());
m_commands[command->getCode()] = AdhocCommandPtr(command);
}
void AdhocCommandManager::handleAdhocCommand(const std::string& commandCode,
gloox::Tag* tag,
const gloox::JID& from, const std::string& id)
{
LOG_DEBUG("Handling command[" << commandCode << "]\n");
std::string sessionid = tag->findAttribute("sessionid");
const std::string& bareJid = from.bare();
const std::string& fullJid = from.full();
std::auto_ptr<gloox::Tag> childElement(new gloox::Tag("command"));
childElement->addAttribute("xmlns", gloox::XMLNS_ADHOC_COMMANDS);
childElement->addAttribute("sessionid", sessionid);
childElement->addAttribute("node", commandCode);
AdhocCommandPtr command = m_commands[commandCode];
if (sessionid.empty()) {
LOG_DEBUG("No previous session!\n");
// Check that the requester has enough permission. Answer forbidden error if
// requester permissions are not enough to execute the requested command
if (!command->hasPermission(bareJid)) {
LOG_DEBUG("No permissions[" << bareJid << "]\n");
sendError(fullJid, commandCode, id, sessionid, "forbidden", "");
return;
} //if
//Create new session ID
sessionid = m_parent->getID();
childElement->addAttribute("sessionid", sessionid);
if (command->getMaxStages() == 0) {
// The command does not require any user interaction (returns results
only)
// Execute the command and return the execution result which may be a
// data form (i.e. report data) or a note element
SessionData tmp;
command->execute(tmp, childElement.get());
childElement->addAttribute("status", "completed");
} else {
// The command requires user interactions (ie. has stages)
// Originate a new command session.
SessionDataPtr session(new SessionData(sessionid, fullJid));
m_sessions[sessionid] = session;
childElement->addAttribute("status", "executing");
// Add to the child element the data form the user must complete and
// the allowed actions
command->addNextStageInformation(*session, childElement.get());
} //if
} else {
// An execution session already exists and the user has requested to
perform a
// certain action.
std::string action = tag->findAttribute("action");
// Check that a Session exists for the specified sessionID
if (m_sessions.find(sessionid) == m_sessions.end()) {
// Answer a bad_request error (bad-sessionid)
sendError(fullJid, commandCode, id, sessionid, "bad-request",
"bad-sessionid");
return;
} //if
SessionDataPtr session = m_sessions[sessionid];
// Check if the user is requesting to cancel the command
if (action == "cancel") {
// User requested to cancel command execution so remove the session data
removeSessionData(sessionid);
sendCanceled(fullJid, id, commandCode, sessionid);
return;
} //if
// If the user didn't specify an action then follow the default execute
action
if (action.empty() || (action == "execute")) {
action = session->getExecuteAction();
}
// Check that the specified action was previously offered
if (!session->isValidAction(action)) {
// Answer a bad_request error (bad-action)
sendError(fullJid, commandCode, id, sessionid, "bad_request",
"bad-action");
return;
} else if (action == "prev") {
// Move to the previous stage and add to the child element the data form
// the user must complete and the allowed actions of the previous stage
childElement->addAttribute("status", "executing");
command->addPreviousStageInformation(*session, childElement.get());
} else if (action == "next") {
// Store the completed form in the session data
saveCompletedForm(tag, *session);
// Move to the next stage and add to the child element the new data form
// the user must complete and the new allowed actions
childElement->addAttribute("status", "executing");
command->addNextStageInformation(*session, childElement.get());
} else if (action == "complete") {
// Store the completed form in the session data
saveCompletedForm(tag, *session);
// Execute the command and return the execution result which may be a
// data form (i.e. report data) or a note element
command->execute(*session, childElement.get());
childElement->addAttribute("status", "completed");
// Command has been executed so remove the session data
removeSessionData(sessionid);
} //if
} //if
//send the reply
gloox::Tag* iq = XmppUtil::createIq("result", fullJid,
m_parent->jid().full(), id);
iq->addChild(childElement.release());
m_parent->send(iq);
}
void AdhocCommandManager::saveCompletedForm(gloox::Tag* iqCommand, SessionData&
session)
{
gloox::Tag* formElement = iqCommand->findChild("x", "xmlns",
gloox::XMLNS_X_DATA);
if (formElement != 0) {
SessionData::DataFormPtr dataForm(new gloox::DataForm(formElement));
// Store the variables and their values in the session data
session.addStageForm(dataForm);
} //if
}
void AdhocCommandManager::removeSessionData(const std::string& sessionid)
{
m_sessions.erase(sessionid);
}
void AdhocCommandManager::sendError(const std::string& to, const std::string&
cmd,
const std::string& id, const std::string& sessionId, const std::string&
reason,
const std::string& specific) const
{
gloox::Tag* iq = XmppUtil::createIq("error", to, m_parent->jid().full(), id);
XmppUtil::createCmdNode(iq, cmd, "execute", sessionId);
XmppUtil::createError(iq, "modify", "400", reason, specific);
m_parent->send(iq);
}
void AdhocCommandManager::sendCanceled(const std::string& to, const
std::string& id,
const std::string& command, const std::string& sessionid) const
{
gloox::Tag* iq = XmppUtil::createIq("result", to, m_parent->jid().full(), id);
XmppUtil::createCmdNode(iq, command, "canceled", sessionid);
m_parent->send(iq);
}
#ifndef ADHOCCOMMANDMANAGER_H
#define ADHOCCOMMANDMANAGER_H
#include <map>
#include <string>
#include <boost/shared_ptr.hpp>
#include "gloox/adhoc.h"
#include "gloox/adhoccommandprovider.h"
#include "gloox/clientbase.h"
#include "gloox/tag.h"
#include "AdhocCommand.h"
/**
* An AdHocCommandHandler is responsbile for providing discoverable information
about the
* supported commands and for handling commands requests. This is an
implementation of JEP-50:
* Ad-Hoc Commands.<p>
*
* Ad-hoc commands that require user interaction will have one or more stages.
For each
* stage the user will complete a data form and send it back to the server. The
data entered
* by the user is kept in a SessionData. Instances of {@link AdhocCommand} are
stateless.
*
* New commands can be added dynamically by sending the message {@link
#addCommand(AdHocCommand)}.
* The command will immediatelly appear in the disco#items list and might be
executed by those
* users with enough execution permissions.
*
* This implementation is taken from wild-/openfire
* \see
http://svn.igniterealtime.org/svn/repos/wildfire/trunk/src/java/org/jivesoftware/wildfire/commands/AdHocCommandHandler.java
*/
class AdhocCommandManager : public gloox::AdhocCommandProvider
{
public:
/**
* Constructor
* \param adhoc The adhoc command implementation
* \param parent The client base to use for sending messages
*/
AdhocCommandManager(gloox::Adhoc* adhoc, gloox::ClientBase* parent);
/**
* Destructor
*/
~AdhocCommandManager();
/**
* Adds a new command to the list of supported ad-hoc commands by this
server. The new
* command will appear in the discoverable items list and will be executed
for those users
* with enough permission.
*
* \param command the new ad-hoc command to add.
*/
void addCommand(AdhocCommand* command);
//gloox api callbacks AdhocCommandProvider
virtual void handleAdhocCommand(const std::string& command, gloox::Tag*
tag,
const gloox::JID& from, const std::string& id);
private:
/**
* Auto delete gloox::Adhoc
*/
std::auto_ptr<gloox::Adhoc> m_adhoc;
/**
* Client for sending messages
*/
gloox::ClientBase* m_parent;
/**
* Type definition for adhoc command implementations
*/
typedef boost::shared_ptr<AdhocCommand> AdhocCommandPtr;
/**
* Type definition for session data
*/
typedef boost::shared_ptr<SessionData> SessionDataPtr;
/**
* Map that holds the offered commands by this service. Note:
Key=commandCode,
* Value=command.
* commandCode matches the node attribute sent by command requesters.
*/
typedef std::map<std::string, AdhocCommandPtr> CommandMap;
CommandMap m_commands;
/**
* Map that holds the command sessions. Used mainly to quickly locate a
SessionData.
* Note: Key=sessionID, Value=SessionData
*/
typedef std::map<std::string, SessionDataPtr> SessionMap;
SessionMap m_sessions;
/**
* Stores in the SessionData the fields and their values as specified in
the completed
* data form by the user.
*
* \param iqCommand the command element containing the data form element.
* \param session the SessionData for this command execution.
*/
void saveCompletedForm(gloox::Tag* iqCommand, SessionData& session);
/**
* Releases the data kept for the command execution whose id is sessionid.
The number of
* commands executions currently being executed by the user (full JID) will
be decreased.
*
* \param sessionid id of the session that identifies this command
execution.
* \param from the full JID of the command requester.
*/
void removeSessionData(const std::string& sessionid);
/**
* Send an IQ error to \c to
* \param to The jid to which the error should be send
* \param cmd The command name
* \param id The original message id
* \param sessionid The original session id
* \param reason The stanza reason
* \param specific The adhoc specific reason
*/
void sendError(const std::string& to, const std::string& cmd, const
std::string& id,
const std::string& sessionId, const std::string& reason,
const std::string& specific) const;
/**
* If a recipient cancel command execution, send 'canceled' as confirmation
* \param to The jid to which the confirmation should be send
* \param id The original message id
* \param command The command name
* \param sessionid The original session id
*/
void sendCanceled(const std::string& to, const std::string& id, const
std::string& command,
const std::string& sessionid) const;
};
#endif
#include "SessionData.h"
SessionData::SessionData(const std::string& sessionid, const gloox::JID& owner)
: m_creationStamp(std::time(0)),
m_id(sessionid),
m_owner(owner),
m_stagesData(),
m_executeAction(),
m_allowedActions(),
m_stage(-1)
{
}
SessionData::SessionData()
: m_creationStamp(0),
m_id(),
m_owner(),
m_stagesData(),
m_executeAction(),
m_allowedActions(),
m_stage(-1)
{
}
SessionData::~SessionData()
{
}
const std::string& SessionData::getId() const
{
return m_id;
}
const gloox::JID& SessionData::getOwner() const
{
return m_owner;
}
std::time_t SessionData::getCreationStamp() const
{
return m_creationStamp;
}
const std::string& SessionData::getExecuteAction() const
{
return m_executeAction;
}
void SessionData::setExecuteAction(const std::string& executeAction)
{
m_executeAction = executeAction;
}
void SessionData::setAllowedActions(const gloox::StringList& allowedActions)
{
m_allowedActions = allowedActions;
}
bool SessionData::isValidAction(const std::string& actionName)
{
gloox::StringList::const_iterator it = m_allowedActions.begin();
for( ; it != m_allowedActions.end(); ++it) {
if (*it == actionName)
return true;
} //for
return false;
}
void SessionData::addStageForm(DataFormPtr& data)
{
m_stagesData[m_stage] = data;
}
SessionData::DataFormPtr& SessionData::getData(int stage)
{
if ((stage < 0) || (stage > m_stage))
return m_stagesData[m_stage];
return m_stagesData[stage];
}
int SessionData::getStage() const
{
return m_stage;
}
void SessionData::setStage(int stage)
{
m_stage = stage;
}
#ifndef SESSIONDATA_H
#define SESSIONDATA_H
#include <ctime>
#include <boost/shared_ptr.hpp>
#include <gloox/dataform.h>
#include <gloox/gloox.h>
#include <gloox/jid.h>
/**
* A SessionData instance is responsible for keeping information gathered
during the many stages
* of the command being executed. Each session data is associated with the
<tt>sessionid</tt>
* attribute included in the <tt>command</tt> child element of the IQ packet.
*
* This implementation is taken from wild-/openfire
* \see
http://svn.igniterealtime.org/svn/repos/wildfire/trunk/src/java/org/jivesoftware/wildfire/commands/SessionData.java
*/
class SessionData
{
public:
/**
* Type definition to allow the sharing of data forms
*/
typedef boost::shared_ptr<gloox::DataForm> DataFormPtr;
/**
* Constructor
* \param sessionid The session's unique id
* \param owner The owner of the session
*/
SessionData(const std::string& sessionid, const gloox::JID& owner);
/**
* Dummy constructor.
*
* This is only used when contructing a temporary empty session
*/
SessionData();
/**
* Destructor
*/
~SessionData();
/**
* \return The session id
*/
const std::string& getId() const;
/**
* \return the JID of the entity that is executing the command.
*/
const gloox::JID& getOwner() const;
/**
* \return The time when the session was created
*/
std::time_t getCreationStamp() const;
/**
* \return The current stage's execute action
*/
const std::string& getExecuteAction() const;
/**
* Set the current execute action
* \param executeAction The execute action
*/
void setExecuteAction(const std::string& executeAction);
/**
* Sets the valid actions that the user can follow from the current stage.
*
* \param allowedActions list of valid actions.
*/
void setAllowedActions(const gloox::StringList& allowedActions);
/**
* Returns true if the specified action is valid in the current stage. The
action should
* have previously been offered to the user.
*
* \param actionName the name of the action to validate.
* \return true if the specified action is valid in the current stage.
*/
bool isValidAction(const std::string& actionName);
/**
* Add the data form for the current stage
* \param data The stage's data form
*/
void addStageForm(DataFormPtr& data);
/**
* Return the data form for the specified stage. Call the method with no
parameter, or
* pass -1, to retrieve the current stage's data.
* \param stage The stage in which data you are interested
* @return The data form
*/
DataFormPtr& getData(int stage = -1);
/**
* Returns the current stage where the requester is located. Stages are
numbered from 0. A
* stage with value 0 means that a command request has just been received
and no data form
* has been sent to the requester yet. The first sent data form of the
first stage would be
* represented as stage 1.
*
* @return the current stage where the requester is located.
*/
int getStage() const;
/**
* Sets the current stage where the requester is located. Stages are
numbered from 0. A
* stage with value 0 means that a command request has just been received
and no data form
* has been sent to the requester yet. The first sent data form of the
first stage would be
* represented as stage 1.
*
* @param stage the current stage where the requester is located.
*/
void setStage(int stage);
private:
/**
* Time of session creation
*/
std::time_t m_creationStamp;
/**
* Session's id
*/
std::string m_id;
/**
* Owner of session
*/
gloox::JID m_owner;
/**
* Map that keeps the association of variables and values obtained in each
stage.
* Note: Key=stage number, Value=DataForm.
*/
typedef std::map<int, DataFormPtr> StagesData;
StagesData m_stagesData;
/**
* Keeps the default execution action to follow if the command requester
does not include
* an action in his command.
*/
std::string m_executeAction;
/**
* List of allowed actions
*/
gloox::StringList m_allowedActions;;
/**
* Indicates the current stage where the requester is located. Stages are
numbered from 0.
*/
int m_stage;
};
#endif