~singpolyma/biboumi

50cadf3dac0d56ef8181d1800cc30f8dcb749141 — louiz’ 7 years ago 7ca95a0
Implement our own database ORM, and update the whole code to use it

Entirely replace LiteSQL

fix #3271
D database/database.xml => database/database.xml +0 -70
@@ 1,70 0,0 @@
<?xml version="1.0"?>
<!DOCTYPE database SYSTEM "litesql.dtd">

<database name="BibouDB" namespace="db">
    <object name="GlobalOptions">
        <field name="owner" type="string" length="3071"/>

        <field name="maxHistoryLength" type="integer" default="20"/>
        <field name="recordHistory" type="boolean" default="true"/>
        <index unique="true">
            <indexfield name="owner"/>
        </index>
    </object>

    <object name="IrcServerOptions">
        <field name="owner" type="string" length="3071"/>
        <field name="server" type="string" length="3071"/>

        <field name="pass" type="string" length="1024" default=""/>
        <field name="afterConnectionCommand" type="string" length="510" default=""/>
        <field name="tlsPorts" type="string" length="4096" default="6697;6670" />
        <field name="ports" type="string" length="4096" default="6667" />
        <field name="username" type="string" length="1024" default=""/>
        <field name="realname" type="string" length="1024" default=""/>
        <field name="verifyCert" type="boolean" default="true"/>
        <field name="trustedFingerprint" type="string"/>

        <field name="encodingOut" type="string" default="ISO-8859-1"/>
        <field name="encodingIn"  type="string" default="ISO-8859-1"/>

        <field name="maxHistoryLength" type="integer" default="20"/>
        <index unique="true">
            <indexfield name="owner"/>
            <indexfield name="server"/>
        </index>
    </object>

    <object name="IrcChannelOptions">
        <field name="owner" type="string" length="3071"/>
        <field name="server" type="string" length="3071"/>
        <field name="channel" type="string" length="1024"/>

        <field name="encodingOut" type="string"/>
        <field name="encodingIn"  type="string"/>

        <field name="maxHistoryLength" type="integer" default="20"/>

        <field name="persistent" type="boolean" default="false"/>

        <index unique="true">
            <indexfield name="owner"/>
            <indexfield name="server"/>
            <indexfield name="channel"/>
        </index>
    </object>

    <object name="MucLogLine">
        <field name="uuid" type="string" length="36" />
        <!-- The bare JID of the user for which we stored the line. It's
        the JID associated with the Bridge -->
        <field name="owner" type="string" length="4096" />
        <!-- The room IID -->
        <field name="ircChanName" type="string" length="4096" />
        <field name="ircServerName" type="string" length="4096" />

        <field name="date" type="datetime" />
        <field name="body" type="string" length="65536"/>
        <field name="nick" type="string" length="4096" />
    </object>
</database>

M src/bridge/bridge.cpp => src/bridge/bridge.cpp +9 -9
@@ 23,7 23,7 @@ static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
#ifdef USE_DATABASE
  const auto jid = bridge.get_bare_jid();
  auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local());
  return options.encodingIn.value();
  return options.col<Database::EncodingIn>();
#else
  (void)bridge;
  (void)iid;


@@ 32,13 32,13 @@ static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
}

Bridge::Bridge(std::string user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller>& poller):
  user_jid(std::move(user_jid)),
 user_jid(std::move(user_jid)),
  xmpp(xmpp),
  poller(poller)
{
#ifdef USE_DATABASE
  const auto options = Database::get_global_options(this->user_jid);
  this->set_record_history(options.recordHistory.value());
  this->set_record_history(options.col<Database::RecordHistory>());
#endif
}



@@ 258,7 258,7 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
#ifdef USE_DATABASE
      const auto xmpp_body = this->make_xmpp_body(line);
      if (this->record_history)
        uuid = Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(),
        uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
                                    std::get<0>(xmpp_body), irc->get_own_nick());
#endif
      for (const auto& resource: this->resources_in_chan[iid.to_tuple()])


@@ 436,7 436,7 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
#ifdef USE_DATABASE
      const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid,
                                                                                  iid.get_server(), iid.get_local());
      persistent = coptions.persistent.value();
      persistent = coptions.col<Database::Persistent>();
