~singpolyma/biboumi

eb9a20187098185cc10ad192e91a90dbba12633a — Florent Le Coz 9 years ago 1c93afc
Implement the support for adhoc commands

We have two basic example commands. But it’s not entirely finished because
there are some error checks that we don’t do.

ref #2521
A src/xmpp/adhoc_command.cpp => src/xmpp/adhoc_command.cpp +82 -0
@@ 0,0 1,82 @@
#include <xmpp/adhoc_command.hpp>

using namespace std::string_literals;

AdhocCommand::AdhocCommand(std::vector<AdhocStep>&& callbacks, const std::string& name, const bool admin_only):
  name(name),
  callbacks(std::move(callbacks)),
  admin_only(admin_only)
{
}

AdhocCommand::~AdhocCommand()
{
}

void PingStep1(AdhocSession&, XmlNode& command_node)
{
  XmlNode note("note");
  note["type"] = "info";
  note.set_inner("Pong");
  note.close();
  command_node.add_child(std::move(note));
}

void HelloStep1(AdhocSession&, XmlNode& command_node)
{
  XmlNode x("jabber:x:data:x");
  x["type"] = "form";
  XmlNode title("title");
  title.set_inner("Configure your name.");
  title.close();
  x.add_child(std::move(title));
  XmlNode instructions("instructions");
  instructions.set_inner("Please provide your name.");
  instructions.close();
  x.add_child(std::move(instructions));
  XmlNode name_field("field");
  name_field["var"] = "name";
  name_field["type"] = "text-single";
  name_field["label"] = "Your name";
  XmlNode required("required");
  required.close();
  name_field.add_child(std::move(required));
  name_field.close();
  x.add_child(std::move(name_field));
  x.close();
  command_node.add_child(std::move(x));
}

void HelloStep2(AdhocSession& session, XmlNode& command_node)
{
  // Find out if the name was provided in the form.
  XmlNode* x = command_node.get_child("x", "jabber:x:data");
  if (x)
    {
      XmlNode* name_field = nullptr;
      for (XmlNode* field: x->get_children("field", "jabber:x:data"))
        if (field->get_tag("var") == "name")
          {
            name_field = field;
            break;
          }
      if (name_field)
        {
          XmlNode* value = name_field->get_child("value", "jabber:x:data");
          if (value)
            {
              XmlNode note("note");
              note["type"] = "info";
              note.set_inner("Hello "s + value->get_inner() + "!"s);
              note.close();
              command_node.delete_all_children();
              command_node.add_child(std::move(note));
              return;
            }
        }
    }
  // TODO insert an error telling the name value is missing.  Also it's
  // useless to terminate it, since this step is the last of the command
  // anyway. But this is for the example.
  session.terminate();
}

A src/xmpp/adhoc_command.hpp => src/xmpp/adhoc_command.hpp +40 -0
@@ 0,0 1,40 @@
#ifndef ADHOC_COMMAND_HPP
# define ADHOC_COMMAND_HPP

/**
 * Describe an ad-hoc command.
 *
 * Can only have zero or one step for now. When execution is requested, it
 * can return a result immediately, or provide a form to be filled, and
 * provide a result once the filled form is received.
 */

#include <xmpp/adhoc_session.hpp>

#include <functional>
#include <string>

class AdhocCommand
{
  friend class AdhocSession;
public:
  AdhocCommand(std::vector<AdhocStep>&& callback, const std::string& name, const bool admin_only);
  ~AdhocCommand();

  const std::string name;

private:
  /**
   * A command may have one or more steps. Each step is a different
   * callback, inserting things into a <command/> XmlNode and calling
   * methods of an AdhocSession.
   */
  std::vector<AdhocStep> callbacks;
  const bool admin_only;
};

void PingStep1(AdhocSession& session, XmlNode& command_node);
void HelloStep1(AdhocSession& session, XmlNode& command_node);
void HelloStep2(AdhocSession& session, XmlNode& command_node);

#endif // ADHOC_COMMAND_HPP

A src/xmpp/adhoc_commands_handler.cpp => src/xmpp/adhoc_commands_handler.cpp +100 -0
@@ 0,0 1,100 @@
#include <xmpp/adhoc_commands_handler.hpp>
#include <xmpp/xmpp_component.hpp>

#include <logger/logger.hpp>

#include <iostream>

AdhocCommandsHandler::AdhocCommandsHandler():
  commands{
  {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)},
  {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)}
  }
{
}

AdhocCommandsHandler::~AdhocCommandsHandler()
{
}

const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() const
{
  return this->commands;
}

