diff options
-rw-r--r-- | doc/biboumi.1.rst | 9 | ||||
-rw-r--r-- | src/database/column.hpp | 4 | ||||
-rw-r--r-- | src/database/database.cpp | 29 | ||||
-rw-r--r-- | src/database/database.hpp | 10 | ||||
-rw-r--r-- | src/database/delete_query.hpp | 33 | ||||
-rw-r--r-- | src/database/table.hpp | 7 | ||||
-rw-r--r-- | src/irc/irc_client.cpp | 5 | ||||
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.cpp | 28 | ||||
-rw-r--r-- | tests/database.cpp | 43 | ||||
-rw-r--r-- | tests/end_to_end/__main__.py | 11 |
10 files changed, 159 insertions, 20 deletions
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst index d4b2f0a..2f17823 100644 --- a/doc/biboumi.1.rst +++ b/doc/biboumi.1.rst @@ -645,10 +645,11 @@ On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com) In encoding into UTF-8. If the conversion fails at some point, some characters will be replaced by the placeholders. * Out encoding: Currently ignored. - * After-connection IRC command: A raw IRC command that will be sent to - the server immediately after the connection has been successful. It - can for example be used to identify yourself using NickServ, with a - command like this: `PRIVMSG NickServ :identify PASSWORD`. + * After-connection IRC commands: Raw IRC commands that will be sent + one by one to the server immediately after the connection has been + successful. It can for example be used to identify yourself using + NickServ, with a command like this: `PRIVMSG NickServ :identify + PASSWORD`. * Ports: The list of TCP ports to use when connecting to this IRC server. This list will be tried in sequence, until the connection succeeds for one of them. The connection made on these ports will not use TLS, the diff --git a/src/database/column.hpp b/src/database/column.hpp index 9367701..50c9c14 100644 --- a/src/database/column.hpp +++ b/src/database/column.hpp @@ -13,6 +13,10 @@ struct Column T value{}; }; +struct ForeignKey: Column<std::size_t> { + static constexpr auto name = "fk_"; +}; + struct Id: Column<std::size_t> { static constexpr std::size_t unset_value = static_cast<std::size_t>(-1); static constexpr auto name = "id_"; diff --git a/src/database/database.cpp b/src/database/database.cpp index d19ed7a..812d27c 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -21,6 +21,7 @@ Database::GlobalOptionsTable Database::global_options("globaloptions_"); Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_"); Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_"); Database::RosterTable Database::roster("roster"); +Database::AfterConnectionCommandsTable Database::after_connection_commands("after_connection_commands_"); std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{}; Database::GlobalPersistent::GlobalPersistent(): @@ -53,6 +54,8 @@ void Database::open(const std::string& filename) Database::irc_channel_options.upgrade(*Database::db); Database::roster.create(*Database::db); Database::roster.upgrade(*Database::db); + Database::after_connection_commands.create(*Database::db); + Database::after_connection_commands.upgrade(*Database::db); create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name()); } @@ -88,6 +91,32 @@ Database::IrcServerOptions Database::get_irc_server_options(const std::string& o return options; } +Database::AfterConnectionCommands Database::get_after_connection_commands(const IrcServerOptions& server_options) +{ + const auto id = server_options.col<Id>(); + if (id == Id::unset_value) + return {}; + auto request = Database::after_connection_commands.select(); + request.where() << ForeignKey{} << "=" << id; + return request.execute(*Database::db); +} + +void Database::set_after_connection_commands(const Database::IrcServerOptions& server_options, Database::AfterConnectionCommands& commands) +{ + const auto id = server_options.col<Id>(); + if (id == Id::unset_value) + return ; + auto query = Database::after_connection_commands.del(); + query.where() << ForeignKey{} << "=" << id; + query.execute(*Database::db); + + for (auto& command: commands) + { + command.col<ForeignKey>() = server_options.col<Id>(); + command.save(Database::db); + } +} + Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel) { auto request = Database::irc_channel_options.select(); diff --git a/src/database/database.hpp b/src/database/database.hpp index 8a967d8..0e88be8 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -92,7 +92,7 @@ class Database using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>; using GlobalOptions = GlobalOptionsTable::RowType; - using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address>; + using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address>; using IrcServerOptions = IrcServerOptionsTable::RowType; using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>; @@ -101,6 +101,9 @@ class Database using RosterTable = Table<LocalJid, RemoteJid>; using RosterItem = RosterTable::RowType; + using AfterConnectionCommandsTable = Table<Id, ForeignKey, AfterConnectionCommand>; + using AfterConnectionCommands = std::vector<AfterConnectionCommandsTable::RowType>; + Database() = default; ~Database() = default; @@ -121,6 +124,9 @@ class Database static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner, const std::string& server, const std::string& channel); + static AfterConnectionCommands get_after_connection_commands(const IrcServerOptions& server_options); + static void set_after_connection_commands(const IrcServerOptions& server_options, AfterConnectionCommands& commands); + /** * Get all the lines between (optional) start and end dates, with a (optional) limit. * If after_id is set, only the records after it will be returned. @@ -158,6 +164,8 @@ class Database static IrcServerOptionsTable irc_server_options; static IrcChannelOptionsTable irc_channel_options; static RosterTable roster; + static AfterConnectionCommandsTable after_connection_commands; + static std::unique_ptr<DatabaseEngine> db; /** 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/table.hpp b/src/database/table.hpp index 680e7cc..c8c1bdd 100644 --- a/src/database/table.hpp +++ b/src/database/table.hpp @@ -3,6 +3,7 @@ #include <database/engine.hpp> #include <database/select_query.hpp> +#include <database/delete_query.hpp> #include <database/row.hpp> #include <algorithm> @@ -85,6 +86,12 @@ class Table return select; } + auto del() + { + DeleteQuery query(this->name); + return query; + } + const std::string& get_name() const { return this->name; diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 764f37b..5f93ea6 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -889,8 +889,9 @@ void IrcClient::on_welcome_message(const IrcMessage& message) #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->get_hostname()); - if (!options.col<Database::AfterConnectionCommand>().empty()) - this->send_raw(options.col<Database::AfterConnectionCommand>()); + const auto commands = Database::get_after_connection_commands(options); + for (const auto& command: commands) + this->send_raw(command.col<Database::AfterConnectionCommand>()); #endif // Install a repeated events to regularly send a PING TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this), diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 38b6165..d773ec3 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -219,6 +219,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com server_domain = target.local; auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain, server_domain); + auto commands = Database::get_after_connection_commands(options); XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; @@ -307,14 +308,14 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com { XmlSubNode after_cnt_cmd(x, "field"); - after_cnt_cmd["var"] = "after_connect_command"; - after_cnt_cmd["type"] = "text-single"; - after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server."; - after_cnt_cmd["label"] = "After-connection IRC command"; - if (!options.col<Database::AfterConnectionCommand>().empty()) + after_cnt_cmd["var"] = "after_connect_commands"; + after_cnt_cmd["type"] = "text-multi"; + after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server."; + after_cnt_cmd["label"] = "After-connection IRC commands"; + for (const auto& command: commands) { XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value"); - after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>()); + after_cnt_cmd_value.set_inner(command.col<Database::AfterConnectionCommand>()); } } @@ -384,6 +385,8 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com server_domain = target.local; auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain, server_domain); + auto commands = Database::get_after_connection_commands(options); + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) { const XmlNode* value = field->get_child("value", "jabber:x:data"); @@ -427,8 +430,16 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com else if (field->get_tag("var") == "pass" && value) options.col<Database::Pass>() = value->get_inner(); - else if (field->get_tag("var") == "after_connect_command" && value) - options.col<Database::AfterConnectionCommand>() = value->get_inner(); + else if (field->get_tag("var") == "after_connect_commands") + { + commands.clear(); + for (const auto& val: values) + { + auto command = Database::after_connection_commands.row(); + command.col<Database::AfterConnectionCommand>() = val->get_inner(); + commands.push_back(std::move(command)); + } + } else if (field->get_tag("var") == "username" && value) { @@ -450,6 +461,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com } Database::invalidate_encoding_in_cache(); options.save(Database::db); + Database::set_after_connection_commands(options, commands); command_node.delete_all_children(); XmlSubNode note(command_node, "note"); diff --git a/tests/database.cpp b/tests/database.cpp index 7ab6da8..c9b8dc7 100644 --- a/tests/database.cpp +++ b/tests/database.cpp @@ -117,6 +117,49 @@ TEST_CASE("Database") } } + SECTION("Server options") + { + const std::string owner{"toto@example.com"}; + const std::string owner2{"toto2@example.com"}; + const std::string server{"irc.example.com"}; + + auto soptions = Database::get_irc_server_options(owner, server); + auto soptions2 = Database::get_irc_server_options(owner2, server); + + auto after_connection_commands = Database::get_after_connection_commands(soptions); + CHECK(after_connection_commands.empty()); + + soptions.save(Database::db); + soptions2.save(Database::db); + auto com = Database::after_connection_commands.row(); + com.col<Database::AfterConnectionCommand>() = "first"; + after_connection_commands.push_back(com); + com.col<Database::AfterConnectionCommand>() = "second"; + after_connection_commands.push_back(com); + Database::set_after_connection_commands(soptions, after_connection_commands); + + after_connection_commands.clear(); + com.col<Database::AfterConnectionCommand>() = "first"; + after_connection_commands.push_back(com); + com.col<Database::AfterConnectionCommand>() = "second"; + after_connection_commands.push_back(com); + Database::set_after_connection_commands(soptions2, after_connection_commands); + + after_connection_commands = Database::get_after_connection_commands(soptions); + CHECK(after_connection_commands.size() == 2); + after_connection_commands = Database::get_after_connection_commands(soptions2); + CHECK(after_connection_commands.size() == 2); + + after_connection_commands.clear(); + after_connection_commands.push_back(com); + Database::set_after_connection_commands(soptions, after_connection_commands); + + after_connection_commands = Database::get_after_connection_commands(soptions); + CHECK(after_connection_commands.size() == 1); + after_connection_commands = Database::get_after_connection_commands(soptions2); + CHECK(after_connection_commands.size() == 2); + } + Database::close(); } #endif diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 99d1dd1..f3f4594 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -2701,7 +2701,7 @@ if __name__ == '__main__': "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']", @@ -2718,7 +2718,7 @@ if __name__ == '__main__': "<field var='verify_cert'><value>1</value></field>" "<field var='fingerprint'><value>12:12:12</value></field>" "<field var='pass'><value>coucou</value></field>" - "<field var='after_connect_command'><value>INVALID command</value></field>" + "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>" "<field var='username'><value>username</value></field>" "<field var='realname'><value>realname</value></field>" "<field var='encoding_out'><value>UTF-8</value></field>" @@ -2736,7 +2736,8 @@ if __name__ == '__main__': "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']", - "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']/dataform:value[text()='INVALID command']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']", + "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']", "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']", @@ -2757,7 +2758,7 @@ if __name__ == '__main__': "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>" "<x xmlns='jabber:x:data' type='submit'>" "<field var='pass'><value></value></field>" - "<field var='after_connect_command'><value></value></field>" + "<field var='after_connect_commands'></field>" "<field var='username'><value></value></field>" "<field var='realname'><value></value></field>" "<field var='encoding_out'><value></value></field>" @@ -2770,7 +2771,7 @@ if __name__ == '__main__': "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']", "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']", "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value", - "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_command']/dataform:value", + "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value", "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value", "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value", "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value", |