#endif
      if (channel->joined && !channel->parting && !persistent)
        {


@@ 837,7 837,7 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st
#ifdef USE_DATABASE
      const auto xmpp_body = this->make_xmpp_body(body, encoding);
      if (!nick.empty() && this->record_history)
        Database::store_muc_message(this->get_bare_jid(), iid, std::chrono::system_clock::now(),
        Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
                                    std::get<0>(xmpp_body), nick);
#endif
      for (const auto& resource: this->resources_in_chan[iid.to_tuple()])


@@ 994,12 994,12 @@ void Bridge::send_room_history(const std::string& hostname, std::string chan_nam
{
#ifdef USE_DATABASE
  const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name);
  const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.maxHistoryLength.value());
  const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, coptions.col<Database::MaxHistoryLength>());
  chan_name.append(utils::empty_if_fixed_server("%" + hostname));
  for (const auto& line: lines)
    {
      const auto seconds = line.date.value().timeStamp();
      this->xmpp.send_history_message(chan_name, line.nick.value(), line.body.value(),
      const auto seconds = line.col<Database::Date>();
      this->xmpp.send_history_message(chan_name, line.col<Database::Nick>(), line.col<Database::Body>(),
                                      this->user_jid + "/" + resource, seconds);
    }
#else

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

#include <cstdint>

template <typename T>
struct Column
{
    using real_type = T;
    T value;
};

struct Id: Column<std::size_t> { static constexpr auto name = "id_";
                                 static constexpr auto options = "PRIMARY KEY AUTOINCREMENT"; };

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

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

#include <string>

#include <sqlite3.h>

struct CountQuery: public Query
{
    CountQuery(std::string name):
        Query("SELECT count(*) FROM ")
    {
      this->body += std::move(name);
    }

    std::size_t execute(sqlite3* db)
    {
      auto statement = this->prepare(db);
      std::size_t res = 0;
      if (sqlite3_step(statement) == SQLITE_ROW)
        res = sqlite3_column_int64(statement, 0);
      else
        {
          log_error("Count request didn’t return a result");
          return 0;
        }
      if (sqlite3_step(statement) != SQLITE_DONE)
        log_warning("Count request returned more than one result.");

      log_debug("Returning count: ", res);
      return res;
    }
};

M src/database/database.cpp => src/database/database.cpp +97 -107
@@ 2,175 2,166 @@
#ifdef USE_DATABASE

#include <database/database.hpp>
#include <logger/logger.hpp>
#include <irc/iid.hpp>
#include <uuid/uuid.h>
#include <utils/get_first_non_empty.hpp>
#include <utils/time.hpp>

using namespace std::string_literals;
#include <sqlite3.h>

std::unique_ptr<db::BibouDB> Database::db;
sqlite3* Database::db;
Database::MucLogLineTable Database::muc_log_lines("MucLogLine_");
Database::GlobalOptionsTable Database::global_options("GlobalOptions_");
Database::IrcServerOptionsTable Database::irc_server_options("IrcServerOptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("IrcChannelOptions_");

void Database::open(const std::string& filename, const std::string& db_type)
void Database::open(const std::string& filename)
{
  try
    {
      auto new_db = std::make_unique<db::BibouDB>(db_type,
                                             "database="s + filename);
      if (new_db->needsUpgrade())
        new_db->upgrade();
      Database::db = std::move(new_db);
    } catch (const litesql::DatabaseError& e) {
      log_error("Failed to open database ", filename, ". ", e.what());
      throw;
    }
  auto res = sqlite3_open_v2(filename.data(), &Database::db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
  log_debug("open: ", res);
  Database::muc_log_lines.create(Database::db);
  Database::global_options.create(Database::db);
  Database::irc_server_options.create(Database::db);
  Database::irc_channel_options.create(Database::db);
}

void Database::set_verbose(const bool val)
{
  Database::db->verbose = val;
}

db::GlobalOptions Database::get_global_options(const std::string& owner)
Database::GlobalOptions Database::get_global_options(const std::string& owner)
{
  try {
    auto options = litesql::select<db::GlobalOptions>(*Database::db,
                                                      db::GlobalOptions::Owner == owner).one();
    return options;
  } catch (const litesql::NotFound& e) {
    db::GlobalOptions options(*Database::db);
    options.owner = owner;
    return options;
  }
  auto request = Database::global_options.select().where() << Owner{} << "=" << owner;

  Database::GlobalOptions options{Database::global_options.get_name()};
  auto result = request.execute(Database::db);
  if (result.size() == 1)
    options = result.front();
  else
    options.col<Owner>() = owner;
  return options;
}

db::IrcServerOptions Database::get_irc_server_options(const std::string& owner,
                                                      const std::string& server)
Database::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server)
{
  try {
    auto options = litesql::select<db::IrcServerOptions>(*Database::db,
                             db::IrcServerOptions::Owner == owner &&
                             db::IrcServerOptions::Server == server).one();
    return options;
  } catch (const litesql::NotFound& e) {
    db::IrcServerOptions options(*Database::db);
    options.owner = owner;
    options.server = server;
    // options.update();
    return options;
  }
  auto request = Database::irc_server_options.select().where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;

  Database::IrcServerOptions options{Database::irc_server_options.get_name()};
  auto result = request.execute(Database::db);
  if (result.size() == 1)
    options = result.front();
  else
    {
      options.col<Owner>() = owner;
      options.col<Server>() = server;
    }
  return options;
}

db::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner,
                                                        const std::string& server,
                                                        const std::string& channel)
Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel)
{
  try {
    auto options = litesql::select<db::IrcChannelOptions>(*Database::db,
                                                         db::IrcChannelOptions::Owner == owner &&
                                                         db::IrcChannelOptions::Server == server &&
                                                         db::IrcChannelOptions::Channel == channel).one();
    return options;
  } catch (const litesql::NotFound& e) {
    db::IrcChannelOptions options(*Database::db);
    options.owner = owner;
    options.server = server;
    options.channel = channel;
    return options;
  }
  auto request = Database::irc_channel_options.select().where() << Owner{} << "=" << owner <<\
                                                        " and " << Server{} << "=" << server <<\
                                                        " and " << Channel{} << "=" << channel;
  Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
  auto result = request.execute(Database::db);
  if (result.size() == 1)
    options = result.front();
  else
    {
      options.col<Owner>() = owner;
      options.col<Server>() = server;
      options.col<Channel>() = channel;
    }
  return options;
}

db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner,
                                                                            const std::string& server,
                                                                            const std::string& channel)
Database::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner, const std::string& server,
                                                                                  const std::string& channel)
{
  auto coptions = Database::get_irc_channel_options(owner, server, channel);
  auto soptions = Database::get_irc_server_options(owner, server);

  coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(),
                                            soptions.encodingIn.value());
  coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(),
                                             soptions.encodingOut.value());
  coptions.col<EncodingIn>() = get_first_non_empty(coptions.col<EncodingIn>(),
                                                   soptions.col<EncodingIn>());
  coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(),
                                                    soptions.col<EncodingOut>());

  coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(),
                                                  soptions.maxHistoryLength.value());
  coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(),
                                                         soptions.col<MaxHistoryLength>());

  return coptions;
}

db::IrcChannelOptions Database::get_irc_channel_options_with_server_and_global_default(const std::string& owner,
                                                                                       const std::string& server,
                                                                                       const std::string& channel)
Database::IrcChannelOptions Database::get_irc_channel_options_with_server_and_global_default(const std::string& owner, const std::string& server, const std::string& channel)
{
  auto coptions = Database::get_irc_channel_options(owner, server, channel);
  auto soptions = Database::get_irc_server_options(owner, server);
  auto goptions = Database::get_global_options(owner);

  coptions.encodingIn = get_first_non_empty(coptions.encodingIn.value(),
                                            soptions.encodingIn.value());
  coptions.encodingOut = get_first_non_empty(coptions.encodingOut.value(),
                                             soptions.encodingOut.value());
  coptions.col<EncodingIn>() = get_first_non_empty(coptions.col<EncodingIn>(),
                                                   soptions.col<EncodingIn>());

  coptions.maxHistoryLength = get_first_non_empty(coptions.maxHistoryLength.value(),
                                                  soptions.maxHistoryLength.value(),
                                                  goptions.maxHistoryLength.value());
  coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(),
                                                    soptions.col<EncodingOut>());

  coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(),
                                                         soptions.col<MaxHistoryLength>(),
                                                         goptions.col<MaxHistoryLength>());

  return coptions;
}

std::string Database::store_muc_message(const std::string& owner, const Iid& iid,
                                        Database::time_point date,
                                        const std::string& body,
                                        const std::string& nick)