XmlNode&& AdhocCommandsHandler::handle_request(const std::string& executor_jid, XmlNode command_node)
{
  // TODO check the type of action. Currently it assumes it is always
  // 'execute'.
  std::string action = command_node.get_tag("action");
  if (action.empty())
    action = "execute";
  command_node.del_tag("action");

  const std::string node = command_node.get_tag("node");
  auto command_it = this->commands.find(node);
  if (command_it == this->commands.end())
    {
      XmlNode error(ADHOC_NS":error");
      error["type"] = "cancel";
      XmlNode condition(STANZA_NS":item-not-found");
      condition.close();
      error.add_child(std::move(condition));
      error.close();
      command_node.add_child(std::move(error));
    }
  else
    {
      std::string sessionid = command_node.get_tag("sessionid");
      if (sessionid.empty())
        {                       // create a new session, with a new id
          sessionid = XmppComponent::next_id();
          command_node["sessionid"] = sessionid;
          this->sessions.emplace(std::piecewise_construct,
                                 std::forward_as_tuple(sessionid, executor_jid),
                                 std::forward_as_tuple(command_it->second, executor_jid));
          // TODO add a timed event to have an expiration date that deletes
          // this session. We could have a nasty client starting commands
          // but never finishing the last step, and that would fill the map
          // with dummy sessions.
        }
      auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
      if (session_it == this->sessions.end())
        {
          XmlNode error(ADHOC_NS":error");
          error["type"] = "modify";
          XmlNode condition(STANZA_NS":bad-request");
          condition.close();
          error.add_child(std::move(condition));
          error.close();
          command_node.add_child(std::move(error));
        }
      else
        {
          // execute the step
          AdhocSession& session = session_it->second;
          const AdhocStep& step = session.get_next_step();
          step(session, command_node);
          if (session.remaining_steps() == 0 ||
              session.is_terminated())
            {
              this->sessions.erase(session_it);
              command_node["status"] = "completed";
            }
          else
            {
              command_node["status"] = "executing";
              XmlNode actions("actions");
              XmlNode next("next");
              next.close();
              actions.add_child(std::move(next));
              actions.close();
              command_node.add_child(std::move(actions));
            }
        }
    }
  // TODO remove that once we make sure so session can stay there for ever,
  // by mistake.
  log_debug("Number of existing sessions: " << this->sessions.size());
  return std::move(command_node);
}

A src/xmpp/adhoc_commands_handler.hpp => src/xmpp/adhoc_commands_handler.hpp +56 -0
@@ 0,0 1,56 @@
#ifndef ADHOC_COMMANDS_HANDLER_HPP
# define ADHOC_COMMANDS_HANDLER_HPP

/**
 * Manage a list of available AdhocCommands and the list of ongoing
 * AdhocCommandSessions.
 */

#include <xmpp/adhoc_command.hpp>
#include <xmpp/xmpp_stanza.hpp>

#include <utility>
#include <string>
#include <map>

class AdhocCommandsHandler
{
public:
  explicit AdhocCommandsHandler();
  ~AdhocCommandsHandler();
  /**
   * Returns the list of available commands.
   */
  const std::map<const std::string, const AdhocCommand>& get_commands() const;
  /**
   * Find the requested command, create a new session or use an existing
   * one, and process the request (provide a new form, an error, or a
   * result).
   *
   * Returns a (moved) XmlNode that will be inserted in the iq response. It
   * should be a <command/> node containing one or more useful children. If
   * it contains an <error/> node, the iq response will have an error type.
   *
   * Takes a copy of the <command/> node so we can actually edit it and use
   * it as our return value.
   */
  XmlNode&& handle_request(const std::string& executor_jid, XmlNode command_node);
private:
  /**
   * The list of all available commands.
   */
  const std::map<const std::string, const AdhocCommand> commands;
  /**
   * The list of all currently on-going commands.
   *
   * Of the form: {{session_id, owner_jid}, session}.
   */
  std::map<std::pair<const std::string, const std::string>, AdhocSession> sessions;

  AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
  AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
  AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
  AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
};

#endif // ADHOC_COMMANDS_HANDLER_HPP

