~singpolyma/biboumi

1f6eea62f46789c0d3ebe7da133ccad2e59c89c8 — Florent Le Coz 8 years ago 700a6c7
Add an ad-hoc command to disconnect a user from one or more IRC server

fix #3077
M doc/biboumi.1.md => doc/biboumi.1.md +6 -0
@@ 410,6 410,12 @@ Biboumi supports a few ad-hoc commands, as described in the XEP 0050.
    “Gateway shutdown” quit message, except that biboumi does not exit when
    using this ad-hoc command.

  - disconnect-from-irc-servers: Disconnect a single user from one or more
    IRC server.  The user is immediately disconnected by closing the socket,
    no message is sent to the IRC server, but the user is of course notified
    with an XMPP message.  The administrator can disconnect any user, while
    the other users can only disconnect themselves.

### Raw IRC messages

Biboumi tries to support as many IRC features as possible, but doesn’t

M louloulibs/xmpp/xmpp_stanza.cpp => louloulibs/xmpp/xmpp_stanza.cpp +5 -0
@@ 260,3 260,8 @@ std::string sanitize(const std::string& data)
  else
    return xml_escape(utils::remove_invalid_xml_chars(utils::convert_to_utf8(data, "ISO-8859-1")));
}

std::ostream& operator<<(std::ostream& os, const XmlNode& node)
{
  return os << node.to_string();
}

M louloulibs/xmpp/xmpp_stanza.hpp => louloulibs/xmpp/xmpp_stanza.hpp +2 -0
@@ 140,4 140,6 @@ private:
 */
typedef XmlNode Stanza;

std::ostream& operator<<(std::ostream& os, const XmlNode& node);

#endif // XMPP_STANZA_INCLUDED

M src/bridge/bridge.cpp => src/bridge/bridge.cpp +5 -0
@@ 690,3 690,8 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe
        ++it;
    }
}

std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
{
  return this->irc_clients;
}

M src/bridge/bridge.hpp => src/bridge/bridge.hpp +1 -0
@@ 187,6 187,7 @@ public:
   * iq_responder_callback_t and remove the callback from the list.
   */
  void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message);
  std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients();