std::string Database::store_muc_message(const std::string& owner, const std::string& chan_name,
                                        const std::string& server_name, Database::time_point date,
                                        const std::string& body, const std::string& nick)
{
  db::MucLogLine line(*Database::db);
  auto line = Database::muc_log_lines.row();

  auto uuid = Database::gen_uuid();

  line.uuid = uuid;
  line.owner = owner;
  line.ircChanName = iid.get_local();
  line.ircServerName = iid.get_server();
  line.date = std::chrono::duration_cast<std::chrono::seconds>(date.time_since_epoch()).count();
  line.body = body;
  line.nick = nick;
  line.col<Uuid>() = uuid;
  line.col<Owner>() = owner;
  line.col<IrcChanName>() = chan_name;
  line.col<IrcServerName>() = server_name;
  line.col<Date>() = std::chrono::duration_cast<std::chrono::seconds>(date.time_since_epoch()).count();
  line.col<Body>() = body;
  line.col<Nick>() = nick;

  line.update();
  line.save(Database::db);

  return uuid;
}

std::vector<db::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
                                                   int limit, const std::string& start, const std::string& end)
{
  auto request = litesql::select<db::MucLogLine>(*Database::db,
                                              db::MucLogLine::Owner == owner &&
                                              db::MucLogLine::IrcChanName == chan_name &&
                                              db::MucLogLine::IrcServerName == server);
  request.orderBy(db::MucLogLine::Id, false);
  auto request = Database::muc_log_lines.select().where() << Database::Owner{} << "=" << owner << \
                                            " and " << Database::IrcChanName{} << "=" << chan_name << \
                                            " and " << Database::IrcServerName{} << "=" << server;

  if (limit >= 0)
    request.limit(limit);
  if (!start.empty())
    {
      const auto start_time = utils::parse_datetime(start);
      if (start_time != -1)
        request.where(db::MucLogLine::Date >= start_time);
        request << " and " << Database::Date{} << ">=" << start_time;
    }
  if (!end.empty())
    {
      const auto end_time = utils::parse_datetime(end);
      if (end_time != -1)
        request.where(db::MucLogLine::Date <= end_time);
        request << " and " << Database::Date{} << "<=" << end_time;
    }
  const auto& res = request.all();
  return {res.crbegin(), res.crend()};

  request.order_by() << Id{} << " DESC ";

  if (limit >= 0)
    request.limit() << limit;

  auto result = request.execute(Database::db);

  return {result.crbegin(), result.crend()};
}

void Database::close()
{
  Database::db.reset(nullptr);
  sqlite3_close_v2(Database::db);
}

std::string Database::gen_uuid()


@@ 182,5 173,4 @@ std::string Database::gen_uuid()
  return uuid_str;
}


#endif
#endif
\ No newline at end of file

M src/database/database.hpp => src/database/database.hpp +115 -38
@@ 1,22 1,101 @@
#pragma once


#include <biboumi.h>
#ifdef USE_DATABASE

#include "biboudb.hpp"

#include <memory>
#include <database/table.hpp>
#include <database/column.hpp>
#include <database/count_query.hpp>

#include <litesql.hpp>
#include <chrono>
#include <string>

#include <memory>

class Iid;

class Database
{
public:
 public:
  using time_point = std::chrono::system_clock::time_point;

  struct Uuid: Column<std::string> { static constexpr auto name = "uuid_";
    static constexpr auto options = ""; };

  struct Owner: Column<std::string> { static constexpr auto name = "owner_";
    static constexpr auto options = ""; };

  struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_";
    static constexpr auto options = ""; };

  struct Channel: Column<std::string> { static constexpr auto name = "channel_";
    static constexpr auto options = ""; };

  struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_";
    static constexpr auto options = ""; };

  struct Server: Column<std::string> { static constexpr auto name = "server_";
    static constexpr auto options = ""; };

  struct Date: Column<time_point::rep> { static constexpr auto name = "date_";
    static constexpr auto options = ""; };

  struct Body: Column<std::string> { static constexpr auto name = "body_";
    static constexpr auto options = ""; };

  struct Nick: Column<std::string> { static constexpr auto name = "nick_";
    static constexpr auto options = ""; };

  struct Pass: Column<std::string> { static constexpr auto name = "pass_";
    static constexpr auto options = ""; };

  struct Ports: Column<std::string> { static constexpr auto name = "tlsPorts_";
    static constexpr auto options = ""; };

  struct TlsPorts: Column<std::string> { static constexpr auto name = "ports_";
    static constexpr auto options = ""; };

  struct Username: Column<std::string> { static constexpr auto name = "username_";
    static constexpr auto options = ""; };

  struct Realname: Column<std::string> { static constexpr auto name = "realname_";
    static constexpr auto options = ""; };

  struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_";
    static constexpr auto options = ""; };

  struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_";
    static constexpr auto options = ""; };

  struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_";
    static constexpr auto options = ""; };

  struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_";
    static constexpr auto options = ""; };

  struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxHistoryLength_";
    static constexpr auto options = ""; };

  struct RecordHistory: Column<bool> { static constexpr auto name = "recordHistory_";
    static constexpr auto options = ""; };

  struct VerifyCert: Column<bool> { static constexpr auto name = "verifyCert_";
    static constexpr auto options = ""; };

  struct Persistent: Column<bool> { static constexpr auto name = "persistent_";
    static constexpr auto options = ""; };

  using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
  using MucLogLine = MucLogLineTable::RowType;

  using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory>;
  using GlobalOptions = GlobalOptionsTable::RowType;

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

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

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