M src/xmpp/xmpp_component.cpp => src/xmpp/xmpp_component.cpp +58 -4
@@ 447,6 447,22 @@ void XmppComponent::handle_iq(const Stanza& stanza)
                }
            }
        }
      else if ((query = stanza.get_child("command", ADHOC_NS)))
        {
          Stanza response("iq");
          response["to"] = from;
          response["from"] = this->served_hostname;
          response["id"] = id;
          XmlNode inner_node = this->adhoc_commands_handler.handle_request(from, *query);
          if (inner_node.get_child("error", ADHOC_NS))
            response["type"] = "error";
          else
            response["type"] = "result";
          response.add_child(std::move(inner_node));
          response.close();
          this->send_stanza(response);
          stanza_error.disable();
        }
    }
  else if (type == "get")
    {


@@ 454,8 470,22 @@ void XmppComponent::handle_iq(const Stanza& stanza)
      if ((query = stanza.get_child("query", DISCO_INFO_NS)))
        { // Disco info
          if (to_str == this->served_hostname)
            { // On the gateway itself
              this->send_self_disco_info(id, from);
            {
              const std::string node = query->get_tag("node");
              if (node.empty())
                {
                  // On the gateway itself
                  this->send_self_disco_info(id, from);
                  stanza_error.disable();
                }
            }
        }
      else if ((query = stanza.get_child("query", DISCO_ITEMS_NS)))
        {
          const std::string node = query->get_tag("node");
          if (node == ADHOC_NS)
            {
              this->send_adhoc_commands_list(id, from);
              stanza_error.disable();
            }
        }


@@ 848,8 878,7 @@ void XmppComponent::send_self_disco_info(const std::string& id, const std::strin
  identity["name"] = "Biboumi XMPP-IRC gateway";
  identity.close();
  query.add_child(std::move(identity));
  for (const std::string& ns: {"http://jabber.org/protocol/disco#info",
                                    "http://jabber.org/protocol/muc"})
  for (const std::string& ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS})
    {
      XmlNode feature("feature");
      feature["var"] = ns;


@@ 862,6 891,31 @@ void XmppComponent::send_self_disco_info(const std::string& id, const std::strin
  this->send_stanza(iq);
}

void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::string& requester_jid)
{
  Stanza iq("iq");
  iq["type"] = "result";
  iq["id"] = id;
  iq["to"] = requester_jid;
  iq["from"] = this->served_hostname;
  XmlNode query("query");
  query["xmlns"] = DISCO_ITEMS_NS;
  query["node"] = ADHOC_NS;
  for (const auto& kv: this->adhoc_commands_handler.get_commands())
    {
      XmlNode item("item");
      item["jid"] = this->served_hostname;
      item["node"] = kv.first;
      item["name"] = kv.second.name;
      item.close();
      query.add_child(std::move(item));
    }
  query.close();
  iq.add_child(std::move(query));
  iq.close();
  this->send_stanza(iq);
}

void XmppComponent::send_iq_version_request(const std::string& from,
                                            const std::string& jid_to)
{

M src/xmpp/xmpp_component.hpp => src/xmpp/xmpp_component.hpp +7 -0
@@ 1,6 1,7 @@
#ifndef XMPP_COMPONENT_INCLUDED
# define XMPP_COMPONENT_INCLUDED

#include <xmpp/adhoc_commands_handler.hpp>
#include <network/socket_handler.hpp>
#include <xmpp/xmpp_parser.hpp>
#include <bridge/bridge.hpp>


@@ 178,6 179,11 @@ public:
   */
  void send_self_disco_info(const std::string& id, const std::string& jid_to);
  /**
   * Send the list of all available ad-hoc commands to that JID. The list is
   * different depending on what JID made the request.
   */
  void send_adhoc_commands_list(const std::string& id, const std::string& requester_jid);
  /**
   * Send an iq version request
   */
  void send_iq_version_request(const std::string& from,


@@ 231,6 237,7 @@ private:

  static unsigned long current_id;

  AdhocCommandsHandler adhoc_commands_handler;
  XmppComponent(const XmppComponent&) = delete;
  XmppComponent(XmppComponent&&) = delete;
  XmppComponent& operator=(const XmppComponent&) = delete;

M src/xmpp/xmpp_stanza.cpp => src/xmpp/xmpp_stanza.cpp +7 -0
@@ 243,6 243,13 @@ const std::string XmlNode::get_tag(const std::string& name) const
    }
}

bool XmlNode::del_tag(const std::string& name)
{
  if (this->attributes.erase(name) != 0)
    return true;
  return false;
}

std::string& XmlNode::operator[](const std::string& name)
{
  return this->attributes[name];

M src/xmpp/xmpp_stanza.hpp => src/xmpp/xmpp_stanza.hpp +20 -1
@@ 34,6 34,21 @@ public:
  {
    node.parent = nullptr;
  }
  /**
   * The copy constructor do not copy the children or parent attributes. The
   * copied node is identical to the original except that it is not attached
   * to any other node.
   */
  XmlNode(const XmlNode& node):
    name(node.name),
    parent(nullptr),
    closed(node.closed),
    attributes(node.attributes),
    children{},
    inner(node.inner),
    tail(node.tail)
  {
  }

  ~XmlNode();



@@ 104,6 119,11 @@ public:
   */
  const std::string get_tag(const std::string& name) const;
  /**
   * Remove the attribute of the node. Does nothing if that attribute is not
   * present. Returns true if the tag was removed, false if it was absent.
   */
  bool del_tag(const std::string& name);
  /**
   * Use this to set an attribute's value, like node["id"] = "12";
   */
  std::string& operator[](const std::string& name);


@@ 117,7 137,6 @@ private:
  std::string inner;
  std::string tail;

  XmlNode(const XmlNode&) = delete;
  XmlNode& operator=(const XmlNode&) = delete;
  XmlNode& operator=(XmlNode&&) = delete;
};