~singpolyma/biboumi

577984faf2befaa7f11a1e4a115dc8d80805fec7 — louiz’ 4 years ago ad7aa5b
Allow the execution of multiple commands after the IRC connection

fix #3275
M doc/biboumi.1.rst => doc/biboumi.1.rst +5 -4
@@ 645,10 645,11 @@ On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
      In encoding into UTF-8. If the conversion fails at some point, some
      characters will be replaced by the placeholders.
    * Out encoding: Currently ignored.
    * After-connection IRC command: A raw IRC command that will be sent to
      the server immediately after the connection has been successful. It
      can for example be used to identify yourself using NickServ, with a
      command like this: `PRIVMSG NickServ :identify PASSWORD`.
    * After-connection IRC commands: Raw IRC commands that will be sent
      one by one to the server immediately after the connection has been
      successful. It can for example be used to identify yourself using
      NickServ, with a command like this: `PRIVMSG NickServ :identify
      PASSWORD`.
    * Ports: The list of TCP ports to use when connecting to this IRC server.
      This list will be tried in sequence, until the connection succeeds for
      one of them. The connection made on these ports will not use TLS, the

M src/database/column.hpp => src/database/column.hpp +4 -0
@@ 13,6 13,10 @@ struct Column
    T value{};
};

struct ForeignKey: Column<std::size_t> {
    static constexpr auto name = "fk_";
};

struct Id: Column<std::size_t> {
    static constexpr std::size_t unset_value = static_cast<std::size_t>(-1);
    static constexpr auto name = "id_";

M src/database/database.cpp => src/database/database.cpp +29 -0
@@ 21,6 21,7 @@ Database::GlobalOptionsTable Database::global_options("globaloptions_");
Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_");
Database::RosterTable Database::roster("roster");
Database::AfterConnectionCommandsTable Database::after_connection_commands("after_connection_commands_");
std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{};

Database::GlobalPersistent::GlobalPersistent():


@@ 53,6 54,8 @@ void Database::open(const std::string& filename)
  Database::irc_channel_options.upgrade(*Database::db);
  Database::roster.create(*Database::db);
  Database::roster.upgrade(*Database::db);
  Database::after_connection_commands.create(*Database::db);
  Database::after_connection_commands.upgrade(*Database::db);
  create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name());
}



@@ 88,6 91,32 @@ Database::IrcServerOptions Database::get_irc_server_options(const std::string& o
  return options;
}

Database::AfterConnectionCommands Database::get_after_connection_commands(const IrcServerOptions& server_options)
{
  const auto id = server_options.col<Id>();
  if (id == Id::unset_value)
    return {};
  auto request = Database::after_connection_commands.select();
  request.where() << ForeignKey{} << "=" << id;
  return request.execute(*Database::db);
}