@@ 25,42 104,40 @@ public:
  Database& operator=(const Database&) = delete;
  Database& operator=(Database&&) = delete;

  static void set_verbose(const bool val);

  template<typename PersistentType>
  static size_t count()
  {
    return litesql::select<PersistentType>(*Database::db).count();
  }
  /**
   * Return the object from the db. Create it beforehand (with all default
   * values) if it is not already present.
   */
  static db::GlobalOptions get_global_options(const std::string& owner);
  static db::IrcServerOptions get_irc_server_options(const std::string& owner,
  static GlobalOptions get_global_options(const std::string& owner);
  static IrcServerOptions get_irc_server_options(const std::string& owner,
                                                     const std::string& server);
  static db::IrcChannelOptions get_irc_channel_options(const std::string& owner,
                                                       const std::string& server,
                                                       const std::string& channel);
  static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
                                                                           const std::string& server,
                                                                           const std::string& channel);
  static db::IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
                                                                                      const std::string& server,
                                                                                      const std::string& channel);
  static std::vector<db::MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
                                                  int limit=-1, const std::string& start="", const std::string& end="");
  static std::string store_muc_message(const std::string& owner, const Iid& iid,
                                time_point date, const std::string& body, const std::string& nick);
  static IrcChannelOptions get_irc_channel_options(const std::string& owner,
                                                   const std::string& server,
                                                   const std::string& channel);
  static IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
                                                                       const std::string& server,
                                                                       const std::string& channel);
  static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
                                                                                  const std::string& server,
                                                                                  const std::string& channel);
  static std::vector<MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
                                              int limit=-1, const std::string& start="", const std::string& end="");
  static std::string store_muc_message(const std::string& owner, const std::string& chan_name, const std::string& server_name,
                                       time_point date, const std::string& body, const std::string& nick);

  static void close();
  static void open(const std::string& filename, const std::string& db_type="sqlite3");
  static void open(const std::string& filename);

  template <typename TableType>
  static std::size_t count(const TableType& table)
  {
    CountQuery query{table.get_name()};
    return query.execute(Database::db);
  }

  static MucLogLineTable muc_log_lines;
  static GlobalOptionsTable global_options;
  static IrcServerOptionsTable irc_server_options;
  static IrcChannelOptionsTable irc_channel_options;
  static sqlite3* db;

private:
 private:
  static std::string gen_uuid();
  static std::unique_ptr<db::BibouDB> db;
};
#endif /* USE_DATABASE */



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

#include <database/column.hpp>
#include <database/query.hpp>
#include <logger/logger.hpp>

#include <type_traits>
#include <vector>
#include <string>
#include <tuple>

#include <sqlite3.h>

template <int N, typename ColumnType, typename... T>
typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
actual_bind(sqlite3_stmt* statement, std::vector<std::string>& params, const std::tuple<T...>&)
{
  const auto value = params.front();
  params.erase(params.begin());
  if (sqlite3_bind_text(statement, N + 1, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) != SQLITE_OK)
    log_error("Failed to bind ", value, " to param ", N);
  else
    log_debug("Bound (not id) [", value, "] to ", N);
}

template <int N, typename ColumnType, typename... T>
typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
actual_bind(sqlite3_stmt* statement, std::vector<std::string>&, const std::tuple<T...>& columns)
{
  auto&& column = std::get<Id>(columns);
  if (column.value != 0)
    {
      if (sqlite3_bind_int64(statement, N + 1, column.value) != SQLITE_OK)
        log_error("Failed to bind ", column.value, " to id.");
    }
  else if (sqlite3_bind_null(statement, N + 1) != SQLITE_OK)
    log_error("Failed to bind NULL to param ", N);
  else
    log_debug("Bound NULL to ", N);
}

struct InsertQuery: public Query
{
  InsertQuery(const std::string& name):
      Query("INSERT OR REPLACE INTO ")
  {
    this->body += name;
  }

  template <typename... T>
  void execute(const std::tuple<T...>& columns, sqlite3* db)
  {
    auto statement = this->prepare(db);
    {
      this->bind_param<0>(columns, statement);
      if (sqlite3_step(statement) != SQLITE_DONE)
        log_error("Failed to execute query: ", sqlite3_errmsg(db));
    }
  }

  template <int N, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  bind_param(const std::tuple<T...>& columns, sqlite3_stmt* statement)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;

    actual_bind<N, ColumnType>(statement, this->params, columns);
    this->bind_param<N+1>(columns, statement);
  }

  template <int N, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  bind_param(const std::tuple<T...>&, sqlite3_stmt*)
  {}

  template <typename... T>
  void insert_values(const std::tuple<T...>& columns)
  {
    this->body += "VALUES (";
    this->insert_value<0>(columns);
    this->body += ")";
  }

  template <int N, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  insert_value(const std::tuple<T...>& columns)
  {
    this->body += "?";
    if (N != sizeof...(T) - 1)
      this->body += ",";
    this->body += " ";
    add_param(*this, std::get<N>(columns));
    this->insert_value<N+1>(columns);
  }
  template <int N, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  insert_value(const std::tuple<T...>&)
  { }

  template <typename... T>
  void insert_col_names(const std::tuple<T...>& columns)
  {
    this->body += " (";
    this->insert_col_name<0>(columns);
    this->body += ")\n";
  }

  template <int N, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  insert_col_name(const std::tuple<T...>& columns)
  {
    auto value = std::get<N>(columns);

    this->body += value.name;

    if (N < (sizeof...(T) - 1))
      this->body += ", ";

    this->insert_col_name<N+1>(columns);
  }
  template <int N, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  insert_col_name(const std::tuple<T...>&)
  {}


 private:
};

A src/database/query.cpp => src/database/query.cpp +11 -0
@@ 0,0 1,11 @@
#include <database/query.hpp>
#include <database/column.hpp>

template <>
void add_param<Id>(Query&, const Id&)
{}

void actual_add_param(Query& query, const std::string& val)
{
  query.params.push_back(val);
}

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

#include <logger/logger.hpp>

#include <vector>
#include <string>

#include <sqlite3.h>

struct Query
{
    std::string body;
    std::vector<std::string> params;

    Query(std::string str):
        body(std::move(str))
    {}

    sqlite3_stmt* prepare(sqlite3* db)
    {
      sqlite3_stmt* statement;
      log_debug(this->body);
      auto res = sqlite3_prepare(db, this->body.data(), static_cast<int>(this->body.size()) + 1,
                                 &statement, nullptr);
      if (res != SQLITE_OK)
        {
          log_error("Error preparing statement: ", sqlite3_errmsg(db));
          return nullptr;
        }
      return statement;
    }
};

template <typename ColumnType>
void add_param(Query& query, const ColumnType& column)
{
  actual_add_param(query, column.value);
}

template <typename T>
void actual_add_param(Query& query, const T& val)
{
  query.params.push_back(std::to_string(val));
}

void actual_add_param(Query& query, const std::string& val);

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

#include <database/insert_query.hpp>
#include <logger/logger.hpp>

#include <type_traits>

#include <sqlite3.h>

template <typename ColumnType, typename... T>
typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
update_id(std::tuple<T...>&, sqlite3*)
{}

template <typename ColumnType, typename... T>
typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
update_id(std::tuple<T...>& columns, sqlite3* db)
{
  auto&& column = std::get<ColumnType>(columns);
  log_debug("Found an autoincrement col.");
  auto res = sqlite3_last_insert_rowid(db);
  log_debug("Value is now: ", res);
  column.value = res;
}