private:
  /**

M src/irc/irc_message.hpp => src/irc/irc_message.hpp +3 -3
@@ 8,9 8,9 @@
class IrcMessage
{
public:
  explicit IrcMessage(std::string&& line);
  explicit IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
  explicit IrcMessage(std::string&& command, std::vector<std::string>&& args);
  IrcMessage(std::string&& line);
  IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
  IrcMessage(std::string&& command, std::vector<std::string>&& args);
  ~IrcMessage();

  std::string prefix;

M src/xmpp/biboumi_adhoc_commands.cpp => src/xmpp/biboumi_adhoc_commands.cpp +166 -0
@@ 313,3 313,169 @@ void ConfigureIrcServerStep2(XmppComponent*, AdhocSession& session, XmlNode& com
  session.terminate();
}
#endif  // USE_DATABASE

void DisconnectUserFromServerStep1(XmppComponent* xmpp_component, AdhocSession& session, XmlNode& command_node)
{
  const Jid owner(session.get_owner_jid());
  if (owner.bare() != Config::get("admin", ""))
    { // A non-admin is not allowed to disconnect other users, only
      // him/herself, so we just skip this step
      auto next_step = session.get_next_step();
      next_step(xmpp_component, session, command_node);
    }
  else
    { // Send a form to select the user to disconnect
      auto biboumi_component = static_cast<BiboumiComponent*>(xmpp_component);

      XmlNode x("jabber:x:data:x");
      x["type"] = "form";
      XmlNode title("title");
      title.set_inner("Disconnect a user from selected IRC servers");
      x.add_child(std::move(title));
      XmlNode instructions("instructions");
      instructions.set_inner("Choose a user JID");
      x.add_child(std::move(instructions));
      XmlNode jids_field("field");
      jids_field["var"] = "jid";
      jids_field["type"] = "list-single";
      jids_field["label"] = "The JID to disconnect";
      XmlNode required("required");
      jids_field.add_child(std::move(required));
      for (Bridge* bridge: biboumi_component->get_bridges())
        {
          XmlNode option("option");
          option["label"] = bridge->get_jid();
          XmlNode value("value");
          value.set_inner(bridge->get_jid());
          option.add_child(std::move(value));
          jids_field.add_child(std::move(option));
        }
      x.add_child(std::move(jids_field));
      command_node.add_child(std::move(x));
    }
}

void DisconnectUserFromServerStep2(XmppComponent* xmpp_component, AdhocSession& session, XmlNode& command_node)
{
  // If no JID is contained in the command node, it means we skipped the
  // previous stage, and the jid to disconnect is the executor's jid
  std::string jid_to_disconnect = session.get_owner_jid();

  if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
    {
      for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
        if (field->get_tag("var") == "jid")
          {
            if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
              jid_to_disconnect = value->get_inner();
          }
    }

  // Save that JID for the last step
  session.vars["jid"] = jid_to_disconnect;

  // Send a data form to let the user choose which server to disconnect the
  // user from
  command_node.delete_all_children();
  auto biboumi_component = static_cast<BiboumiComponent*>(xmpp_component);

  XmlNode x("jabber:x:data:x");
  x["type"] = "form";
  XmlNode title("title");
  title.set_inner("Disconnect a user from selected IRC servers");
  x.add_child(std::move(title));
  XmlNode instructions("instructions");
  instructions.set_inner("Choose one or more servers to disconnect this JID from");
  x.add_child(std::move(instructions));
  XmlNode jids_field("field");
  jids_field["var"] = "irc-servers";
  jids_field["type"] = "list-multi";
  jids_field["label"] = "The servers to disconnect from";
  XmlNode required("required");
  jids_field.add_child(std::move(required));
  Bridge* bridge = biboumi_component->find_user_bridge(jid_to_disconnect);

  if (!bridge || bridge->get_irc_clients().empty())
    {
      XmlNode note("note");
      note["type"] = "info";
      note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server.");
      command_node.add_child(std::move(note));
      session.terminate();
      return ;
    }

  for (const auto& pair: bridge->get_irc_clients())
    {
      XmlNode option("option");
      option["label"] = pair.first;
      XmlNode value("value");
      value.set_inner(pair.first);
      option.add_child(std::move(value));
      jids_field.add_child(std::move(option));
    }
  x.add_child(std::move(jids_field));

  XmlNode message_field("field");
  message_field["var"] = "quit-message";
  message_field["type"] = "text-single";
  message_field["label"] = "Quit message";
  XmlNode message_value("value");
  message_value.set_inner("Killed by admin");
  message_field.add_child(std::move(message_value));
  x.add_child(std::move(message_field));

  command_node.add_child(std::move(x));
}

void DisconnectUserFromServerStep3(XmppComponent* xmpp_component, AdhocSession& session, XmlNode& command_node)
{
  const auto it = session.vars.find("jid");
  if (it == session.vars.end())
    return ;
  const auto jid_to_disconnect = it->second;

  std::vector<std::string> servers;
  std::string quit_message;

  if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
    {
      for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
        {
          if (field->get_tag("var") == "irc-servers")
            {
              for (const XmlNode* value: field->get_children("value", "jabber:x:data"))
                servers.push_back(value->get_inner());
            }
          else if (field->get_tag("var") == "quit-message")
            if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
              quit_message = value->get_inner();
        }
    }

  auto biboumi_component = static_cast<BiboumiComponent*>(xmpp_component);
  Bridge* bridge = biboumi_component->find_user_bridge(jid_to_disconnect);
  auto& clients = bridge->get_irc_clients();

  std::size_t number = 0;

  for (const auto& hostname: servers)
    {
      auto it = clients.find(hostname);
      if (it != clients.end())
        {
          it->second->on_error({"ERROR", {quit_message}});
          clients.erase(it);
          number++;
        }
    }
  command_node.delete_all_children();
  XmlNode note("note");
  note["type"] = "info";
  std::string msg = jid_to_disconnect + " was disconnected from " + std::to_string(number) + " IRC server";
  if (number > 1)
    msg += "s";
  msg += ".";
  note.set_inner(msg);
  command_node.add_child(std::move(note));
}

M src/xmpp/biboumi_adhoc_commands.hpp => src/xmpp/biboumi_adhoc_commands.hpp +4 -0
@@ 13,4 13,8 @@ void DisconnectUserStep2(XmppComponent*, AdhocSession& session, XmlNode& command
void ConfigureIrcServerStep1(XmppComponent*, AdhocSession& session, XmlNode& command_node);
void ConfigureIrcServerStep2(XmppComponent*, AdhocSession& session, XmlNode& command_node);

void DisconnectUserFromServerStep1(XmppComponent*, AdhocSession& session, XmlNode& command_node);
void DisconnectUserFromServerStep2(XmppComponent*, AdhocSession& session, XmlNode& command_node);
void DisconnectUserFromServerStep3(XmppComponent*, AdhocSession& session, XmlNode& command_node);

#endif /* BIBOUMI_ADHOC_COMMANDS_HPP_INCLUDED */

M src/xmpp/biboumi_component.cpp => src/xmpp/biboumi_component.cpp +2 -1
@@ 55,7 55,8 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::st
  this->adhoc_commands_handler.get_commands() = {
    {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)},
    {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)},
    {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect a user from the gateway", true)},
    {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true)},
    {"disconnect-from-irc-servers", AdhocCommand({&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false)},
    {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)}
  };