void Database::set_after_connection_commands(const Database::IrcServerOptions& server_options, Database::AfterConnectionCommands& commands)
{
  const auto id = server_options.col<Id>();
  if (id == Id::unset_value)
    return ;
  auto query = Database::after_connection_commands.del();
  query.where() << ForeignKey{} << "=" << id;
  query.execute(*Database::db);

  for (auto& command: commands)
    {
      command.col<ForeignKey>() = server_options.col<Id>();
      command.save(Database::db);
    }
}

Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel)
{
  auto request = Database::irc_channel_options.select();

M src/database/database.hpp => src/database/database.hpp +9 -1
@@ 92,7 92,7 @@ class Database
  using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>;
  using GlobalOptions = GlobalOptionsTable::RowType;

  using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address>;
  using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address>;
  using IrcServerOptions = IrcServerOptionsTable::RowType;

  using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>;


@@ 101,6 101,9 @@ class Database
  using RosterTable = Table<LocalJid, RemoteJid>;
  using RosterItem = RosterTable::RowType;

  using AfterConnectionCommandsTable = Table<Id, ForeignKey, AfterConnectionCommand>;
  using AfterConnectionCommands = std::vector<AfterConnectionCommandsTable::RowType>;

  Database() = default;
  ~Database() = default;



@@ 121,6 124,9 @@ class Database
  static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
                                                                                  const std::string& server,
                                                                                  const std::string& channel);
  static AfterConnectionCommands get_after_connection_commands(const IrcServerOptions& server_options);
  static void set_after_connection_commands(const IrcServerOptions& server_options, AfterConnectionCommands& commands);

  /**
   * Get all the lines between (optional) start and end dates, with a (optional) limit.
   * If after_id is set, only the records after it will be returned.


@@ 158,6 164,8 @@ class Database
  static IrcServerOptionsTable irc_server_options;
  static IrcChannelOptionsTable irc_channel_options;
  static RosterTable roster;
  static AfterConnectionCommandsTable after_connection_commands;

  static std::unique_ptr<DatabaseEngine> db;

  /**

A src/database/delete_query.hpp => src/database/delete_query.hpp +33 -0
@@ 0,0 1,33 @@
#pragma once

#include <database/query.hpp>
#include <database/engine.hpp>

class DeleteQuery: public Query
{
public:
  DeleteQuery(const std::string& name):
      Query("DELETE")
  {
    this->body += " from " + name;
  }

  DeleteQuery& where()
  {
    this->body += " WHERE ";
    return *this;
  };

  void execute(DatabaseEngine& db)
  {
    auto statement = db.prepare(this->body);
    if (!statement)
      return;
#ifdef DEBUG_SQL_QUERIES
    const auto timer = this->log_and_time();
#endif
    statement->bind(std::move(this->params));
    if (statement->step() != StepResult::Done)
      log_error("Failed to execute DELETE command");
  }
};

M src/database/table.hpp => src/database/table.hpp +7 -0
@@ 3,6 3,7 @@
#include <database/engine.hpp>

#include <database/select_query.hpp>
#include <database/delete_query.hpp>
#include <database/row.hpp>

#include <algorithm>


@@ 85,6 86,12 @@ class Table
    return select;
  }

  auto del()
  {
    DeleteQuery query(this->name);
    return query;
  }

  const std::string& get_name() const
  {
    return this->name;

M src/irc/irc_client.cpp => src/irc/irc_client.cpp +3 -2
@@ 889,8 889,9 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
                                                  this->get_hostname());
  if (!options.col<Database::AfterConnectionCommand>().empty())
    this->send_raw(options.col<Database::AfterConnectionCommand>());
  const auto commands = Database::get_after_connection_commands(options);
  for (const auto& command: commands)
    this->send_raw(command.col<Database::AfterConnectionCommand>());
#endif
  // Install a repeated events to regularly send a PING
  TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),

M src/xmpp/biboumi_adhoc_commands.cpp => src/xmpp/biboumi_adhoc_commands.cpp +20 -8
@@ 219,6 219,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
    server_domain = target.local;
  auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
                                                  server_domain);
  auto commands = Database::get_after_connection_commands(options);

  XmlSubNode x(command_node, "jabber:x:data:x");
  x["type"] = "form";


@@ 307,14 308,14 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com

  {
    XmlSubNode after_cnt_cmd(x, "field");
    after_cnt_cmd["var"] = "after_connect_command";
    after_cnt_cmd["type"] = "text-single";
    after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
    after_cnt_cmd["label"] = "After-connection IRC command";
    if (!options.col<Database::AfterConnectionCommand>().empty())
    after_cnt_cmd["var"] = "after_connect_commands";
    after_cnt_cmd["type"] = "text-multi";
    after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server.";
    after_cnt_cmd["label"] = "After-connection IRC commands";
    for (const auto& command: commands)
      {
        XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
        after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
        after_cnt_cmd_value.set_inner(command.col<Database::AfterConnectionCommand>());
      }
  }



@@ 384,6 385,8 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
        server_domain = target.local;
      auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
                                                      server_domain);
      auto commands = Database::get_after_connection_commands(options);

      for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
        {
          const XmlNode* value = field->get_child("value", "jabber:x:data");


@@ 427,8 430,16 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
          else if (field->get_tag("var") == "pass" && value)
            options.col<Database::Pass>() = value->get_inner();

          else if (field->get_tag("var") == "after_connect_command" && value)
            options.col<Database::AfterConnectionCommand>() = value->get_inner();
          else if (field->get_tag("var") == "after_connect_commands")
            {
              commands.clear();
              for (const auto& val: values)
                {
                  auto command = Database::after_connection_commands.row();
                  command.col<Database::AfterConnectionCommand>() = val->get_inner();
                  commands.push_back(std::move(command));
                }
            }

          else if (field->get_tag("var") == "username" && value)
            {


@@ 450,6 461,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
        }
      Database::invalidate_encoding_in_cache();
      options.save(Database::db);
      Database::set_after_connection_commands(options, commands);

      command_node.delete_all_children();
      XmlSubNode note(command_node, "note");

M tests/database.cpp => tests/database.cpp +43 -0
@@ 117,6 117,49 @@ TEST_CASE("Database")
        }
    }

  SECTION("Server options")
    {
      const std::string owner{"toto@example.com"};
      const std::string owner2{"toto2@example.com"};
      const std::string server{"irc.example.com"};

      auto soptions = Database::get_irc_server_options(owner, server);
      auto soptions2 = Database::get_irc_server_options(owner2, server);

      auto after_connection_commands =  Database::get_after_connection_commands(soptions);
      CHECK(after_connection_commands.empty());

      soptions.save(Database::db);
      soptions2.save(Database::db);
      auto com = Database::after_connection_commands.row();
      com.col<Database::AfterConnectionCommand>() = "first";
      after_connection_commands.push_back(com);
      com.col<Database::AfterConnectionCommand>() = "second";
      after_connection_commands.push_back(com);
      Database::set_after_connection_commands(soptions, after_connection_commands);

      after_connection_commands.clear();
      com.col<Database::AfterConnectionCommand>() = "first";
      after_connection_commands.push_back(com);
      com.col<Database::AfterConnectionCommand>() = "second";
      after_connection_commands.push_back(com);
      Database::set_after_connection_commands(soptions2, after_connection_commands);

      after_connection_commands =  Database::get_after_connection_commands(soptions);
      CHECK(after_connection_commands.size() == 2);
      after_connection_commands =  Database::get_after_connection_commands(soptions2);
      CHECK(after_connection_commands.size() == 2);

      after_connection_commands.clear();
      after_connection_commands.push_back(com);
      Database::set_after_connection_commands(soptions, after_connection_commands);

      after_connection_commands =  Database::get_after_connection_commands(soptions);
      CHECK(after_connection_commands.size() == 1);
      after_connection_commands =  Database::get_after_connection_commands(soptions2);
      CHECK(after_connection_commands.size() == 2);
    }

  Database::close();
}
#endif

M tests/end_to_end/__main__.py => tests/end_to_end/__main__.py +6 -5
@@ 2701,7 2701,7 @@ if __name__ == '__main__':
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",


@@ 2718,7 2718,7 @@ if __name__ == '__main__':
                                          "<field var='verify_cert'><value>1</value></field>"
                                          "<field var='fingerprint'><value>12:12:12</value></field>"
                                          "<field var='pass'><value>coucou</value></field>"
                                          "<field var='after_connect_command'><value>INVALID command</value></field>"
                                          "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>"
                                          "<field var='username'><value>username</value></field>"
                                          "<field var='realname'><value>realname</value></field>"
                                          "<field var='encoding_out'><value>UTF-8</value></field>"


@@ 2736,7 2736,8 @@ if __name__ == '__main__':
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']/dataform:value[text()='INVALID command']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']",


@@ 2757,7 2758,7 @@ if __name__ == '__main__':
                                          "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
                                          "<x xmlns='jabber:x:data' type='submit'>"
                                          "<field var='pass'><value></value></field>"
                                          "<field var='after_connect_command'><value></value></field>"
                                          "<field var='after_connect_commands'></field>"
                                          "<field var='username'><value></value></field>"
                                          "<field var='realname'><value></value></field>"
                                          "<field var='encoding_out'><value></value></field>"


@@ 2770,7 2771,7 @@ if __name__ == '__main__':
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']",
                                             "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_command']/dataform:value",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value",
                                             "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value",