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