template <std::size_t N, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>& columns, sqlite3* db)
{
  using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
  update_id<ColumnType>(columns, db);
  update_autoincrement_id<N+1>(columns, db);
}

template <std::size_t N, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>&, sqlite3*)
{}

template <typename... T>
struct Row
{
    Row(std::string name):
    table_name(std::move(name))
    {}

    template <typename Type>
    auto& col()
    {
      auto&& col = std::get<Type>(this->columns);
      return col.value;
    }

    template <typename Type>
    const auto& col() const
    {
      auto&& col = std::get<Type>(this->columns);
      return col.value;
    }

    void save(sqlite3* db)
    {
      InsertQuery query(this->table_name);
      query.insert_col_names(this->columns);
      query.insert_values(this->columns);
      log_debug(query.body);

      query.execute(this->columns, db);

      update_autoincrement_id<0>(this->columns, db);
    }

    std::tuple<T...> columns;
    std::string table_name;
};

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

#include <database/query.hpp>
#include <logger/logger.hpp>
#include <database/row.hpp>

#include <vector>
#include <string>

#include <sqlite3.h>

using namespace std::string_literals;

template <typename T>
typename std::enable_if<std::is_integral<T>::value, sqlite3_int64>::type
extract_row_value(sqlite3_stmt* statement, const int i)
{
  return sqlite3_column_int64(statement, i);
}

template <typename T>
typename std::enable_if<std::is_same<std::string, T>::value, std::string>::type
extract_row_value(sqlite3_stmt* statement, const int i)
{
  const auto size = sqlite3_column_bytes(statement, i);
  const unsigned char* str = sqlite3_column_text(statement, i);
  std::string result(reinterpret_cast<const char*>(str), size);
  return result;
}

template <std::size_t N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
extract_row_values(Row<T...>& row, sqlite3_stmt* statement)
{
  using ColumnType = typename std::remove_reference<decltype(std::get<N>(row.columns))>::type;

  auto&& column = std::get<N>(row.columns);
  column.value = static_cast<decltype(column.value)>(extract_row_value<typename ColumnType::real_type>(statement, N));

  extract_row_values<N+1>(row, statement);
}

template <std::size_t N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
extract_row_values(Row<T...>&, sqlite3_stmt*)
{}

template <typename... T>
struct SelectQuery: public Query
{
    SelectQuery(std::string table_name):
        Query("SELECT"),
        table_name(table_name)
    {
      this->insert_col_name<0>();
      this->body += " from " + this->table_name;
    }

    template <std::size_t N>
    typename std::enable_if<N < sizeof...(T), void>::type
    insert_col_name()
    {
      using ColumnsType = std::tuple<T...>;
      ColumnsType tuple{};
      auto value = std::get<N>(tuple);

      this->body += " "s + value.name;

      if (N < (sizeof...(T) - 1))
        this->body += ", ";

      this->insert_col_name<N+1>();
    }
    template <std::size_t N>
    typename std::enable_if<N == sizeof...(T), void>::type
    insert_col_name()
    {}

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

    SelectQuery& order_by()
    {
      this->body += " ORDER BY ";
      return *this;
    }

    SelectQuery& limit()
    {
      this->body += " LIMIT ";
      return *this;
    }

    auto execute(sqlite3* db)
    {
      auto statement = this->prepare(db);
      int i = 1;
      for (const std::string& param: this->params)
        {
          if (sqlite3_bind_text(statement, i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
            log_debug("Failed to bind ", param, " to param ", i);
          else
            log_debug("Bound ", param, " to ", i);

          i++;
        }
      std::vector<Row<T...>> rows;
      while (sqlite3_step(statement) == SQLITE_ROW)
        {
          Row<T...> row(this->table_name);
          extract_row_values(row, statement);
          rows.push_back(row);
        }
      return rows;
    }

    const std::string table_name;
};

template <typename T, typename... ColumnTypes>
typename std::enable_if<!std::is_integral<T>::value, SelectQuery<ColumnTypes...>&>::type
operator<<(SelectQuery<ColumnTypes...>& query, const T&)
{
  query.body += T::name;
  return query;
}

template <typename... ColumnTypes>
SelectQuery<ColumnTypes...>& operator<<(SelectQuery<ColumnTypes...>& query, const char* str)
{
  query.body += str;
  return query;
}

template <typename... ColumnTypes>
SelectQuery<ColumnTypes...>& operator<<(SelectQuery<ColumnTypes...>& query, const std::string& str)
{
  query.body += "?";
  actual_add_param(query, str);
  return query;
}

template <typename Integer, typename... ColumnTypes>
typename std::enable_if<std::is_integral<Integer>::value, SelectQuery<ColumnTypes...>&>::type
operator<<(SelectQuery<ColumnTypes...>& query, const Integer& i)
{
  query.body += "?";
  actual_add_param(query, i);
  return query;
}

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

#include <database/select_query.hpp>
#include <database/type_to_sql.hpp>
#include <logger/logger.hpp>
#include <database/row.hpp>

#include <algorithm>
#include <string>

template <typename... T>
class Table
{
  static_assert(sizeof...(T) > 0, "Table cannot be empty");
  using ColumnTypes = std::tuple<T...>;

 public:
  using RowType = Row<T...>;

  Table(std::string name):
      name(std::move(name))
  {}

  void create(sqlite3* db)
  {
    std::string res{"CREATE TABLE IF NOT EXISTS "};
    res += this->name;
    res += " (\n";
    this->add_column_create<0>(res);
    res += ")";

    log_debug(res);

    char* error;
    const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
    log_debug("result: ", +result);
    if (result != SQLITE_OK)
      {
        log_error("Error executing query: ", error);
        sqlite3_free(error);
      }
  }

  RowType row()
  {
    return {this->name};
  }

  SelectQuery<T...> select()
  {
    SelectQuery<T...> select(this->name);
    return select;
  }

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

 private:
  template <std::size_t N>
  typename std::enable_if<N < sizeof...(T), void>::type
  add_column_create(std::string& str)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
    using RealType = typename ColumnType::real_type;
    str += ColumnType::name;
    str += " ";
    str += TypeToSQLType<RealType>::type;
    str += " "s + ColumnType::options;
    if (N != sizeof...(T) - 1)
      str += ",";
    str += "\n";

    add_column_create<N+1>(str);
  }

  template <std::size_t N>
  typename std::enable_if<N == sizeof...(T), void>::type
  add_column_create(std::string&)
  { }

  const std::string name;
};

A src/database/type_to_sql.cpp => src/database/type_to_sql.cpp +7 -0
@@ 0,0 1,7 @@
#include <database/type_to_sql.hpp>

template <> const std::string TypeToSQLType<int>::type = "INTEGER";
template <> const std::string TypeToSQLType<std::size_t>::type = "INTEGER";
template <> const std::string TypeToSQLType<long>::type = "INTEGER";
template <> const std::string TypeToSQLType<bool>::type = "INTEGER";
template <> const std::string TypeToSQLType<std::string>::type = "TEXT";

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

#include <string>

template <typename T>
struct TypeToSQLType { static const std::string type; };

M src/irc/irc_client.cpp => src/irc/irc_client.cpp +12 -12
@@ 156,11 156,11 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
                                                  this->get_hostname());
  std::vector<std::string> ports = utils::split(options.ports, ';', false);
  std::vector<std::string> ports = utils::split(options.col<Database::Ports>(), ';', false);
  for (auto it = ports.rbegin(); it != ports.rend(); ++it)
    this->ports_to_try.emplace(*it, false);
