diff options
Diffstat (limited to 'src')
37 files changed, 591 insertions, 350 deletions
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 54bee84..1c646fe 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -5,6 +5,7 @@ #include <utils/empty_if_fixed_server.hpp> #include <utils/encoding.hpp> #include <utils/tolower.hpp> +#include <utils/uuid.hpp> #include <logger/logger.hpp> #include <utils/revstr.hpp> #include <utils/split.hpp> @@ -63,7 +64,6 @@ void Bridge::shutdown(const std::string& exit_message) for (auto& pair: this->irc_clients) { pair.second->send_quit_command(exit_message); - pair.second->leave_dummy_channel(exit_message, {}); } } @@ -103,7 +103,7 @@ const std::string& Bridge::get_jid() const std::string Bridge::get_bare_jid() const { Jid jid(this->user_jid); - return jid.local + "@" + jid.domain; + return jid.bare(); } Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding) @@ -166,56 +166,36 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const } } -bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, - const std::string& resource, HistoryLimit history_limit) +bool Bridge::join_irc_channel(const Iid& iid, std::string nickname, + const std::string& password, + const std::string& resource, + HistoryLimit history_limit, + const bool force_join) { const auto& hostname = iid.get_server(); +#ifdef USE_DATABASE + auto soptions = Database::get_irc_server_options(this->get_bare_jid(), hostname); + if (!soptions.col<Database::Nick>().empty()) + nickname = soptions.col<Database::Nick>(); +#endif IrcClient* irc = this->make_irc_client(hostname, nickname); irc->history_limit = history_limit; this->add_resource_to_server(hostname, resource); auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource); if (!res_in_chan) this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource); - if (iid.get_local().empty()) - { // Join the dummy channel - if (irc->is_welcomed()) - { - if (res_in_chan) - return false; - // Immediately simulate a message coming from the IRC server saying that we - // joined the channel - if (irc->get_dummy_channel().joined) - { - this->generate_channel_join_for_resource(iid, resource); - } - else - { - const IrcMessage join_message(irc->get_nick(), "JOIN", {""}); - irc->on_channel_join(join_message); - const IrcMessage end_join_message(std::string(iid.get_server()), "366", - {irc->get_nick(), - "", "End of NAMES list"}); - irc->on_channel_completely_joined(end_join_message); - } - } - else - { - irc->get_dummy_channel().joining = true; - irc->start(); - } - return true; - } if (irc->is_channel_joined(iid.get_local()) == false) { irc->send_join_command(iid.get_local(), password); return true; - } else if (!res_in_chan) { + } else if (!res_in_chan || force_join) { + // See https://github.com/xsf/xeps/pull/499 for the force_join argument this->generate_channel_join_for_resource(iid, resource); } return false; } -void Bridge::send_channel_message(const Iid& iid, const std::string& body) +void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id) { if (iid.get_server().empty()) { @@ -240,6 +220,7 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body) std::vector<std::string> lines = utils::split(body, '\n', true); if (lines.empty()) return ; + bool first = true; for (const std::string& line: lines) { if (line.substr(0, 5) == "/mode") @@ -261,9 +242,12 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body) 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 + if (!first || id.empty()) + id = utils::gen_uuid(); for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), - this->user_jid + "/" + resource, uuid); + this->user_jid + "/" + resource, uuid, id); + first = false; } } @@ -445,15 +429,11 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con #endif if (channel->joined && !channel->parting && !persistent) { - const auto& chan_name = iid.get_local(); - if (chan_name.empty()) - irc->leave_dummy_channel(status_message, resource); - else - irc->send_part_command(iid.get_local(), status_message); + irc->send_part_command(iid.get_local(), status_message); } else if (channel->joined) { - this->send_muc_leave(iid, channel->get_self()->nick, "", true, true, resource); + this->send_muc_leave(iid, *channel->get_self(), "", true, true, resource, irc); } if (persistent) this->remove_resource_from_chan(key, resource); @@ -464,9 +444,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con else { if (channel && channel->joined) - this->send_muc_leave(iid, channel->get_self()->nick, + this->send_muc_leave(iid, *channel->get_self(), "Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.", - true, true, resource); + true, true, resource, irc); this->remove_resource_from_chan(key, resource); } if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0) @@ -839,19 +819,19 @@ void Bridge::send_irc_version_request(const std::string& irc_hostname, const std void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc) { const auto encoding = in_encoding_for(*this, iid); + std::string uuid{}; if (muc) { #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.get_local(), iid.get_server(), std::chrono::system_clock::now(), - std::get<0>(xmpp_body), nick); + 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), nick); #endif for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) { this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding), - this->user_jid + "/" + resource, {}); - + this->user_jid + "/" + resource, uuid, utils::gen_uuid()); } } else @@ -881,19 +861,24 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick, this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text); } -void Bridge::send_muc_leave(const Iid& iid, const std::string& nick, +void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user, const std::string& message, const bool self, const bool user_requested, - const std::string& resource) + const std::string& resource, + const IrcClient* client) { + std::string affiliation; + std::string role; + std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user.get_most_significant_mode(client->get_sorted_user_modes())); + if (!resource.empty()) - this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message), - this->user_jid + "/" + resource, self, user_requested); + this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message), + this->user_jid + "/" + resource, self, user_requested, affiliation, role); else { for (const auto &res: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message), - this->user_jid + "/" + res, self, user_requested); + this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message), + this->user_jid + "/" + res, self, user_requested, affiliation, role); if (self) { // Copy the resources currently in that channel @@ -1020,7 +1005,7 @@ void Bridge::send_room_history(const std::string& hostname, std::string chan_nam auto limit = coptions.col<Database::MaxHistoryLength>(); if (history_limit.stanzas >= 0 && history_limit.stanzas < limit) limit = history_limit.stanzas; - const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since); + const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since, {}, Id::unset_value, Database::Paging::last); chan_name.append(utils::empty_if_fixed_server("%" + hostname)); for (const auto& line: lines) { @@ -1254,9 +1239,6 @@ std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc res++; } - IrcClient* irc = this->find_irc_client(irc_hostname); - if (irc && (irc->get_dummy_channel().joined || irc->get_dummy_channel().joining)) - res++; return res; } diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index c2f0233..8e7d9d7 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -75,9 +75,13 @@ public: * Try to join an irc_channel, does nothing and return true if the channel * was already joined. */ - bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource, HistoryLimit history_limit); + bool join_irc_channel(const Iid& iid, std::string nickname, + const std::string& password, + const std::string& resource, + HistoryLimit history_limit, + const bool force_join); - void send_channel_message(const Iid& iid, const std::string& body); + void send_channel_message(const Iid& iid, const std::string& body, std::string id); void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG"); void send_raw_message(const std::string& hostname, const std::string& body); void leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource); @@ -170,10 +174,11 @@ public: /** * Send an unavailable presence from this participant */ - void send_muc_leave(const Iid& iid, const std::string& nick, + void send_muc_leave(const Iid& iid, const IrcUser& nick, const std::string& message, const bool self, const bool user_requested, - const std::string& resource=""); + const std::string& resource, + const IrcClient* client); /** * Send presences to indicate that an user old_nick (ourself if self == * true) changed his nick to new_nick. The user_mode is needed because diff --git a/src/config/config.cpp b/src/config/config.cpp index 412b170..2f64b9e 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,10 +1,12 @@ #include <config/config.hpp> #include <utils/tolower.hpp> +#include <utils/split.hpp> -#include <iostream> -#include <cstring> - +#include <algorithm> #include <cstdlib> +#include <cstring> +#include <iostream> +#include <vector> using namespace std::string_literals; @@ -40,6 +42,15 @@ int Config::get_int(const std::string& option, const int& def) return def; } +bool Config::is_in_list(const std::string& option, const std::string& value) +{ + std::string res = Config::get(option, ""); + if (res.empty()) + return false; + std::vector<std::string> list = utils::split(res, ':'); + return std::find(list.cbegin(), list.cend(), value) != list.cend(); +} + void Config::set(const std::string& option, const std::string& value, bool save) { Config::values[option] = value; diff --git a/src/config/config.hpp b/src/config/config.hpp index c5ef15d..9c28e8c 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -46,6 +46,11 @@ public: static int get_int(const std::string&, const int&); static bool get_bool(const std::string&, const bool); /** + * Returns true if value is present in a colon-separated list, otherwise + * false. + */ + static bool is_in_list(const std::string& option, const std::string& value); + /** * Set a value for the given option. And write all the config * in the file from which it was read if save is true. */ diff --git a/src/database/column.hpp b/src/database/column.hpp index 1f16bcf..50c9c14 100644 --- a/src/database/column.hpp +++ b/src/database/column.hpp @@ -13,10 +13,14 @@ 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_"; static constexpr auto options = "PRIMARY KEY"; - Id(): Column<std::size_t>(-1) {} + Id(): Column<std::size_t>(unset_value) {} }; diff --git a/src/database/database.cpp b/src/database/database.cpp index 3622963..3b3e890 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -1,10 +1,12 @@ #include "biboumi.h" #ifdef USE_DATABASE +#include <database/select_query.hpp> +#include <database/save.hpp> #include <database/database.hpp> -#include <uuid/uuid.h> #include <utils/get_first_non_empty.hpp> #include <utils/time.hpp> +#include <utils/uuid.hpp> #include <config/config.hpp> #include <database/sqlite3_engine.hpp> @@ -21,6 +23,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,57 +56,80 @@ 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()); } Database::GlobalOptions Database::get_global_options(const std::string& owner) { - auto request = Database::global_options.select(); + auto request = select(Database::global_options); request.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 result.front(); + Database::GlobalOptions options{Database::global_options.get_name()}; + options.col<Owner>() = owner; return options; } Database::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server) { - auto request = Database::irc_server_options.select(); + auto request = select(Database::irc_server_options); request.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 + return result.front(); + Database::IrcServerOptions options{Database::irc_server_options.get_name()}; + options.col<Owner>() = owner; + options.col<Server>() = server; + 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 = select(Database::after_connection_commands); + 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 ; + + Transaction transaction; + auto query = Database::after_connection_commands.del(); + query.where() << ForeignKey{} << "=" << id; + query.execute(*Database::db); + + for (auto& command: commands) { - options.col<Owner>() = owner; - options.col<Server>() = server; + command.col<ForeignKey>() = server_options.col<Id>(); + save(command, *Database::db); } - return options; } 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(); + auto request = select(Database::irc_channel_options); request.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 result.front(); + Database::IrcChannelOptions options{Database::irc_channel_options.get_name()}; + options.col<Owner>() = owner; + options.col<Server>() = server; + options.col<Channel>() = channel; return options; } @@ -159,15 +185,18 @@ std::string Database::store_muc_message(const std::string& owner, const std::str line.col<Body>() = body; line.col<Nick>() = nick; - line.save(Database::db); + save(line, *Database::db); return uuid; } 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) + int limit, const std::string& start, const std::string& end, const Id::real_type reference_record_id, Database::Paging paging) { - auto request = Database::muc_log_lines.select(); + if (limit == 0) + return {}; + + auto request = select(Database::muc_log_lines); request.where() << Database::Owner{} << "=" << owner << \ " and " << Database::IrcChanName{} << "=" << chan_name << \ " and " << Database::IrcServerName{} << "=" << server; @@ -184,15 +213,59 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne if (end_time != -1) request << " and " << Database::Date{} << "<=" << end_time; } + if (reference_record_id != Id::unset_value) + { + request << " and " << Id{}; + if (paging == Database::Paging::first) + request << ">"; + else + request << "<"; + request << reference_record_id; + } - request.order_by() << Id{} << " DESC "; + if (paging == Database::Paging::first) + request.order_by() << Id{} << " ASC "; + else + request.order_by() << Id{} << " DESC "; if (limit >= 0) request.limit() << limit; auto result = request.execute(*Database::db); - return {result.crbegin(), result.crend()}; + if (paging == Database::Paging::first) + return result; + else + return {result.crbegin(), result.crend()}; +} + +Database::MucLogLine Database::get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server, + const std::string& uuid, const std::string& start, const std::string& end) +{ + auto request = select(Database::muc_log_lines); + request.where() << Database::Owner{} << "=" << owner << \ + " and " << Database::IrcChanName{} << "=" << chan_name << \ + " and " << Database::IrcServerName{} << "=" << server << \ + " and " << Database::Uuid{} << "=" << uuid; + + if (!start.empty()) + { + const auto start_time = utils::parse_datetime(start); + if (start_time != -1) + request << " and " << Database::Date{} << ">=" << start_time; + } + if (!end.empty()) + { + const auto end_time = utils::parse_datetime(end); + if (end_time != -1) + request << " and " << Database::Date{} << "<=" << end_time; + } + + auto result = request.execute(*Database::db); + + if (result.empty()) + throw Database::RecordNotFound{}; + return result.front(); } void Database::add_roster_item(const std::string& local, const std::string& remote) @@ -202,7 +275,7 @@ void Database::add_roster_item(const std::string& local, const std::string& remo roster_item.col<Database::LocalJid>() = local; roster_item.col<Database::RemoteJid>() = remote; - roster_item.save(Database::db); + save(roster_item, *Database::db); } void Database::delete_roster_item(const std::string& local, const std::string& remote) @@ -216,7 +289,7 @@ void Database::delete_roster_item(const std::string& local, const std::string& r bool Database::has_roster_item(const std::string& local, const std::string& remote) { - auto query = Database::roster.select(); + auto query = select(Database::roster); query.where() << Database::LocalJid{} << "=" << local << \ " and " << Database::RemoteJid{} << "=" << remote; @@ -227,7 +300,7 @@ bool Database::has_roster_item(const std::string& local, const std::string& remo std::vector<Database::RosterItem> Database::get_contact_list(const std::string& local) { - auto query = Database::roster.select(); + auto query = select(Database::roster); query.where() << Database::LocalJid{} << "=" << local; return query.execute(*Database::db); @@ -235,7 +308,7 @@ std::vector<Database::RosterItem> Database::get_contact_list(const std::string& std::vector<Database::RosterItem> Database::get_full_roster() { - auto query = Database::roster.select(); + auto query = select(Database::roster); return query.execute(*Database::db); } @@ -247,11 +320,26 @@ void Database::close() std::string Database::gen_uuid() { - char uuid_str[37]; - uuid_t uuid; - uuid_generate(uuid); - uuid_unparse(uuid, uuid_str); - return uuid_str; + return utils::gen_uuid(); +} + +Transaction::Transaction() +{ + const auto result = Database::raw_exec("BEGIN"); + if (std::get<bool>(result) == false) + log_error("Failed to create SQL transaction: ", std::get<std::string>(result)); + else + this->success = true; + } +Transaction::~Transaction() +{ + if (this->success) + { + const auto result = Database::raw_exec("END"); + if (std::get<bool>(result) == false) + log_error("Failed to end SQL transaction: ", std::get<std::string>(result)); + } +} #endif diff --git a/src/database/database.hpp b/src/database/database.hpp index ec44543..d986ecc 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -22,6 +22,8 @@ class Database { public: using time_point = std::chrono::system_clock::time_point; + struct RecordNotFound: public std::exception {}; + enum class Paging { first, last }; struct Uuid: Column<std::string> { static constexpr auto name = "uuid_"; }; @@ -82,6 +84,7 @@ class Database struct RemoteJid: Column<std::string> { static constexpr auto name = "remote"; }; + struct Address: Column<std::string> { static constexpr auto name = "address_"; }; using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>; using MucLogLine = MucLogLineTable::RowType; @@ -89,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>; + using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick>; using IrcServerOptions = IrcServerOptionsTable::RowType; using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>; @@ -98,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; @@ -118,8 +124,22 @@ 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. + */ 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=""); + int limit=-1, const std::string& start="", const std::string& end="", + const Id::real_type reference_record_id=Id::unset_value, Paging=Paging::first); + + /** + * Get just one single record matching the given uuid, between (optional) end and start. + * If it does not exist (or is not between end and start), throw a RecordNotFound exception. + */ + static MucLogLine get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server, const std::string& uuid, 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); @@ -144,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; /** @@ -181,11 +203,20 @@ class Database static auto raw_exec(const std::string& query) { - Database::db->raw_exec(query); + return Database::db->raw_exec(query); } private: static std::string gen_uuid(); static std::map<CacheKey, EncodingIn::real_type> encoding_in_cache; }; + +class Transaction +{ +public: + Transaction(); + ~Transaction(); + bool success{false}; +}; + #endif /* USE_DATABASE */ diff --git a/src/database/delete_query.hpp b/src/database/delete_query.hpp new file mode 100644 index 0000000..dce705b --- /dev/null +++ b/src/database/delete_query.hpp @@ -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"); + } +}; diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp index 9726424..230e873 100644 --- a/src/database/insert_query.hpp +++ b/src/database/insert_query.hpp @@ -1,10 +1,15 @@ #pragma once #include <database/statement.hpp> +#include <database/database.hpp> #include <database/column.hpp> #include <database/query.hpp> +#include <database/row.hpp> + #include <logger/logger.hpp> +#include <utils/is_one_of.hpp> + #include <type_traits> #include <vector> #include <string> @@ -22,7 +27,7 @@ update_autoincrement_id(std::tuple<T...>& columns, Statement& statement) template <std::size_t N=0, typename... T> typename std::enable_if<N == sizeof...(T), void>::type -update_autoincrement_id(std::tuple<T...>&, Statement& statement) +update_autoincrement_id(std::tuple<T...>&, Statement&) {} struct InsertQuery: public Query @@ -127,3 +132,13 @@ struct InsertQuery: public Query insert_col_name(const std::tuple<T...>&) {} }; + +template <typename... T> +void insert(Row<T...>& row, DatabaseEngine& db) +{ + InsertQuery query(row.table_name, row.columns); + // Ugly workaround for non portable stuff + if (is_one_of<Id, T...>) + query.body += db.get_returning_id_sql_string(Id::name); + query.execute(db, row.columns); +} diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp index 984a959..59bc885 100644 --- a/src/database/postgresql_engine.cpp +++ b/src/database/postgresql_engine.cpp @@ -11,6 +11,8 @@ #include <logger/logger.hpp> +#include <cstring> + PostgresqlEngine::PostgresqlEngine(PGconn*const conn): conn(conn) {} @@ -20,6 +22,15 @@ PostgresqlEngine::~PostgresqlEngine() PQfinish(this->conn); } +static void logging_notice_processor(void*, const char* original) +{ + if (original && std::strlen(original) > 0) + { + std::string message{original, std::strlen(original) - 1}; + log_warning("PostgreSQL: ", message); + } +} + std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& conninfo) { PGconn* con = PQconnectdb(conninfo.data()); @@ -34,8 +45,10 @@ std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& connin { const char* errmsg = PQerrorMessage(con); log_error("Postgresql connection failed: ", errmsg); + PQfinish(con); throw std::runtime_error("failed to open connection."); } + PQsetNoticeProcessor(con, &logging_notice_processor, nullptr); return std::make_unique<PostgresqlEngine>(con); } diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp index fe4fb53..1a9c249 100644 --- a/src/database/postgresql_engine.hpp +++ b/src/database/postgresql_engine.hpp @@ -36,12 +36,15 @@ private: #else +using namespace std::string_literals; + class PostgresqlEngine { public: static std::unique_ptr<DatabaseEngine> open(const std::string& string) { throw std::runtime_error("Cannot open postgresql database "s + string + ": biboumi is not compiled with libpq."); + return {}; } }; diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp index 571c8f1..37e8ea0 100644 --- a/src/database/postgresql_statement.hpp +++ b/src/database/postgresql_statement.hpp @@ -6,6 +6,8 @@ #include <libpq-fe.h> +#include <cstring> + class PostgresqlStatement: public Statement { public: @@ -90,7 +92,7 @@ class PostgresqlStatement: public Statement private: private: - bool execute() + bool execute(const bool second_attempt=false) { std::vector<const char*> params; params.reserve(this->params.size()); @@ -108,8 +110,20 @@ private: const auto status = PQresultStatus(this->result); if (status != PGRES_TUPLES_OK && status != PGRES_COMMAND_OK) { - log_error("Failed to execute command: ", PQresultErrorMessage(this->result)); - return false; + const char* original = PQerrorMessage(this->conn); + if (original && std::strlen(original) > 0) + log_error("Failed to execute command: ", std::string{original, std::strlen(original) - 1}); + if (PQstatus(this->conn) != CONNECTION_OK && !second_attempt) + { + log_info("Trying to reconnect to PostgreSQL server and execute the query again."); + PQreset(this->conn); + return this->execute(true); + } + else + { + log_error("Givin up."); + return false; + } } return true; } diff --git a/src/database/query.cpp b/src/database/query.cpp index 4054007..d72066e 100644 --- a/src/database/query.cpp +++ b/src/database/query.cpp @@ -6,7 +6,7 @@ void actual_bind(Statement& statement, const std::string& value, int index) statement.bind_text(index, value); } -void actual_bind(Statement& statement, const std::size_t value, int index) +void actual_bind(Statement& statement, const std::int64_t& value, int index) { statement.bind_int64(index, value); } @@ -21,7 +21,6 @@ void actual_bind(Statement& statement, const OptionalBool& value, int index) statement.bind_int64(index, -1); } - void actual_add_param(Query& query, const std::string& val) { query.params.push_back(val); diff --git a/src/database/query.hpp b/src/database/query.hpp index 547138f..ba28b1a 100644 --- a/src/database/query.hpp +++ b/src/database/query.hpp @@ -12,7 +12,12 @@ #include <string> void actual_bind(Statement& statement, const std::string& value, int index); -void actual_bind(Statement& statement, const std::size_t value, int index); +void actual_bind(Statement& statement, const std::int64_t& value, int index); +template <typename T, typename std::enable_if_t<std::is_integral<T>::value>* = 0> +void actual_bind(Statement& statement, const T& value, int index) +{ + actual_bind(statement, static_cast<std::int64_t>(value), index); +} void actual_bind(Statement& statement, const OptionalBool& value, int index); #ifdef DEBUG_SQL_QUERIES diff --git a/src/database/row.hpp b/src/database/row.hpp index 2d55897..27caf43 100644 --- a/src/database/row.hpp +++ b/src/database/row.hpp @@ -1,9 +1,5 @@ #pragma once -#include <database/insert_query.hpp> -#include <database/update_query.hpp> -#include <logger/logger.hpp> - #include <utils/is_one_of.hpp> #include <type_traits> @@ -29,43 +25,7 @@ struct Row return col.value; } - template <bool Coucou=true> - void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<!is_one_of<Id, T...>::value && Coucou>::type* = nullptr) - { - this->insert(*db); - } - - template <bool Coucou=true> - void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<is_one_of<Id, T...>::value && Coucou>::type* = nullptr) - { - const Id& id = std::get<Id>(this->columns); - if (id.value == Id::unset_value) - { - this->insert(*db); - std::get<Id>(this->columns).value = db->last_inserted_rowid; - } - else - this->update(*db); - } - - private: - void insert(DatabaseEngine& db) - { - InsertQuery query(this->table_name, this->columns); - // Ugly workaround for non portable stuff - query.body += db.get_returning_id_sql_string(Id::name); - query.execute(db, this->columns); - } - - void update(DatabaseEngine& db) - { - UpdateQuery query(this->table_name, this->columns); - - query.execute(db, this->columns); - } - public: std::tuple<T...> columns; std::string table_name; - }; diff --git a/src/database/save.hpp b/src/database/save.hpp new file mode 100644 index 0000000..4362110 --- /dev/null +++ b/src/database/save.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include <database/update_query.hpp> +#include <database/insert_query.hpp> + +#include <database/engine.hpp> + +#include <database/row.hpp> + +#include <utils/is_one_of.hpp> + +template <typename... T, bool Coucou=true> +void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr) +{ + insert(row, db); +} + +template <typename... T, bool Coucou=true> +void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr) +{ + const Id& id = std::get<Id>(row.columns); + if (id.value == Id::unset_value) + { + insert(row, db); + if (db.last_inserted_rowid >= 0) + std::get<Id>(row.columns).value = static_cast<Id::real_type>(db.last_inserted_rowid); + } + else + update(row, db); +} + diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp index 5a17f38..b9fdc06 100644 --- a/src/database/select_query.hpp +++ b/src/database/select_query.hpp @@ -2,6 +2,8 @@ #include <database/engine.hpp> +#include <database/table.hpp> +#include <database/database.hpp> #include <database/statement.hpp> #include <database/query.hpp> #include <logger/logger.hpp> @@ -115,6 +117,8 @@ struct SelectQuery: public Query #endif auto statement = db.prepare(this->body); + if (!statement) + return rows; statement->bind(std::move(this->params)); while (statement->step() == StepResult::Row) @@ -130,3 +134,9 @@ struct SelectQuery: public Query const std::string table_name; }; +template <typename... T> +auto select(const Table<T...> table) +{ + SelectQuery<T...> query(table.name); + return query; +} diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp index ae4a146..5e3bba1 100644 --- a/src/database/sqlite3_engine.cpp +++ b/src/database/sqlite3_engine.cpp @@ -3,7 +3,6 @@ #ifdef SQLITE3_FOUND #include <database/sqlite3_engine.hpp> - #include <database/sqlite3_statement.hpp> #include <database/query.hpp> diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp index 5b8176c..a7bfcdb 100644 --- a/src/database/sqlite3_engine.hpp +++ b/src/database/sqlite3_engine.hpp @@ -35,12 +35,15 @@ private: #else +using namespace std::string_literals; + class Sqlite3Engine { public: static std::unique_ptr<DatabaseEngine> open(const std::string& string) { throw std::runtime_error("Cannot open sqlite3 database "s + string + ": biboumi is not compiled with sqlite3 lib."); + return {}; } }; diff --git a/src/database/sqlite3_statement.hpp b/src/database/sqlite3_statement.hpp index 7738fa6..3ed60c0 100644 --- a/src/database/sqlite3_statement.hpp +++ b/src/database/sqlite3_statement.hpp @@ -88,5 +88,4 @@ class Sqlite3Statement: public Statement private: sqlite3_stmt* stmt; - int last_step_result{SQLITE_OK}; }; diff --git a/src/database/table.hpp b/src/database/table.hpp index 680e7cc..0b8bfc0 100644 --- a/src/database/table.hpp +++ b/src/database/table.hpp @@ -2,7 +2,7 @@ #include <database/engine.hpp> -#include <database/select_query.hpp> +#include <database/delete_query.hpp> #include <database/row.hpp> #include <algorithm> @@ -79,10 +79,10 @@ class Table return {this->name}; } - auto select() + auto del() { - SelectQuery<T...> select(this->name); - return select; + DeleteQuery query(this->name); + return query; } const std::string& get_name() const @@ -90,6 +90,8 @@ class Table return this->name; } + const std::string name; + private: template <std::size_t N=0> @@ -124,5 +126,4 @@ class Table add_column_create(DatabaseEngine&, std::string&) { } - const std::string name; }; diff --git a/src/database/update_query.hpp b/src/database/update_query.hpp index a29ac3f..c2b819d 100644 --- a/src/database/update_query.hpp +++ b/src/database/update_query.hpp @@ -1,7 +1,8 @@ #pragma once -#include <database/query.hpp> #include <database/engine.hpp> +#include <database/query.hpp> +#include <database/row.hpp> using namespace std::string_literals; @@ -102,3 +103,11 @@ struct UpdateQuery: public Query actual_bind(statement, value.value, sizeof...(T)); } }; + +template <typename... T> +void update(Row<T...>& row, DatabaseEngine& db) +{ + UpdateQuery query(row.table_name, row.columns); + + query.execute(db, row.columns); +} diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp index 53043c7..2dd20fe 100644 --- a/src/irc/irc_channel.cpp +++ b/src/irc/irc_channel.cpp @@ -33,8 +33,9 @@ IrcUser* IrcChannel::find_user(const std::string& name) const return nullptr; } -void IrcChannel::remove_user(const IrcUser* user) +std::unique_ptr<IrcUser> IrcChannel::remove_user(const IrcUser* user) { + std::unique_ptr<IrcUser> result{}; const auto nick = user->nick; const bool is_self = (user == this->self); const auto it = std::find_if(this->users.begin(), this->users.end(), @@ -44,6 +45,7 @@ void IrcChannel::remove_user(const IrcUser* user) }); if (it != this->users.end()) { + result = std::move(*it); this->users.erase(it); if (is_self) { @@ -51,16 +53,5 @@ void IrcChannel::remove_user(const IrcUser* user) this->joined = false; } } -} - -void IrcChannel::remove_all_users() -{ - this->users.clear(); - this->self = nullptr; -} - -DummyIrcChannel::DummyIrcChannel(): - IrcChannel(), - joining(false) -{ + return result; } diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp index 8f85edb..7000ada 100644 --- a/src/irc/irc_channel.hpp +++ b/src/irc/irc_channel.hpp @@ -32,8 +32,7 @@ public: IrcUser* add_user(const std::string& name, const std::map<char, char>& prefix_to_mode); IrcUser* find_user(const std::string& name) const; - void remove_user(const IrcUser* user); - void remove_all_users(); + std::unique_ptr<IrcUser> remove_user(const IrcUser* user); const std::vector<std::unique_ptr<IrcUser>>& get_users() const { return this->users; } @@ -42,33 +41,3 @@ protected: IrcUser* self{nullptr}; std::vector<std::unique_ptr<IrcUser>> users{}; }; - -/** - * A special channel that is not actually linked to any real irc - * channel. This is just a channel representing a connection to the - * server. If an user wants to maintain the connection to the server without - * having to be on any irc channel of that server, he can just join this - * dummy channel. - * It’s not actually dummy because it’s useful and it does things, but well. - */ -class DummyIrcChannel: public IrcChannel -{ -public: - explicit DummyIrcChannel(); - DummyIrcChannel(const DummyIrcChannel&) = delete; - DummyIrcChannel(DummyIrcChannel&&) = delete; - DummyIrcChannel& operator=(const DummyIrcChannel&) = delete; - DummyIrcChannel& operator=(DummyIrcChannel&&) = delete; - - /** - * This flag is at true whenever the user wants to join this channel, but - * he is not yet connected to the server. When the connection is made, we - * check that flag and if it’s true, we inform the user that he has just - * joined that channel. - * If the user is already connected to the server when he tries to join - * the channel, we don’t use that flag, we just join it immediately. - */ - bool joining; -}; - - diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 40078d9..8f77e0d 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -145,14 +145,6 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, chanmodes({"", "", "", ""}), chantypes({'#', '&'}) { - this->dummy_channel.topic = "This is a virtual channel provided for " - "convenience by biboumi, it is not connected " - "to any actual IRC channel of the server '" + this->hostname + - "', and sending messages in it has no effect. " - "Its main goal is to keep the connection to the IRC server " - "alive without having to join a real channel of that server. " - "To disconnect from the IRC server, leave this room and all " - "other IRC channels of that server."; #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->get_hostname()); @@ -194,20 +186,22 @@ void IrcClient::start() bool tls; std::tie(port, tls) = this->ports_to_try.top(); this->ports_to_try.pop(); - this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " + - this->hostname + ":" + port + " (" + - (tls ? "encrypted" : "not encrypted") + ")"); - this->bind_addr = Config::get("outgoing_bind", ""); + std::string address = this->hostname; -#ifdef BOTAN_FOUND -# ifdef USE_DATABASE +#ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->get_hostname()); +# ifdef BOTAN_FOUND this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>()); # endif + if (!options.col<Database::Address>().empty()) + address = options.col<Database::Address>(); #endif - this->connect(this->hostname, port, tls); + this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " + + address + ":" + port + " (" + + (tls ? "encrypted" : "not encrypted") + ")"); + this->connect(address, port, tls); } void IrcClient::on_connection_failed(const std::string& reason) @@ -315,8 +309,6 @@ void IrcClient::on_connection_close(const std::string& error_msg) IrcChannel* IrcClient::get_channel(const std::string& n) { - if (n.empty()) - return &this->dummy_channel; const std::string name = utils::tolower(n); try { @@ -670,10 +662,7 @@ void IrcClient::on_channel_join(const IrcMessage& message) { const std::string chan_name = utils::tolower(message.arguments[0]); IrcChannel* channel; - if (chan_name.empty()) - channel = &this->dummy_channel; - else - channel = this->get_channel(chan_name); + channel = this->get_channel(chan_name); const std::string nick = message.prefix; IrcUser* user = channel->add_user(nick, this->prefix_to_mode); if (channel->joined == false) @@ -900,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), @@ -948,18 +938,6 @@ void IrcClient::on_welcome_message(const IrcMessage& message) if (!channels_with_key.empty()) this->send_join_command(channels_with_key, keys); this->channels_to_join.clear(); - // Indicate that the dummy channel is joined as well, if needed - if (this->dummy_channel.joining) - { - // Simulate a message coming from the IRC server saying that we joined - // the channel - const IrcMessage join_message(this->get_nick(), "JOIN", {""}); - this->on_channel_join(join_message); - const IrcMessage end_join_message(std::string(this->hostname), "366", - {this->get_nick(), - "", "End of NAMES list"}); - this->on_channel_completely_joined(end_join_message); - } } void IrcClient::on_part(const IrcMessage& message) @@ -976,18 +954,18 @@ void IrcClient::on_part(const IrcMessage& message) { std::string nick = user->nick; bool self = channel->get_self() && channel->get_self()->nick == nick; - channel->remove_user(user); - Iid iid; - iid.set_local(chan_name); - iid.set_server(this->hostname); - iid.type = Iid::Type::Channel; + auto user_ptr = channel->remove_user(user); if (self) { this->channels.erase(utils::tolower(chan_name)); // channel pointer is now invalid channel = nullptr; } - this->bridge.send_muc_leave(iid, std::move(nick), txt, self, true); + Iid iid; + iid.set_local(chan_name); + iid.set_server(this->hostname); + iid.type = Iid::Type::Channel; + this->bridge.send_muc_leave(iid, *user_ptr, txt, self, true, {}, this); } } @@ -1004,8 +982,7 @@ void IrcClient::on_error(const IrcMessage& message) IrcChannel* channel = pair.second.get(); if (!channel->joined) continue; - std::string own_nick = channel->get_self()->nick; - this->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true, false); + this->bridge.send_muc_leave(iid, *channel->get_self(), leave_message, true, false, {}, this); } this->channels.clear(); this->send_gateway_message("ERROR: " + leave_message); @@ -1030,7 +1007,7 @@ void IrcClient::on_quit(const IrcMessage& message) iid.set_local(chan_name); iid.set_server(this->hostname); iid.type = Iid::Type::Channel; - this->bridge.send_muc_leave(iid, user->nick, txt, self, false); + this->bridge.send_muc_leave(iid, *user, txt, self, false, {}, this); channel->remove_user(user); } } @@ -1062,10 +1039,6 @@ void IrcClient::on_nick(const IrcMessage& message) } }; - if (this->get_dummy_channel().joined) - { - change_nick_func("", &this->get_dummy_channel()); - } for (const auto& pair: this->channels) { change_nick_func(pair.first, pair.second.get()); @@ -1248,25 +1221,7 @@ void IrcClient::on_unknown_message(const IrcMessage& message) size_t IrcClient::number_of_joined_channels() const { - if (this->dummy_channel.joined) - return this->channels.size() + 1; - else - return this->channels.size(); -} - -DummyIrcChannel& IrcClient::get_dummy_channel() -{ - return this->dummy_channel; -} - -void IrcClient::leave_dummy_channel(const std::string& exit_message, const std::string& resource) -{ - if (!this->dummy_channel.joined) - return; - this->dummy_channel.joined = false; - this->dummy_channel.joining = false; - this->dummy_channel.remove_all_users(); - this->bridge.send_muc_leave(Iid("%" + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, true, resource); + return this->channels.size(); } #ifdef BOTAN_FOUND diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index de5c520..fd97fe6 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -279,15 +279,6 @@ public: * Return the number of joined channels */ size_t number_of_joined_channels() const; - /** - * Get a reference to the unique dummy channel - */ - DummyIrcChannel& get_dummy_channel(); - /** - * Leave the dummy channel: forward a message to the user to indicate that - * he left it, and mark it as not joined. - */ - void leave_dummy_channel(const std::string& exit_message, const std::string& resource); const std::string& get_hostname() const { return this->hostname; } std::string get_nick() const { return this->current_nick; } @@ -340,11 +331,6 @@ private: */ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels; /** - * A single channel with a iid of the form "hostname" (normal channel have - * an iid of the form "chan%hostname". - */ - DummyIrcChannel dummy_channel; - /** * A list of chan we want to join (tuples with the channel name and the * password, if any), but we need a response 001 from the server before * sending the actual JOIN commands. So we just keep the channel names in diff --git a/src/main.cpp b/src/main.cpp index c877e43..59fda4e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,7 +135,9 @@ int main(int ac, char** av) std::make_shared<BiboumiComponent>(p, hostname, password); xmpp_component->start(); - IdentdServer identd(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113))); + std::unique_ptr<IdentdServer> identd; + if (Config::get_int("identd_port", 113) != 0) + identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113))); auto timeout = TimedEventsManager::instance().get_timeout(); while (p->poll(timeout) != -1) @@ -144,7 +146,8 @@ int main(int ac, char** av) // Check for empty irc_clients (not connected, or with no joined // channel) and remove them xmpp_component->clean(); - identd.clean(); + if (identd) + identd->clean(); if (stop) { log_info("Signal received, exiting..."); @@ -157,7 +160,8 @@ int main(int ac, char** av) #ifdef UDNS_FOUND dns_handler.destroy(); #endif - identd.shutdown(); + if (identd) + identd->shutdown(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } @@ -199,7 +203,8 @@ int main(int ac, char** av) #ifdef UDNS_FOUND dns_handler.destroy(); #endif - identd.shutdown(); + if (identd) + identd->shutdown(); } } // If the only existing connection is the one to the XMPP component: diff --git a/src/utils/is_one_of.hpp b/src/utils/is_one_of.hpp index c706421..4d6770e 100644 --- a/src/utils/is_one_of.hpp +++ b/src/utils/is_one_of.hpp @@ -3,12 +3,15 @@ #include <type_traits> template <typename...> -struct is_one_of { +struct is_one_of_implem { static constexpr bool value = false; }; template <typename F, typename S, typename... T> -struct is_one_of<F, S, T...> { +struct is_one_of_implem<F, S, T...> { static constexpr bool value = - std::is_same<F, S>::value || is_one_of<F, T...>::value; + std::is_same<F, S>::value || is_one_of_implem<F, T...>::value; }; + +template<typename... T> +constexpr bool is_one_of = is_one_of_implem<T...>::value; diff --git a/src/utils/time.cpp b/src/utils/time.cpp index bc2c18d..71306fd 100644 --- a/src/utils/time.cpp +++ b/src/utils/time.cpp @@ -9,9 +9,10 @@ namespace utils { -std::string to_string(const std::time_t& timestamp) +std::string to_string(const std::chrono::system_clock::time_point::rep& time) { constexpr std::size_t stamp_size = 21; + const std::time_t timestamp = static_cast<std::time_t>(time); char date_buf[stamp_size]; if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp)) != stamp_size - 1) return ""; diff --git a/src/utils/time.hpp b/src/utils/time.hpp index c71cd9c..4b19634 100644 --- a/src/utils/time.hpp +++ b/src/utils/time.hpp @@ -2,9 +2,10 @@ #include <ctime> #include <string> +#include <chrono> namespace utils { -std::string to_string(const std::time_t& timestamp); +std::string to_string(const std::chrono::system_clock::time_point::rep& timestamp); std::time_t parse_datetime(const std::string& stamp); -}
\ No newline at end of file +} diff --git a/src/utils/uuid.cpp b/src/utils/uuid.cpp new file mode 100644 index 0000000..23b71fe --- /dev/null +++ b/src/utils/uuid.cpp @@ -0,0 +1,14 @@ +#include <utils/uuid.hpp> +#include <uuid/uuid.h> + +namespace utils +{ +std::string gen_uuid() +{ + char uuid_str[37]; + uuid_t uuid; + uuid_generate(uuid); + uuid_unparse(uuid, uuid_str); + return uuid_str; +} +} diff --git a/src/utils/uuid.hpp b/src/utils/uuid.hpp new file mode 100644 index 0000000..d550475 --- /dev/null +++ b/src/utils/uuid.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include <string> + +namespace utils +{ +std::string gen_uuid(); +} diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index bb48781..bc4c108 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -41,7 +41,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co XmlSubNode condition(error, STANZA_NS":item-not-found"); } else if (command_it->second.is_admin_only() && - Config::get("admin", "") != jid.local + "@" + jid.domain) + !Config::is_in_list("admin", jid.bare())) { XmlSubNode error(command_node, ADHOC_NS":error"); error["type"] = "cancel"; diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index bcdac39..53806d6 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -14,6 +14,7 @@ #ifdef USE_DATABASE #include <database/database.hpp> +#include <database/save.hpp> #endif #ifndef HAS_PUT_TIME @@ -196,7 +197,7 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, options.col<Database::GlobalPersistent>() = to_bool(value->get_inner()); } - options.save(Database::db); + save(options, *Database::db); command_node.delete_all_children(); XmlSubNode note(command_node, "note"); @@ -219,6 +220,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"; @@ -228,6 +230,19 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com instructions.set_inner("Edit the form, to configure the settings of the IRC server " + server_domain); { + XmlSubNode field(x, "field"); + field["var"] = "address"; + field["type"] = "text-single"; + field["label"] = "Address"; + field["desc"] = "The address (hostname or IP) to connect to."; + XmlSubNode value(field, "value"); + if (options.col<Database::Address>().empty()) + value.set_inner(server_domain); + else + value.set_inner(options.col<Database::Address>()); + } + + { XmlSubNode ports(x, "field"); ports["var"] = "ports"; ports["type"] = "text-multi"; @@ -279,6 +294,20 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com } } #endif + + { + XmlSubNode field(x, "field"); + field["var"] = "nick"; + field["type"] = "text-single"; + field["label"] = "Nickname"; + field["desc"] = "If set, will override the nickname provided in the initial presence sent to join the first server channel"; + if (!options.col<Database::Nick>().empty()) + { + XmlSubNode value(field, "value"); + value.set_inner(options.col<Database::Nick>()); + } + } + { XmlSubNode pass(x, "field"); pass["var"] = "pass"; @@ -294,14 +323,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>()); } } @@ -371,10 +400,16 @@ 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"); const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data"); + + if (field->get_tag("var") == "address" && value) + options.col<Database::Address>() = value->get_inner(); + if (field->get_tag("var") == "ports") { std::string ports; @@ -406,11 +441,22 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com #endif // BOTAN_FOUND + else if (field->get_tag("var") == "nick" && value) + options.col<Database::Nick>() = value->get_inner(); + 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) { @@ -431,7 +477,8 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com } Database::invalidate_encoding_in_cache(); - options.save(Database::db); + save(options, *Database::db); + Database::set_after_connection_commands(options, commands); command_node.delete_all_children(); XmlSubNode note(command_node, "note"); @@ -600,7 +647,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const } Database::invalidate_encoding_in_cache(requester.bare(), iid.get_server(), iid.get_local()); - options.save(Database::db); + save(options, *Database::db); } return true; } @@ -611,7 +658,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) { const Jid owner(session.get_owner_jid()); - if (owner.bare() != Config::get("admin", "")) + if (!Config::is_in_list("admin", owner.bare())) { // 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(); diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 8775869..6dc5fc5 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -7,6 +7,7 @@ #include <xmpp/adhoc_command.hpp> #include <xmpp/biboumi_adhoc_commands.hpp> #include <bridge/list_element.hpp> +#include <utils/encoding.hpp> #include <config/config.hpp> #include <utils/time.hpp> #include <xmpp/jid.hpp> @@ -147,13 +148,10 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) try { if (iid.type == Iid::Type::Channel && !iid.get_server().empty()) - { // presence toward a MUC that corresponds to an irc channel, or a - // dummy channel if iid.chan is empty + { // presence toward a MUC that corresponds to an irc channel if (type.empty()) { const std::string own_nick = bridge->get_own_nick(iid); - if (!own_nick.empty() && own_nick != to.resource) - bridge->send_irc_nick_change(iid, to.resource, from.resource); const XmlNode* x = stanza.get_child("x", MUC_NS); const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr; const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr; @@ -181,7 +179,9 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) history_limit.stanzas = 0; } bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", - from.resource, history_limit); + from.resource, history_limit, x != nullptr); + if (!own_nick.empty() && own_nick != to.resource) + bridge->send_irc_nick_change(iid, to.resource, from.resource); } else if (type == "unavailable") { @@ -279,7 +279,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) { if (body && !body->get_inner().empty()) { - bridge->send_channel_message(iid, body->get_inner()); + bridge->send_channel_message(iid, body->get_inner(), id); } const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS); if (subject) @@ -466,8 +466,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) #ifdef USE_DATABASE else if ((query = stanza.get_child("query", MAM_NS))) { - if (this->handle_mam_request(stanza)) - stanza_error.disable(); + try { + if (this->handle_mam_request(stanza)) + stanza_error.disable(); + } catch (const Database::RecordNotFound& exc) { + error_name = "item-not-found"; + return; + } } else if ((query = stanza.get_child("query", MUC_OWNER_NS))) { @@ -546,24 +551,21 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) if (to.local.empty()) { // Get biboumi's adhoc commands this->send_adhoc_commands_list(id, from, this->served_hostname, - (Config::get("admin", "") == - from_jid.bare()), + Config::is_in_list("admin", from_jid.bare()), this->adhoc_commands_handler); stanza_error.disable(); } else if (iid.type == Iid::Type::Server) { // Get the server's adhoc commands this->send_adhoc_commands_list(id, from, to_str, - (Config::get("admin", "") == - from_jid.bare()), + Config::is_in_list("admin", from_jid.bare()), this->irc_server_adhoc_commands_handler); stanza_error.disable(); } else if (iid.type == Iid::Type::Channel && to.resource.empty()) { // Get the channel's adhoc commands this->send_adhoc_commands_list(id, from, to_str, - (Config::get("admin", "") == - from_jid.bare()), + Config::is_in_list("admin", from_jid.bare()), this->irc_channel_adhoc_commands_handler); stanza_error.disable(); } @@ -714,29 +716,58 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) } const XmlNode* set = query->get_child("set", RSM_NS); int limit = -1; + Id::real_type reference_record_id{Id::unset_value}; + Database::Paging paging_order{Database::Paging::first}; if (set) { const XmlNode* max = set->get_child("max", RSM_NS); if (max) limit = std::atoi(max->get_inner().data()); + const XmlNode* after = set->get_child("after", RSM_NS); + if (after) + { + auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), + after->get_inner(), start, end); + reference_record_id = after_record.col<Id>(); + } + const XmlNode* before = set->get_child("before", RSM_NS); + if (before) + { + paging_order = Database::Paging::last; + if (!before->get_inner().empty()) + { + auto before_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), before->get_inner(), start, end); + reference_record_id = before_record.col<Id>(); + } + } } - // If the archive is really big, and the client didn’t specify any - // limit, we avoid flooding it: we set an arbitrary max limit. - if (limit == -1 && start.empty() && end.empty()) + // Do not send more than 100 messages, even if the client asked for more, + // or if it didn’t specify any limit. + // 101 is just a trick to know if there are more available messages. + // If our query returns 101 message, we know it’s incomplete, but we + // still send only 100 + if ((limit == -1 && start.empty() && end.empty()) + || limit > 100) + limit = 101; + auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end, reference_record_id, paging_order); + bool complete = true; + if (lines.size() > 100) { - limit = 100; + complete = false; + lines.erase(lines.begin(), std::prev(lines.end(), 100)); } - const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end); for (const Database::MucLogLine& line: lines) - { - if (!line.col<Database::Nick>().empty()) - this->send_archived_message(line, to.full(), from.full(), query_id); - } + { + if (!line.col<Database::Nick>().empty()) + this->send_archived_message(line, to.full(), from.full(), query_id); + } { auto fin_ptr = std::make_unique<XmlNode>("fin"); { XmlNode& fin = *(fin_ptr.get()); fin["xmlns"] = MAM_NS; + if (complete) + fin["complete"] = "true"; XmlSubNode set(fin, "set"); set["xmlns"] = RSM_NS; if (!lines.empty()) @@ -881,7 +912,7 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = "Biboumi XMPP-IRC gateway"; - for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; @@ -905,7 +936,7 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = "IRC server " + from.local + " over Biboumi"; - for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; @@ -947,8 +978,8 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const XmlSubNode identity(query, "identity"); identity["category"] = "conference"; identity["type"] = "irc"; - identity["name"] = "IRC channel " + iid.get_local() + " from server " + iid.get_server() + " over biboumi"; - for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + identity["name"] = ""s + iid.get_local() + " on " + iid.get_server(); + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; @@ -1010,7 +1041,9 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std for (auto it = begin; it != end; ++it) { XmlSubNode item(query, "item"); - item["jid"] = it->channel + "@" + this->served_hostname; + std::string channel_name = it->channel; + xep0106::encode(channel_name); + item["jid"] = channel_name + "@" + this->served_hostname; } if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index 8f6826e..b3d925e 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -2,6 +2,7 @@ #include <utils/scopeguard.hpp> #include <utils/tolower.hpp> #include <logger/logger.hpp> +#include <utils/uuid.hpp> #include <xmpp/xmpp_component.hpp> #include <config/config.hpp> @@ -14,8 +15,6 @@ #include <iostream> #include <set> -#include <uuid/uuid.h> - #include <cstdlib> #include <set> @@ -364,10 +363,11 @@ void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, cons this->send_stanza(message); } -void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid) +void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid, std::string id) { Stanza message("message"); message["to"] = jid_to; + message["id"] = std::move(id); if (!nick.empty()) message["from"] = muc_name + "@" + this->served_hostname + "/" + nick; else // Message from the room itself @@ -398,7 +398,8 @@ void XmppComponent::send_muc_message(const std::string& muc_name, const std::str this->send_stanza(message); } -void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, std::time_t timestamp) +#ifdef USE_DATABASE +void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, Database::time_point::rep timestamp) { Stanza message("message"); message["to"] = jid_to; @@ -421,9 +422,11 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std: this->send_stanza(message); } +#endif void XmppComponent::send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message, - const std::string& jid_to, const bool self, const bool user_requested) + const std::string& jid_to, const bool self, const bool user_requested, + const std::string& affiliation, const std::string& role) { Stanza presence("presence"); { @@ -445,6 +448,9 @@ void XmppComponent::send_muc_leave(const std::string& muc_name, const std::strin status["code"] = "332"; } } + XmlSubNode item(x, "item"); + item["affiliation"] = affiliation; + item["role"] = role; if (!message_str.empty()) { XmlSubNode status(presence, "status"); @@ -667,9 +673,5 @@ void XmppComponent::send_iq_result(const std::string& id, const std::string& to_ std::string XmppComponent::next_id() { - char uuid_str[37]; - uuid_t uuid; - uuid_generate(uuid); - uuid_unparse(uuid, uuid_str); - return uuid_str; + return utils::gen_uuid(); } diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index 3950863..e18da40 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -1,8 +1,10 @@ #pragma once +#include "biboumi.h" #include <xmpp/adhoc_commands_handler.hpp> #include <network/tcp_client_socket_handler.hpp> +#include <database/database.hpp> #include <xmpp/xmpp_parser.hpp> #include <xmpp/body.hpp> @@ -35,6 +37,7 @@ #define RSM_NS "http://jabber.org/protocol/rsm" #define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic" #define STABLE_ID_NS "urn:xmpp:sid:0" +#define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id" /** * An XMPP component, communicating with an XMPP server using the protocole @@ -132,12 +135,14 @@ public: * Send a (non-private) message to the MUC */ void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to, - std::string uuid); + std::string uuid, std::string id); +#ifdef USE_DATABASE /** * Send a message, with a <delay/> element, part of a MUC history */ void send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body, - const std::string& jid_to, const std::time_t timestamp); + const std::string& jid_to, Database::time_point::rep timestamp); +#endif /** * Send an unavailable presence for this nick */ @@ -146,7 +151,8 @@ public: Xmpp::body&& message, const std::string& jid_to, const bool self, - const bool user_requested); + const bool user_requested, + const std::string& affiliation, const std::string& role); /** * Indicate that a participant changed his nick */ |