# ifdef BOTAN_FOUND
  ports = utils::split(options.tlsPorts, ';', false);
  ports = utils::split(options.col<Database::TlsPorts>(), ';', false);
  for (auto it = ports.rbegin(); it != ports.rend(); ++it)
    this->ports_to_try.emplace(*it, true);
# endif // BOTAN_FOUND


@@ 204,7 204,7 @@ void IrcClient::start()
# ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
                                                  this->get_hostname());
  this->credential_manager.set_trusted_fingerprint(options.trustedFingerprint);
  this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
#endif
  this->connect(this->hostname, port, tls);


@@ 275,8 275,8 @@ void IrcClient::on_connected()
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
                                                  this->get_hostname());
  if (!options.pass.value().empty())
    this->send_pass_command(options.pass.value());
  if (!options.col<Database::Pass>().empty())
    this->send_pass_command(options.col<Database::Pass>());
#endif

  this->send_nick_command(this->current_nick);


@@ 284,10 284,10 @@ void IrcClient::on_connected()
#ifdef USE_DATABASE
  if (Config::get("realname_customization", "true") == "true")
    {
      if (!options.username.value().empty())
        this->username = options.username.value();
      if (!options.realname.value().empty())
        this->realname = options.realname.value();
      if (!options.col<Database::Username>().empty())
        this->username = options.col<Database::Username>();
      if (!options.col<Database::Realname>().empty())
        this->realname = options.col<Database::Realname>();
      this->send_user_command(username, realname);
    }
  else


@@ 894,8 894,8 @@ 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.afterConnectionCommand.value().empty())
    this->send_raw(options.afterConnectionCommand.value());
  if (!options.col<Database::AfterConnectionCommand>().empty())
    this->send_raw(options.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),


@@ 1260,7 1260,7 @@ bool IrcClient::abort_on_invalid_cert() const
{
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname);
  return options.verifyCert.value();
  return options.col<Database::VerifyCert>();
#endif
  return true;
}

M src/main.cpp => src/main.cpp +1 -4
@@ 12,9 12,6 @@

#include <atomic>
#include <csignal>
#ifdef USE_DATABASE
# include <litesql.hpp>
#endif

#include <identd/identd_server.hpp>



@@ 91,7 88,7 @@ int main(int ac, char** av)
#ifdef USE_DATABASE
  try {
    open_database();
  } catch (const litesql::DatabaseError&) {
  } catch (...) {
    return 1;
  }
#endif

M src/utils/reload.cpp => src/utils/reload.cpp +1 -1
@@ 26,7 26,7 @@ void reload_process()
#ifdef USE_DATABASE
  try {
      open_database();
    } catch (const litesql::DatabaseError&) {
    } catch (...) {
      log_warning("Re-using the previous database.");
    }
#endif

M src/xmpp/biboumi_adhoc_commands.cpp => src/xmpp/biboumi_adhoc_commands.cpp +43 -45
@@ 130,7 130,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman

  {
    XmlSubNode value(max_histo_length, "value");
    value.set_inner(std::to_string(options.maxHistoryLength.value()));
    value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
  }

  XmlSubNode record_history(x, "field");


@@ 142,7 142,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
  {
    XmlSubNode value(record_history, "value");
    value.set_name("value");
    if (options.recordHistory.value())
    if (options.col<Database::RecordHistory>())
      value.set_inner("true");
    else
      value.set_inner("false");


@@ 164,18 164,18 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, 

          if (field->get_tag("var") == "max_history_length" &&
              value && !value->get_inner().empty())
            options.maxHistoryLength = value->get_inner();
            options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data());
          else if (field->get_tag("var") == "record_history" &&
                   value && !value->get_inner().empty())
            {
              options.recordHistory = to_bool(value->get_inner());
              options.col<Database::RecordHistory>() = to_bool(value->get_inner());
              Bridge* bridge = biboumi_component.find_user_bridge(owner.bare());
              if (bridge)
                bridge->set_record_history(options.recordHistory.value());
                bridge->set_record_history(options.col<Database::RecordHistory>());
            }
        }

      options.update();
      options.save(Database::db);

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


@@ 211,8 211,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  ports["type"] = "text-multi";
  ports["label"] = "Ports";
  ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
  auto vals = utils::split(options.ports.value(), ';', false);
  for (const auto& val: vals)
  for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
    {
      XmlSubNode ports_value(ports, "value");
      ports_value.set_inner(val);


@@ 224,8 223,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  tls_ports["type"] = "text-multi";
  tls_ports["label"] = "TLS ports";
  tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
  vals = utils::split(options.tlsPorts.value(), ';', false);
  for (const auto& val: vals)
  for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
    {
      XmlSubNode tls_ports_value(tls_ports, "value");
      tls_ports_value.set_inner(val);


@@ 237,7 235,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  verify_cert["label"] = "Verify certificate";
  verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
  XmlSubNode verify_cert_value(verify_cert, "value");
  if (options.verifyCert.value())
  if (options.col<Database::VerifyCert>())
    verify_cert_value.set_inner("true");
  else
    verify_cert_value.set_inner("false");


@@ 246,10 244,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  fingerprint["var"] = "fingerprint";
  fingerprint["type"] = "text-single";
  fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
  if (!options.trustedFingerprint.value().empty())
  if (!options.col<Database::TrustedFingerprint>().empty())
    {
      XmlSubNode fingerprint_value(fingerprint, "value");
      fingerprint_value.set_inner(options.trustedFingerprint.value());
      fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>());
    }
#endif



@@ 258,10 256,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  pass["type"] = "text-private";
  pass["label"] = "Server password";
  pass["desc"] = "Will be used in a PASS command when connecting";
  if (!options.pass.value().empty())
  if (!options.col<Database::Pass>().empty())
    {
      XmlSubNode pass_value(pass, "value");
      pass_value.set_inner(options.pass.value());
      pass_value.set_inner(options.col<Database::Pass>());
    }

  XmlSubNode after_cnt_cmd(x, "field");


@@ 269,10 267,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  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.afterConnectionCommand.value().empty())
  if (!options.col<Database::AfterConnectionCommand>().empty())
    {
      XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
      after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value());
      after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
    }

  if (Config::get("realname_customization", "true") == "true")


@@ 281,20 279,20 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
      username["var"] = "username";
      username["type"] = "text-single";
      username["label"] = "Username";
      if (!options.username.value().empty())
      if (!options.col<Database::Username>().empty())
        {
          XmlSubNode username_value(username, "value");
          username_value.set_inner(options.username.value());
          username_value.set_inner(options.col<Database::Username>());
        }

      XmlSubNode realname(x, "field");
      realname["var"] = "realname";
      realname["type"] = "text-single";
      realname["label"] = "Realname";
      if (!options.realname.value().empty())
      if (!options.col<Database::Realname>().empty())
        {
          XmlSubNode realname_value(realname, "value");
          realname_value.set_inner(options.realname.value());
          realname_value.set_inner(options.col<Database::Realname>());
        }
    }



@@ 303,10 301,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  encoding_out["type"] = "text-single";
  encoding_out["desc"] = "The encoding used when sending messages to the IRC server.";
  encoding_out["label"] = "Out encoding";
  if (!options.encodingOut.value().empty())
  if (!options.col<Database::EncodingOut>().empty())
    {
      XmlSubNode encoding_out_value(encoding_out, "value");
      encoding_out_value.set_inner(options.encodingOut.value());
      encoding_out_value.set_inner(options.col<Database::EncodingOut>());
    }

  XmlSubNode encoding_in(x, "field");


@@ 314,10 312,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
  encoding_in["type"] = "text-single";
  encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
  encoding_in["label"] = "In encoding";
  if (!options.encodingIn.value().empty())
  if (!options.col<Database::EncodingIn>().empty())
    {
      XmlSubNode encoding_in_value(encoding_in, "value");
      encoding_in_value.set_inner(options.encodingIn.value());
      encoding_in_value.set_inner(options.col<Database::EncodingIn>());
    }
}



@@ 342,7 340,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
              std::string ports;
              for (const auto& val: values)
                ports += val->get_inner() + ";";
              options.ports = ports;
              options.col<Database::Ports>() = ports;
            }

#ifdef BOTAN_FOUND


@@ 351,31 349,31 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
              std::string ports;
              for (const auto& val: values)
                ports += val->get_inner() + ";";
              options.tlsPorts = ports;
              options.col<Database::TlsPorts>() = ports;
            }

          else if (field->get_tag("var") == "verify_cert" && value
                   && !value->get_inner().empty())
            {
              auto val = to_bool(value->get_inner());
              options.verifyCert = val;
              options.col<Database::VerifyCert>() = val;
            }

          else if (field->get_tag("var") == "fingerprint" && value &&
                   !value->get_inner().empty())
            {
              options.trustedFingerprint = value->get_inner();
              options.col<Database::TrustedFingerprint>() = value->get_inner();
            }

#endif // BOTAN_FOUND

          else if (field->get_tag("var") == "pass" &&
                   value && !value->get_inner().empty())
            options.pass = value->get_inner();
            options.col<Database::Pass>() = value->get_inner();

          else if (field->get_tag("var") == "after_connect_command" &&
                   value && !value->get_inner().empty())
            options.afterConnectionCommand = value->get_inner();
            options.col<Database::AfterConnectionCommand>() = value->get_inner();

          else if (field->get_tag("var") == "username" &&
                   value && !value->get_inner().empty())


@@ 383,24 381,24 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
              auto username = value->get_inner();
              // The username must not contain spaces
              std::replace(username.begin(), username.end(), ' ', '_');
              options.username = username;
              options.col<Database::Username>() = username;
            }

          else if (field->get_tag("var") == "realname" &&
                   value && !value->get_inner().empty())
            options.realname = value->get_inner();
            options.col<Database::Realname>() = value->get_inner();

          else if (field->get_tag("var") == "encoding_out" &&
                   value && !value->get_inner().empty())
            options.encodingOut = value->get_inner();
            options.col<Database::EncodingOut>() = value->get_inner();

          else if (field->get_tag("var") == "encoding_in" &&
                   value && !value->get_inner().empty())
            options.encodingIn = value->get_inner();
            options.col<Database::EncodingIn>() = value->get_inner();

        }

      options.update();
      options.save(Database::db);

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


@@ 441,10 439,10 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 
  encoding_out["type"] = "text-single";
  encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
  encoding_out["label"] = "Out encoding";
  if (!options.encodingOut.value().empty())
  if (!options.col<Database::EncodingOut>().empty())
    {
      XmlSubNode encoding_out_value(encoding_out, "value");
      encoding_out_value.set_inner(options.encodingOut.value());
      encoding_out_value.set_inner(options.col<Database::EncodingOut>());
    }

  XmlSubNode encoding_in(x, "field");


@@ 452,10 450,10 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 
  encoding_in["type"] = "text-single";
  encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
  encoding_in["label"] = "In encoding";
  if (!options.encodingIn.value().empty())
  if (!options.col<Database::EncodingIn>().empty())
    {
      XmlSubNode encoding_in_value(encoding_in, "value");
      encoding_in_value.set_inner(options.encodingIn.value());
      encoding_in_value.set_inner(options.col<Database::EncodingIn>());
    }

  XmlSubNode persistent(x, "field");


@@ 466,7 464,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, 
  {
    XmlSubNode value(persistent, "value");
    value.set_name("value");
    if (options.persistent.value())
    if (options.col<Database::Persistent>())
      value.set_inner("true");
    else
      value.set_inner("false");


@@ 510,18 508,18 @@ bool handle_irc_channel_configuration_form(const XmlNode& node, const Jid& reque

              if (field->get_tag("var") == "encoding_out" &&
                  value && !value->get_inner().empty())
                options.encodingOut = value->get_inner();
                options.col<Database::EncodingOut>() = value->get_inner();

              else if (field->get_tag("var") == "encoding_in" &&
                       value && !value->get_inner().empty())
                options.encodingIn = value->get_inner();
                options.col<Database::EncodingIn>() = value->get_inner();

              else if (field->get_tag("var") == "persistent" &&
                       value)
                options.persistent = to_bool(value->get_inner());
                options.col<Database::Persistent>() = to_bool(value->get_inner());
            }

          options.update();
          options.save(Database::db);
        }
      return true;
    }

M src/xmpp/biboumi_component.cpp => src/xmpp/biboumi_component.cpp +7 -9
@@ 18,8 18,6 @@

#include <biboumi.h>

#include <uuid/uuid.h>

#ifdef SYSTEMD_FOUND
# include <systemd/sd-daemon.h>
#endif


@@ 648,9 646,9 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
            limit = 100;
          }
        const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
        for (const db::MucLogLine& line: lines)
        for (const Database::MucLogLine& line: lines)
        {
          if (!line.nick.value().empty())
          if (!line.col<Database::Nick>().empty())
            this->send_archived_message(line, to.full(), from.full(), query_id);
        }
        this->send_iq_result_full_jid(id, from.full(), to.full());


@@ 659,7 657,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
  return false;
}

void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
                                             const std::string& queryid)
{
  Stanza message("message");


@@ 671,22 669,22 @@ void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, con
    result["xmlns"] = MAM_NS;
    if (!queryid.empty())
      result["queryid"] = queryid;
    result["id"] = log_line.uuid.value();
    result["id"] = log_line.col<Database::Uuid>();

    XmlSubNode forwarded(result, "forwarded");
    forwarded["xmlns"] = FORWARD_NS;

    XmlSubNode delay(forwarded, "delay");
    delay["xmlns"] = DELAY_NS;
    delay["stamp"] = utils::to_string(log_line.date.value().timeStamp());
    delay["stamp"] = utils::to_string(log_line.col<Database::Date>());

    XmlSubNode submessage(forwarded, "message");
    submessage["xmlns"] = CLIENT_NS;
    submessage["from"] = from + "/" + log_line.nick.value();
    submessage["from"] = from + "/" + log_line.col<Database::Nick>();
    submessage["type"] = "groupchat";

    XmlSubNode body(submessage, "body");
    body.set_inner(log_line.body.value());
    body.set_inner(log_line.col<Database::Body>());
  }
  this->send_stanza(message);
}

M src/xmpp/biboumi_component.hpp => src/xmpp/biboumi_component.hpp +2 -2
@@ 1,6 1,6 @@
#pragma once


#include <database/database.hpp>
#include <xmpp/xmpp_component.hpp>
#include <xmpp/jid.hpp>



@@ 96,7 96,7 @@ public:

#ifdef USE_DATABASE
  bool handle_mam_request(const Stanza& stanza);
  void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
  void send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
                             const std::string& queryid);
  bool handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id);
  bool handle_room_configuration_form(const XmlNode& query, const std::string& from, const Jid& to, const std::string& id);

M tests/database.cpp => tests/database.cpp +22 -24
@@ 8,24 8,22 @@ TEST_CASE("Database")
{
#ifdef USE_DATABASE
  Database::open(":memory:");
  Database::set_verbose(false);

  SECTION("Basic retrieve and update")
    {
      auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
      o.update();
      o.save(Database::db);
      auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
      auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");

      // b does not yet exist in the db, the object is created but not yet
      // inserted
      CHECK(1 == Database::count<db::IrcServerOptions>());
      CHECK(1 == Database::count(Database::irc_server_options));

      b.update();
      CHECK(2 == Database::count<db::IrcServerOptions>());
      b.save(Database::db);
      CHECK(2 == Database::count(Database::irc_server_options));

      CHECK(b.pass == "");
      CHECK(b.pass.value() == "");
      CHECK(b.col<Database::Pass>() == "");
    }

  SECTION("channel options")


@@ 33,16 31,16 @@ TEST_CASE("Database")
      Config::set("db_name", ":memory:");
      auto o = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");

      CHECK(o.encodingIn == "");
      o.encodingIn = "ISO-8859-1";
      o.update();
      CHECK(o.col<Database::EncodingIn>() == "");
      o.col<Database::EncodingIn>() = "ISO-8859-1";
      o.save(Database::db);
      auto b = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
      CHECK(o.encodingIn == "ISO-8859-1");
      CHECK(o.col<Database::EncodingIn>() == "ISO-8859-1");
    }

  SECTION("Channel options with server default")
    {
      const std::string owner{"zouzou@example.com"};
      const std::string owner{"CACA@example.com"};
      const std::string server{"irc.example.com"};
      const std::string chan1{"#foo"};



@@ 51,43 49,43 @@ TEST_CASE("Database")

      GIVEN("An option defined for the channel but not the server")
      {
        c.encodingIn = "channelEncoding";
        c.update();
        c.col<Database::EncodingIn>() = "channelEncoding";
        c.save(Database::db);
        WHEN("we fetch that option")
          {
            auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
            THEN("we get the channel option")
              CHECK(r.encodingIn == "channelEncoding");
              CHECK(r.col<Database::EncodingIn>() == "channelEncoding");
          }
      }
      GIVEN("An option defined for the server but not the channel")
        {
          s.encodingIn = "serverEncoding";
          s.update();
          s.col<Database::EncodingIn>() = "serverEncoding";
          s.save(Database::db);
        WHEN("we fetch that option")
          {
            auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
            THEN("we get the server option")
              CHECK(r.encodingIn == "serverEncoding");
              CHECK(r.col<Database::EncodingIn>() == "serverEncoding");
          }
        }
      GIVEN("An option defined for both the server and the channel")
        {
          s.encodingIn = "serverEncoding";
          s.update();
          c.encodingIn = "channelEncoding";
          c.update();
          s.col<Database::EncodingIn>() = "serverEncoding";
          s.save(Database::db);
          c.col<Database::EncodingIn>() = "channelEncoding";
          c.save(Database::db);
        WHEN("we fetch that option")
          {
            auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
            THEN("we get the channel option")
              CHECK(r.encodingIn == "channelEncoding");
              CHECK(r.col<Database::EncodingIn>() == "channelEncoding");
          }
        WHEN("we fetch that option, with no channel specified")
          {
            auto r = Database::get_irc_channel_options_with_server_default(owner, server, "");
            THEN("we get the server option")
              CHECK(r.encodingIn == "serverEncoding");
              CHECK(r.col<Database::EncodingIn>() == "serverEncoding");
          }
        }
    }