summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst10
-rw-r--r--INSTALL.rst2
-rw-r--r--cmake/Modules/FindBOTAN.cmake5
-rw-r--r--doc/biboumi.1.rst48
-rw-r--r--docker/biboumi/alpine/Dockerfile (renamed from docker/biboumi/Dockerfile)5
-rw-r--r--docker/biboumi/alpine/README.md (renamed from docker/biboumi/README.md)0
-rw-r--r--docker/biboumi/alpine/biboumi.cfg (renamed from docker/biboumi/biboumi.cfg)0
-rw-r--r--src/bridge/bridge.cpp43
-rw-r--r--src/bridge/bridge.hpp9
-rw-r--r--src/database/count_query.hpp1
-rw-r--r--src/database/database.cpp51
-rw-r--r--src/database/database.hpp74
-rw-r--r--src/database/insert_query.hpp4
-rw-r--r--src/database/query.hpp5
-rw-r--r--src/database/row.hpp5
-rw-r--r--src/database/select_query.hpp2
-rw-r--r--src/database/table.cpp4
-rw-r--r--src/database/table.hpp21
-rw-r--r--src/irc/iid.cpp3
-rw-r--r--src/irc/irc_client.cpp33
-rw-r--r--src/network/credentials_manager.cpp23
-rw-r--r--src/network/credentials_manager.hpp5
-rw-r--r--src/network/tcp_client_socket_handler.cpp6
-rw-r--r--src/network/tcp_socket_handler.cpp9
-rw-r--r--src/network/tcp_socket_handler.hpp20
-rw-r--r--src/utils/sha1.cpp6
-rw-r--r--src/xmpp/adhoc_command.cpp2
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp8
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp400
-rw-r--r--src/xmpp/biboumi_component.cpp141
-rw-r--r--src/xmpp/biboumi_component.hpp9
-rw-r--r--src/xmpp/xmpp_component.cpp50
-rw-r--r--src/xmpp/xmpp_component.hpp13
-rw-r--r--tests/end_to_end/__main__.py211
-rw-r--r--tests/iid.cpp6
35 files changed, 794 insertions, 440 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c8cddfe..f327fb1 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,8 +3,18 @@ Version 6.0
- The LiteSQL dependency was removed. Only libsqlite3 is now necessary
to work with the database.
+ - Some JIDs can be added into users’ rosters. The component JID tells if
+ biboumi is started or not, and the IRC-server JIDs tell if the user is
+ currently connected to that server.
- The RecordHistory option can now also be configured for each IRC channel,
individually.
+ - Add a global option to make all channels persistent.
+ - Status code='332' is sent with the unavailable presences when biboumi is
+ being shutdown or the connection to the IRC server is cut unexpectedly.
+ - Support for botan version 1.11.x has been dropped, only version 2.x is
+ supported.
+ - Invitations can now be sent to any JID, not only JIDs served by the biboumi
+ instance itself.
Version 5.0 - 2017-05-24
========================
diff --git a/INSTALL.rst b/INSTALL.rst
index 5bb0ca8..3cf55a2 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -47,7 +47,7 @@ udns_ (optional, but recommended)
performances when connecting to a big number of IRC servers at the same
time.
-libbotan_ 1.11 or 2.0 (optional)
+libbotan_ 2.x (optional)
Provides TLS support. Without it, IRC connections are all made in
plain-text mode.
diff --git a/cmake/Modules/FindBOTAN.cmake b/cmake/Modules/FindBOTAN.cmake
index 9eb76c4..3f223e2 100644
--- a/cmake/Modules/FindBOTAN.cmake
+++ b/cmake/Modules/FindBOTAN.cmake
@@ -19,15 +19,14 @@ include(FindPkgConfig)
if(NOT BOTAN_FOUND)
pkg_check_modules(BOTAN botan-2)
- pkg_check_modules(BOTAN botan-1.11)
endif()
if(NOT BOTAN_FOUND)
find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h
- PATH_SUFFIXES botan-2 botan-1.11
+ PATH_SUFFIXES botan-2
DOC "The botan include directory")
- find_library(BOTAN_LIBRARIES NAMES botan botan-2 botan-1.11
+ find_library(BOTAN_LIBRARIES NAMES botan botan-2
DOC "The botan library")
# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
index 4a2225e..3de4160 100644
--- a/doc/biboumi.1.rst
+++ b/doc/biboumi.1.rst
@@ -344,6 +344,29 @@ connect you to the IRC server without joining any channel), then send your
authentication message to the user ``bot%irc.example.com@biboumi.example.com``
and finally join the room ``#foo%irc.example.com@biboumi.example.com``.
+Roster
+------
+
+You can add some JIDs provided by biboumi into your own roster, to receive
+presence from them. Biboumi will always automatically accept your requests.
+
+Biboumi’s JID
+-------------
+
+By adding the component JID into your roster, the user will receive an available
+presence whenever it is started, and an unavailable presence whenever it is being
+shutdown. This is useful to quickly view if that biboumi instance is started or
+not.
+
+IRC server JID
+--------------
+
+These presence will appear online in the user’s roster whenever they are
+connected to that IRC server (see *Connect to an IRC server* for more
+details). This is useful to keep track of which server an user is connected
+to: this is sometimes hard to remember, when they have many clients, or if
+they are using persistent channels.
+
Channel messages
----------------
@@ -432,17 +455,22 @@ be replaced by a space, because the IRC server wouldn’t accept it.
Invitations
-----------
-Biboumi forwards the mediated invitations to the target nick. If the user
-wishes to invite the user “FooBar” into a room, they can invite one of the
-following “JIDs” (one of them is not a JID, actually):
+If the invited JID is a user JID served by this biboumi instance, it will forward the
+invitation to the target nick, over IRC.
+Otherwise, the mediated instance will directly be sent to the invited JID, over XMPP.
-- foobar%anything@anything
-- anything@anything/FooBar
+Example: if the user wishes to invite the IRC user “FooBar” into a room, they can
+invite one of the following “JIDs” (one of them is not a JID, actually):
+
+- foobar%anything@biboumi.example.com
+- anything@biboumi.example.com/FooBar
- FooBar
-Note that the “anything” part are simply ignored because they have no
-meaning for the IRC server: we already know which IRC server is targeted
-using the JID of the target channel.
+(Note that the “anything” parts are simply ignored because they carry no
+additional meaning for biboumi: we already know which IRC server is targeted
+using the JID of the target channel.)
+
+Otherwise, any valid JID can be used, to invite any XMPP user.
Kicks and bans
--------------
@@ -558,6 +586,10 @@ On the gateway itself (e.g on the JID biboumi.example.com):
the database.
* Max history length: The maximum number of lines in the history
that the server is allowed to send when joining a channel.
+ * Persistent: Overrides the value specified in each individual channel,
+ all channels are persistent, whether or not their specific value is
+ true or false. See below for more details on what a persistent
+ channel is.
On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docker/biboumi/Dockerfile b/docker/biboumi/alpine/Dockerfile
index 67f0f41..c1bf5fd 100644
--- a/docker/biboumi/Dockerfile
+++ b/docker/biboumi/alpine/Dockerfile
@@ -1,4 +1,9 @@
# This Dockerfile creates a docker image running biboumi
+#
+# It is built by compiling the sources and all its dependencies
+# directly inside the image.
+# This is the prefered way to build the release image, used by the
+# end users, in production.
FROM docker.io/alpine:latest
diff --git a/docker/biboumi/README.md b/docker/biboumi/alpine/README.md
index 4b9e1e5..4b9e1e5 100644
--- a/docker/biboumi/README.md
+++ b/docker/biboumi/alpine/README.md
diff --git a/docker/biboumi/biboumi.cfg b/docker/biboumi/alpine/biboumi.cfg
index 98c5a9f..98c5a9f 100644
--- a/docker/biboumi/biboumi.cfg
+++ b/docker/biboumi/alpine/biboumi.cfg
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index 23ecfe9..e0cb36d 100644
--- a/src/bridge/bridge.cpp
+++ b/src/bridge/bridge.cpp
@@ -378,7 +378,7 @@ void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string&
}
else if (message.command == "472" && message.arguments.size() >= 2)
{
- std::string error_message = "Unknown mode: "s + message.arguments[1];
+ std::string error_message = "Unknown mode: " + message.arguments[1];
if (message.arguments.size() >= 3)
error_message = message.arguments[2];
this->xmpp.send_stanza_error("iq", from, std::to_string(iid), id, "cancel", "not-allowed",
@@ -436,9 +436,14 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
// acknowledgment from the server
bool persistent = false;
#ifdef USE_DATABASE
- const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid,
- iid.get_server(), iid.get_local());
- persistent = coptions.col<Database::Persistent>();
+ const auto goptions = Database::get_global_options(this->user_jid);
+ if (goptions.col<Database::Persistent>())
+ persistent = true;
+ else
+ {
+ const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid, iid.get_server(), iid.get_local());
+ persistent = coptions.col<Database::Persistent>();
+ }
#endif
if (channel->joined && !channel->parting && !persistent)
{
@@ -450,8 +455,10 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
}
else if (channel->joined)
{
- this->send_muc_leave(iid, channel->get_self()->nick, "", true, resource);
+ this->send_muc_leave(iid, channel->get_self()->nick, "", true, true, resource);
}
+ if (persistent)
+ this->remove_resource_from_chan(key, resource);
// Since there are no resources left in that channel, we don't
// want to receive private messages using this room's JID
this->remove_all_preferred_from_jid_of_room(iid.get_local());
@@ -460,8 +467,8 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
{
if (channel && channel->joined)
this->send_muc_leave(iid, channel->get_self()->nick,
- "Biboumi note: "s + std::to_string(resources - 1) + " resources are still in this channel.",
- true, resource);
+ "Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.",
+ true, true, resource);
this->remove_resource_from_chan(key, resource);
if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
this->remove_resource_from_server(iid.get_server(), resource);
@@ -697,12 +704,12 @@ void Bridge::send_xmpp_version_to_irc(const Iid& iid, const std::string& name, c
{
std::string result(name + " " + version + " " + os);
- this->send_private_message(iid, "\01VERSION "s + result + "\01", "NOTICE");
+ this->send_private_message(iid, "\01VERSION " + result + "\01", "NOTICE");
}
void Bridge::send_irc_ping_result(const Iid& iid, const std::string& id)
{
- this->send_private_message(iid, "\01PING "s + utils::revstr(id) + "\01", "NOTICE");
+ this->send_private_message(iid, "\01PING " + utils::revstr(id) + "\01", "NOTICE");
}
void Bridge::send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick,
@@ -851,7 +858,6 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st
}
else
{
- std::string target = std::to_string(iid);
const auto it = this->preferred_user_from.find(iid.get_local());
if (it != this->preferred_user_from.end())
{
@@ -878,16 +884,17 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick,
void Bridge::send_muc_leave(const Iid& iid, const std::string& nick,
const std::string& message, const bool self,
+ const bool user_requested,
const std::string& resource)
{
if (!resource.empty())
this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
- this->user_jid + "/" + resource, self);
+ this->user_jid + "/" + resource, self, user_requested);
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);
+ this->user_jid + "/" + res, self, user_requested);
if (self)
this->remove_all_resources_from_chan(iid.to_tuple());
@@ -918,7 +925,7 @@ void Bridge::send_xmpp_message(const std::string& from, const std::string& autho
if (!author.empty())
{
IrcUser user(author);
- body = "\u000303"s + user.nick + (user.host.empty()?
+ body = "\u000303" + user.nick + (user.host.empty()?
"\u0003: ":
(" (\u000310" + user.host + "\u000303)\u0003: ")) + msg;
}
@@ -1076,6 +1083,16 @@ void Bridge::send_xmpp_invitation(const Iid& iid, const std::string& author)
this->xmpp.send_invitation(std::to_string(iid), this->user_jid + "/" + resource, author);
}
+void Bridge::on_irc_client_connected(const std::string& hostname)
+{
+ this->xmpp.on_irc_client_connected(hostname, this->user_jid);
+}
+
+void Bridge::on_irc_client_disconnected(const std::string& hostname)
+{
+ this->xmpp.on_irc_client_disconnected(hostname, this->user_jid);
+}
+
void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid)
{
auto it = this->preferred_user_from.find(nick);
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index 033291c..c10631b 100644
--- a/src/bridge/bridge.hpp
+++ b/src/bridge/bridge.hpp
@@ -169,7 +169,10 @@ public:
/**
* Send an unavailable presence from this participant
*/
- void send_muc_leave(const Iid& iid, const std::string& nick, const std::string& message, const bool self, const std::string& resource = "");
+ void send_muc_leave(const Iid& iid, const std::string& nick,
+ const std::string& message, const bool self,
+ const bool user_requested,
+ const std::string& resource="");
/**
* 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
@@ -198,6 +201,8 @@ public:
void send_xmpp_ping_request(const std::string& nick, const std::string& hostname,
const std::string& id);
void send_xmpp_invitation(const Iid& iid, const std::string& author);
+ void on_irc_client_connected(const std::string& hostname);
+ void on_irc_client_disconnected(const std::string& hostname);
/**
* Misc
@@ -298,8 +303,8 @@ private:
using ChannelKey = std::tuple<ChannelName, IrcHostname>;
public:
std::map<ChannelKey, std::set<Resource>> resources_in_chan;
-private:
std::map<IrcHostname, std::set<Resource>> resources_in_server;
+private:
/**
* Manage which resource is in which channel
*/
diff --git a/src/database/count_query.hpp b/src/database/count_query.hpp
index b7bbf51..0dde63c 100644
--- a/src/database/count_query.hpp
+++ b/src/database/count_query.hpp
@@ -29,7 +29,6 @@ struct CountQuery: public Query
if (sqlite3_step(statement.get()) != SQLITE_DONE)
log_warning("Count request returned more than one result.");
- log_debug("Returning count: ", res);
return res;
}
};
diff --git a/src/database/database.cpp b/src/database/database.cpp
index 92f7682..85c675e 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -13,6 +13,8 @@ Database::MucLogLineTable Database::muc_log_lines("MucLogLine_");
Database::GlobalOptionsTable Database::global_options("GlobalOptions_");
Database::IrcServerOptionsTable Database::irc_server_options("IrcServerOptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("IrcChannelOptions_");
+Database::RosterTable Database::roster("roster");
+
void Database::open(const std::string& filename)
{
@@ -36,6 +38,8 @@ void Database::open(const std::string& filename)
Database::irc_server_options.upgrade(Database::db);
Database::irc_channel_options.create(Database::db);
Database::irc_channel_options.upgrade(Database::db);
+ Database::roster.create(Database::db);
+ Database::roster.upgrade(Database::db);
}
@@ -177,6 +181,51 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne
return {result.crbegin(), result.crend()};
}
+void Database::add_roster_item(const std::string& local, const std::string& remote)
+{
+ auto roster_item = Database::roster.row();
+
+ roster_item.col<Database::LocalJid>() = local;
+ roster_item.col<Database::RemoteJid>() = remote;
+
+ roster_item.save(Database::db);
+}
+
+void Database::delete_roster_item(const std::string& local, const std::string& remote)
+{
+ Query query("DELETE FROM "s + Database::roster.get_name());
+ query << " WHERE " << Database::RemoteJid{} << "=" << remote << \
+ " AND " << Database::LocalJid{} << "=" << local;
+
+ query.execute(Database::db);
+}
+
+bool Database::has_roster_item(const std::string& local, const std::string& remote)
+{
+ auto query = Database::roster.select();
+ query.where() << Database::LocalJid{} << "=" << local << \
+ " and " << Database::RemoteJid{} << "=" << remote;
+
+ auto res = query.execute(Database::db);
+
+ return !res.empty();
+}
+
+std::vector<Database::RosterItem> Database::get_contact_list(const std::string& local)
+{
+ auto query = Database::roster.select();
+ query.where() << Database::LocalJid{} << "=" << local;
+
+ return query.execute(Database::db);
+}
+
+std::vector<Database::RosterItem> Database::get_full_roster()
+{
+ auto query = Database::roster.select();
+
+ return query.execute(Database::db);
+}
+
void Database::close()
{
sqlite3_close_v2(Database::db);
@@ -192,4 +241,4 @@ std::string Database::gen_uuid()
return uuid_str;
}
-#endif \ No newline at end of file
+#endif
diff --git a/src/database/database.hpp b/src/database/database.hpp
index 28b6b1b..c00c938 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -20,85 +20,67 @@ class Database
public:
using time_point = std::chrono::system_clock::time_point;
- struct Uuid: Column<std::string> { static constexpr auto name = "uuid_";
- static constexpr auto options = ""; };
+ struct Uuid: Column<std::string> { static constexpr auto name = "uuid_"; };
- struct Owner: Column<std::string> { static constexpr auto name = "owner_";
- static constexpr auto options = ""; };
+ struct Owner: Column<std::string> { static constexpr auto name = "owner_"; };
- struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_";
- static constexpr auto options = ""; };
+ struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_"; };
- struct Channel: Column<std::string> { static constexpr auto name = "channel_";
- static constexpr auto options = ""; };
+ struct Channel: Column<std::string> { static constexpr auto name = "channel_"; };
- struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_";
- static constexpr auto options = ""; };
+ struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_"; };
- struct Server: Column<std::string> { static constexpr auto name = "server_";
- static constexpr auto options = ""; };
+ struct Server: Column<std::string> { static constexpr auto name = "server_"; };
- struct Date: Column<time_point::rep> { static constexpr auto name = "date_";
- static constexpr auto options = ""; };
+ struct Date: Column<time_point::rep> { static constexpr auto name = "date_"; };
- struct Body: Column<std::string> { static constexpr auto name = "body_";
- static constexpr auto options = ""; };
+ struct Body: Column<std::string> { static constexpr auto name = "body_"; };
- struct Nick: Column<std::string> { static constexpr auto name = "nick_";
- static constexpr auto options = ""; };
+ struct Nick: Column<std::string> { static constexpr auto name = "nick_"; };
- struct Pass: Column<std::string> { static constexpr auto name = "pass_";
- static constexpr auto options = ""; };
+ struct Pass: Column<std::string> { static constexpr auto name = "pass_"; };
struct Ports: Column<std::string> { static constexpr auto name = "ports_";
- static constexpr auto options = "";
Ports(): Column<std::string>("6667") {} };
struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsPorts_";
- static constexpr auto options = "";
TlsPorts(): Column<std::string>("6697;6670") {} };
- struct Username: Column<std::string> { static constexpr auto name = "username_";
- static constexpr auto options = ""; };
+ struct Username: Column<std::string> { static constexpr auto name = "username_"; };
- struct Realname: Column<std::string> { static constexpr auto name = "realname_";
- static constexpr auto options = ""; };
+ struct Realname: Column<std::string> { static constexpr auto name = "realname_"; };
- struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_";
- static constexpr auto options = ""; };
+ struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_"; };
- struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_";
- static constexpr auto options = ""; };
+ struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_"; };
- struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_";
- static constexpr auto options = ""; };
+ struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_"; };
- struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_";
- static constexpr auto options = ""; };
+ struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_"; };
struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxHistoryLength_";
- static constexpr auto options = "";
MaxHistoryLength(): Column<int>(20) {} };
struct RecordHistory: Column<bool> { static constexpr auto name = "recordHistory_";
- static constexpr auto options = "";
RecordHistory(): Column<bool>(true) {}};
- struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordHistory_";
- static constexpr auto options = ""; };
+ struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordHistory_"; };
struct VerifyCert: Column<bool> { static constexpr auto name = "verifyCert_";
- static constexpr auto options = "";
VerifyCert(): Column<bool>(true) {} };
struct Persistent: Column<bool> { static constexpr auto name = "persistent_";
- static constexpr auto options = "";
Persistent(): Column<bool>(false) {} };
+ struct LocalJid: Column<std::string> { static constexpr auto name = "local"; };
+
+ struct RemoteJid: Column<std::string> { static constexpr auto name = "remote"; };
+
+
using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
using MucLogLine = MucLogLineTable::RowType;
- using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory>;
+ using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, Persistent>;
using GlobalOptions = GlobalOptionsTable::RowType;
using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength>;
@@ -107,6 +89,9 @@ class Database
using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>;
using IrcChannelOptions = IrcChannelOptionsTable::RowType;
+ using RosterTable = Table<LocalJid, RemoteJid>;
+ using RosterItem = RosterTable::RowType;
+
Database() = default;
~Database() = default;
@@ -132,6 +117,12 @@ class Database
static std::string store_muc_message(const std::string& owner, const std::string& chan_name, const std::string& server_name,
time_point date, const std::string& body, const std::string& nick);
+ static void add_roster_item(const std::string& local, const std::string& remote);
+ static bool has_roster_item(const std::string& local, const std::string& remote);
+ static void delete_roster_item(const std::string& local, const std::string& remote);
+ static std::vector<Database::RosterItem> get_contact_list(const std::string& local);
+ static std::vector<Database::RosterItem> get_full_roster();
+
static void close();
static void open(const std::string& filename);
@@ -146,6 +137,7 @@ class Database
static GlobalOptionsTable global_options;
static IrcServerOptionsTable irc_server_options;
static IrcChannelOptionsTable irc_channel_options;
+ static RosterTable roster;
static sqlite3* db;
private:
diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp
index 9e410ce..2ece69d 100644
--- a/src/database/insert_query.hpp
+++ b/src/database/insert_query.hpp
@@ -20,8 +20,6 @@ actual_bind(Statement& statement, std::vector<std::string>& params, const std::t
params.erase(params.begin());
if (sqlite3_bind_text(statement.get(), N + 1, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) != SQLITE_OK)
log_error("Failed to bind ", value, " to param ", N);
- else
- log_debug("Bound (not id) [", value, "] to ", N);
}
template <int N, typename ColumnType, typename... T>
@@ -36,8 +34,6 @@ actual_bind(Statement& statement, std::vector<std::string>&, const std::tuple<T.
}
else if (sqlite3_bind_null(statement.get(), N + 1) != SQLITE_OK)
log_error("Failed to bind NULL to param ", N);
- else
- log_debug("Bound NULL to ", N);
}
struct InsertQuery: public Query
diff --git a/src/database/query.hpp b/src/database/query.hpp
index f103fe9..6e1db12 100644
--- a/src/database/query.hpp
+++ b/src/database/query.hpp
@@ -23,7 +23,6 @@ struct Query
Statement prepare(sqlite3* db)
{
sqlite3_stmt* stmt;
- log_debug(this->body);
auto res = sqlite3_prepare(db, this->body.data(), static_cast<int>(this->body.size()) + 1,
&stmt, nullptr);
if (res != SQLITE_OK)
@@ -36,9 +35,7 @@ struct Query
for (const std::string& param: this->params)
{
if (sqlite3_bind_text(statement.get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
- log_debug("Failed to bind ", param, " to param ", i);
- else
- log_debug("Bound ", param, " to ", i);
+ log_error("Failed to bind ", param, " to param ", i);
i++;
}
diff --git a/src/database/row.hpp b/src/database/row.hpp
index e7a58c4..2b50874 100644
--- a/src/database/row.hpp
+++ b/src/database/row.hpp
@@ -17,9 +17,7 @@ typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>
update_id(std::tuple<T...>& columns, sqlite3* db)
{
auto&& column = std::get<ColumnType>(columns);
- log_debug("Found an autoincrement col.");
auto res = sqlite3_last_insert_rowid(db);
- log_debug("Value is now: ", res);
column.value = static_cast<Id::real_type>(res);
}
@@ -45,7 +43,7 @@ struct Row
{}
template <typename Type>
- auto& col()
+ typename Type::real_type& col()
{
auto&& col = std::get<Type>(this->columns);
return col.value;
@@ -63,7 +61,6 @@ struct Row
InsertQuery query(this->table_name);
query.insert_col_names(this->columns);
query.insert_values(this->columns);
- log_debug(query.body);
query.execute(this->columns, db);
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
index f4d71af..872001c 100644
--- a/src/database/select_query.hpp
+++ b/src/database/select_query.hpp
@@ -79,7 +79,7 @@ struct SelectQuery: public Query
using ColumnsType = std::tuple<T...>;
using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnsType>()))>::type;
- this->body += " "s + ColumnType::name;
+ this->body += " " + std::string{ColumnType::name};
if (N < (sizeof...(T) - 1))
this->body += ", ";
diff --git a/src/database/table.cpp b/src/database/table.cpp
index 5929f33..9224d79 100644
--- a/src/database/table.cpp
+++ b/src/database/table.cpp
@@ -4,12 +4,10 @@ std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string&
{
std::set<std::string> result;
char* errmsg;
- std::string query{"PRAGMA table_info("s + table_name + ")"};
- log_debug(query);
+ std::string query{"PRAGMA table_info(" + table_name + ")"};
int res = sqlite3_exec(db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
constexpr int name_column = 1;
std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
- log_debug("Table has column ", columns[name_column]);
if (name_column < columns_nb)
result->insert(columns[name_column]);
return 0;
diff --git a/src/database/table.hpp b/src/database/table.hpp
index 411ac6a..0060211 100644
--- a/src/database/table.hpp
+++ b/src/database/table.hpp
@@ -2,7 +2,6 @@
#include <database/select_query.hpp>
#include <database/type_to_sql.hpp>
-#include <logger/logger.hpp>
#include <database/row.hpp>
#include <algorithm>
@@ -17,8 +16,7 @@ template <typename ColumnType>
void add_column_to_table(sqlite3* db, const std::string& table_name)
{
const std::string name = ColumnType::name;
- std::string query{"ALTER TABLE "s + table_name + " ADD " + ColumnType::name + " " + TypeToSQLType<typename ColumnType::real_type>::type};
- log_debug(query);
+ std::string query{"ALTER TABLE " + table_name + " ADD " + ColumnType::name + " " + TypeToSQLType<typename ColumnType::real_type>::type};
char* error;
const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
if (result != SQLITE_OK)
@@ -28,6 +26,17 @@ void add_column_to_table(sqlite3* db, const std::string& table_name)
}
}
+
+template <typename ColumnType, decltype(ColumnType::options) = nullptr>
+void append_option(std::string& s)
+{
+ s += " "s + ColumnType::options;
+}
+
+template <typename, typename... Args>
+void append_option(Args&& ...)
+{ }
+
template <typename... T>
class Table
{
@@ -55,11 +64,8 @@ class Table
this->add_column_create(res);
res += ")";
- log_debug(res);
-
char* error;
const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
- log_debug("result: ", +result);
if (result != SQLITE_OK)
{
log_error("Error executing query: ", error);
@@ -110,14 +116,13 @@ class Table
str += ColumnType::name;
str += " ";
str += TypeToSQLType<RealType>::type;
- str += " "s + ColumnType::options;
+ append_option<ColumnType>(str);
if (N != sizeof...(T) - 1)
str += ",";
str += "\n";
add_column_create<N+1>(str);
}
-
template <std::size_t N=0>
typename std::enable_if<N == sizeof...(T), void>::type
add_column_create(std::string&)
diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp
index a63a1c3..131c18c 100644
--- a/src/irc/iid.cpp
+++ b/src/irc/iid.cpp
@@ -35,7 +35,8 @@ Iid::Iid(const std::string& iid, const Bridge *bridge)
void Iid::set_type(const std::set<char>& chantypes)
{
- if (this->local.empty() && this->server.empty())
+ if (this->local.empty() && (
+ !Config::get("fixed_irc_server", "").empty() || this->server.empty()))
this->type = Iid::Type::None;
if (this->local.empty())
return;
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index bacb89e..46dbdbe 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -178,7 +178,7 @@ IrcClient::~IrcClient()
{
// This event may or may not exist (if we never got connected, it
// doesn't), but it's ok
- TimedEventsManager::instance().cancel("PING"s + this->hostname + this->bridge.get_jid());
+ TimedEventsManager::instance().cancel("PING" + this->hostname + this->bridge.get_jid());
}
void IrcClient::start()
@@ -194,7 +194,7 @@ 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 "s +
+ this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " +
this->hostname + ":" + port + " (" +
(tls ? "encrypted" : "not encrypted") + ")");
@@ -213,7 +213,7 @@ void IrcClient::start()
void IrcClient::on_connection_failed(const std::string& reason)
{
this->bridge.send_xmpp_message(this->hostname, "",
- "Connection failed: "s + reason);
+ "Connection failed: " + reason);
if (this->hostname_resolution_failed)
while (!this->ports_to_try.empty())
@@ -260,7 +260,7 @@ void IrcClient::on_connected()
{
if (this->is_connected())
{
- this->on_connection_close("Could not resolve hostname "s + this->user_hostname +
+ this->on_connection_close("Could not resolve hostname " + this->user_hostname +
": " + error_msg);
this->send_quit_command("");
}
@@ -297,6 +297,7 @@ void IrcClient::on_connected()
#endif
this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
this->send_pending_data();
+ this->bridge.on_irc_client_connected(this->get_hostname());
}
void IrcClient::on_connection_close(const std::string& error_msg)
@@ -309,6 +310,7 @@ void IrcClient::on_connection_close(const std::string& error_msg)
const IrcMessage error{"ERROR", {message}};
this->on_error(error);
log_warning(message);
+ this->bridge.on_irc_client_disconnected(this->get_hostname());
}
IrcChannel* IrcClient::get_channel(const std::string& n)
@@ -585,7 +587,7 @@ void IrcClient::on_notice(const IrcMessage& message)
// The notice was directed at a channel we are in. Modify the message
// to indicate that it is a notice, and make it a MUC message coming
// from the MUC JID
- IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 "s + body});
+ IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 " + body});
this->on_channel_message(modified_message);
}
}
@@ -697,7 +699,7 @@ void IrcClient::on_channel_message(const IrcMessage& message)
{
if (body.substr(1, 6) == "ACTION")
this->bridge.send_message(iid, nick,
- "/me"s + body.substr(7, body.size() - 8), muc);
+ "/me" + body.substr(7, body.size() - 8), muc);
else if (body.substr(1, 8) == "VERSION\01")
this->bridge.send_iq_version_request(nick, this->hostname);
else if (body.substr(1, 5) == "PING ")
@@ -899,7 +901,7 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
#endif
// Install a repeated events to regularly send a PING
TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
- "PING"s + this->hostname + this->bridge.get_jid()));
+ "PING" + this->hostname + this->bridge.get_jid()));
std::string channels{};
std::string channels_with_key{};
std::string keys{};
@@ -981,7 +983,7 @@ void IrcClient::on_part(const IrcMessage& message)
// channel pointer is now invalid
channel = nullptr;
}
- this->bridge.send_muc_leave(iid, std::move(nick), txt, self);
+ this->bridge.send_muc_leave(iid, std::move(nick), txt, self, true);
}
}
@@ -999,10 +1001,10 @@ void IrcClient::on_error(const IrcMessage& message)
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);
+ this->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true, false);
}
this->channels.clear();
- this->send_gateway_message("ERROR: "s + leave_message);
+ this->send_gateway_message("ERROR: " + leave_message);
}
void IrcClient::on_quit(const IrcMessage& message)
@@ -1015,6 +1017,9 @@ void IrcClient::on_quit(const IrcMessage& message)
const std::string& chan_name = pair.first;
IrcChannel* channel = pair.second.get();
const IrcUser* user = channel->find_user(message.prefix);
+ bool self = false;
+ if (user == channel->get_self())
+ self = true;
if (user)
{
std::string nick = user->nick;
@@ -1023,7 +1028,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, std::move(nick), txt, false);
+ this->bridge.send_muc_leave(iid, std::move(nick), txt, self, false);
}
}
}
@@ -1135,7 +1140,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message)
mode_arguments += message.arguments[i];
}
}
- this->bridge.send_message(iid, "", "Mode "s + iid.get_local() +
+ this->bridge.send_message(iid, "", "Mode " + iid.get_local() +
" [" + mode_arguments + "] by " + user.nick,
true);
const IrcChannel* channel = this->get_channel(iid.get_local());
@@ -1213,7 +1218,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message)
void IrcClient::on_user_mode(const IrcMessage& message)
{
this->bridge.send_xmpp_message(this->hostname, "",
- "User mode for "s + message.arguments[0] +
+ "User mode for " + message.arguments[0] +
" is [" + message.arguments[1] + "]");
}
@@ -1252,7 +1257,7 @@ void IrcClient::leave_dummy_channel(const std::string& exit_message, const std::
this->dummy_channel.joined = false;
this->dummy_channel.joining = false;
this->dummy_channel.remove_all_users();
- this->bridge.send_muc_leave(Iid("%"s + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, resource);
+ this->bridge.send_muc_leave(Iid("%" + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, true, resource);
}
#ifdef BOTAN_FOUND
diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp
index f93a366..7f07cef 100644
--- a/src/network/credentials_manager.cpp
+++ b/src/network/credentials_manager.cpp
@@ -54,29 +54,6 @@ void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
std::rethrow_exception(exc);
}
-#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
-void BasicCredentialsManager::verify_certificate_chain(const std::string& type,
- const std::string& purported_hostname,
- const std::vector<Botan::X509_Certificate>& certs)
-{
- log_debug("Checking remote certificate (", type, ") for hostname ", purported_hostname);
- try
- {
- Botan::Credentials_Manager::verify_certificate_chain(type, purported_hostname, certs);
- log_debug("Certificate is valid");
- }
- catch (const std::exception& tls_exception)
- {
- log_warning("TLS certificate check failed: ", tls_exception.what());
- std::exception_ptr exception_ptr{};
- if (this->socket_handler->abort_on_invalid_cert())
- exception_ptr = std::current_exception();
-
- check_tls_certificate(certs, purported_hostname, this->trusted_fingerprint, exception_ptr);
- }
-}
-#endif
-
bool BasicCredentialsManager::try_to_open_one_ca_bundle(const std::vector<std::string>& paths)
{
for (const auto& path: paths)
diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp
index e7c247d..aa4732a 100644
--- a/src/network/credentials_manager.hpp
+++ b/src/network/credentials_manager.hpp
@@ -31,11 +31,6 @@ public:
BasicCredentialsManager& operator=(const BasicCredentialsManager&) = delete;
BasicCredentialsManager& operator=(BasicCredentialsManager&&) = delete;
-#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
- void verify_certificate_chain(const std::string& type,
- const std::string& purported_hostname,
- const std::vector<Botan::X509_Certificate>&) override final;
-#endif
std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string& type,
const std::string& context) override final;
void set_trusted_fingerprint(const std::string& fingerprint);
diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp
index 35f2446..aac13d0 100644
--- a/src/network/tcp_client_socket_handler.cpp
+++ b/src/network/tcp_client_socket_handler.cpp
@@ -146,7 +146,7 @@ void TCPClientSocketHandler::connect(const std::string& address, const std::stri
|| errno == EISCONN)
{
log_info("Connection success.");
- TimedEventsManager::instance().cancel("connection_timeout"s +
+ TimedEventsManager::instance().cancel("connection_timeout" +
std::to_string(this->socket));
this->poller->add_socket_handler(this);
this->connected = true;
@@ -196,7 +196,7 @@ void TCPClientSocketHandler::connect(const std::string& address, const std::stri
TimedEventsManager::instance().add_event(
TimedEvent(std::chrono::steady_clock::now() + 5s,
std::bind(&TCPClientSocketHandler::on_connection_timeout, this),
- "connection_timeout"s + std::to_string(this->socket)));
+ "connection_timeout" + std::to_string(this->socket)));
return ;
}
log_info("Connection failed:", std::strerror(errno));
@@ -220,7 +220,7 @@ void TCPClientSocketHandler::connect()
void TCPClientSocketHandler::close()
{
- TimedEventsManager::instance().cancel("connection_timeout"s +
+ TimedEventsManager::instance().cancel("connection_timeout" +
std::to_string(this->socket));
TCPSocketHandler::close();
diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp
index 1049375..6239162 100644
--- a/src/network/tcp_socket_handler.cpp
+++ b/src/network/tcp_socket_handler.cpp
@@ -237,14 +237,7 @@ void TCPSocketHandler::start_tls(const std::string& address, const std::string&
this->policy.load(policy_directory + "policy.txt");
this->policy.load(policy_directory + address + ".policy.txt");
this->tls = std::make_unique<Botan::TLS::Client>(
-# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
*this,
-# else
- [this](const Botan::byte* data, size_t size) { this->tls_emit_data(data, size); },
- [this](const Botan::byte* data, size_t size) { this->tls_record_received(0, data, size); },
- [this](Botan::TLS::Alert alert, const Botan::byte*, size_t) { this->tls_alert(alert); },
- [this](const Botan::TLS::Session& session) { return this->tls_session_established(session); },
-# endif
get_session_manager(), this->credential_manager, this->policy,
get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version());
}
@@ -327,7 +320,6 @@ bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& sessio
return true;
}
-#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,34)
void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& trusted_roots,
@@ -350,7 +342,6 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certi
check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr);
}
}
-#endif
void TCPSocketHandler::on_tls_activated()
{
diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp
index f68698e..5cef739 100644
--- a/src/network/tcp_socket_handler.hpp
+++ b/src/network/tcp_socket_handler.hpp
@@ -25,22 +25,14 @@
# include <botan/tls_session_manager.h>
# include <network/tls_policy.hpp>
-# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
-# define BOTAN_TLS_CALLBACKS_OVERRIDE override final
-# else
-# define BOTAN_TLS_CALLBACKS_OVERRIDE
-# endif
#endif
-
/**
* Does all the read/write, buffering etc. With optional tls.
* But doesn’t do any connect() or accept() or anything else.
*/
class TCPSocketHandler: public SocketHandler
#ifdef BOTAN_FOUND
-# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
,public Botan::TLS::Callbacks
-# endif
#endif
{
protected:
@@ -146,31 +138,29 @@ private:
* Called by the tls object that some data has been decrypt. We call
* parse_in_buffer() to handle that unencrypted data.
*/
- void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) override final;
/**
* Called by the tls object to indicate that some data has been encrypted
* and is now ready to be sent on the socket as is.
*/
- void tls_emit_data(const Botan::byte* data, size_t size) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ void tls_emit_data(const Botan::byte* data, size_t size) override final;
/**
* Called by the tls object to indicate that a TLS alert has been
* received. We don’t use it, we just log some message, at the moment.
*/
- void tls_alert(Botan::TLS::Alert alert) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ void tls_alert(Botan::TLS::Alert alert) override final;
/**
* Called by the tls object at the end of the TLS handshake. We don't do
* anything here appart from logging the TLS session information.
*/
- bool tls_session_established(const Botan::TLS::Session& session) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ bool tls_session_established(const Botan::TLS::Session& session) override final;
-#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,34)
void tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& trusted_roots,
Botan::Usage_Type usage,
const std::string& hostname,
- const Botan::TLS::Policy& policy) BOTAN_TLS_CALLBACKS_OVERRIDE;
-#endif
+ const Botan::TLS::Policy& policy) override final;
/**
* Called whenever the tls session goes from inactive to active. This
* means that the handshake has just been successfully done, and we can
diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp
index 2e6efc2..1a0d185 100644
--- a/src/utils/sha1.cpp
+++ b/src/utils/sha1.cpp
@@ -18,13 +18,7 @@
std::string sha1(const std::string& input)
{
#ifdef BOTAN_FOUND
-# if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
- auto sha1 = Botan::HashFunction::create("SHA-1");
- if (!sha1)
- throw Botan::Algorithm_Not_Found("SHA-1");
-# else
auto sha1 = Botan::HashFunction::create_or_throw("SHA-1");
-# endif
sha1->update(input);
return Botan::hex_encode(sha1->final(), false);
#endif
diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp
index e02bf35..fbf4ce2 100644
--- a/src/xmpp/adhoc_command.cpp
+++ b/src/xmpp/adhoc_command.cpp
@@ -59,7 +59,7 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
command_node.delete_all_children();
XmlSubNode note(command_node, "note");
note["type"] = "info";
- note.set_inner("Hello "s + value_str + "!"s);
+ note.set_inner("Hello " + value_str + "!"s);
return;
}
}
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
index 040d0ff..e4dcd5c 100644
--- a/src/xmpp/adhoc_commands_handler.cpp
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -19,7 +19,7 @@ void AdhocCommandsHandler::add_command(std::string name, AdhocCommand command)
{
const auto found = this->commands.find(name);
if (found != this->commands.end())
- throw std::runtime_error("Trying to add an ad-hoc command that already exist: "s + name);
+ throw std::runtime_error("Trying to add an ad-hoc command that already exist: " + name);
this->commands.emplace(std::make_pair(std::move(name), std::move(command)));
}
@@ -59,7 +59,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
std::forward_as_tuple(command_it->second, executor_jid, to));
TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 3600s,
std::bind(&AdhocCommandsHandler::remove_session, this, sessionid, executor_jid),
- "adhocsession"s + sessionid + executor_jid));
+ "adhocsession" + sessionid + executor_jid));
}
auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
if ((session_it != this->sessions.end()) &&
@@ -74,7 +74,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
{
this->sessions.erase(session_it);
command_node["status"] = "completed";
- TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ TimedEventsManager::instance().cancel("adhocsession" + sessionid + executor_jid);
}
else
{
@@ -87,7 +87,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
{
this->sessions.erase(session_it);
command_node["status"] = "canceled";
- TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ TimedEventsManager::instance().cancel("adhocsession" + sessionid + executor_jid);
}
else // unsupported action
{
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index ad4faf8..60af506 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -122,30 +122,48 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
XmlSubNode instructions(x, "instructions");
instructions.set_inner("Edit the form, to configure your global settings for the component.");
- XmlSubNode max_histo_length(x, "field");
- max_histo_length["var"] = "max_history_length";
- max_histo_length["type"] = "text-single";
- max_histo_length["label"] = "Max history length";
- max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel";
-
{
- XmlSubNode value(max_histo_length, "value");
- value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
+ XmlSubNode max_histo_length(x, "field");
+ max_histo_length["var"] = "max_history_length";
+ max_histo_length["type"] = "text-single";
+ max_histo_length["label"] = "Max history length";
+ max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel";
+ {
+ XmlSubNode value(max_histo_length, "value");
+ value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
+ }
}
- XmlSubNode record_history(x, "field");
- record_history["var"] = "record_history";
- record_history["type"] = "boolean";
- record_history["label"] = "Record history";
- record_history["desc"] = "Whether to save the messages into the database, or not";
+ {
+ XmlSubNode record_history(x, "field");
+ record_history["var"] = "record_history";
+ record_history["type"] = "boolean";
+ record_history["label"] = "Record history";
+ record_history["desc"] = "Whether to save the messages into the database, or not";
+ {
+ XmlSubNode value(record_history, "value");
+ value.set_name("value");
+ if (options.col<Database::RecordHistory>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
+ }
+ }
{
- XmlSubNode value(record_history, "value");
- value.set_name("value");
- if (options.col<Database::RecordHistory>())
- value.set_inner("true");
- else
- value.set_inner("false");
+ XmlSubNode persistent(x, "field");
+ persistent["var"] = "persistent";
+ persistent["type"] = "boolean";
+ persistent["label"] = "Make all channels persistent";
+ persistent["desc"] = "If true, all channels will be persistent";
+ {
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
+ }
}
}
@@ -173,6 +191,9 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
if (bridge)
bridge->set_record_history(options.col<Database::RecordHistory>());
}
+ else if (field->get_tag("var") == "persistent" &&
+ value)
+ options.col<Database::Persistent>() = to_bool(value->get_inner());
}
options.save(Database::db);
@@ -202,100 +223,116 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
XmlSubNode title(x, "title");
- title.set_inner("Configure the IRC server "s + server_domain);
+ title.set_inner("Configure the IRC server " + server_domain);
XmlSubNode instructions(x, "instructions");
- instructions.set_inner("Edit the form, to configure the settings of the IRC server "s + server_domain);
-
- XmlSubNode ports(x, "field");
- ports["var"] = "ports";
- ports["type"] = "text-multi";
- ports["label"] = "Ports";
- ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
- for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
- {
- XmlSubNode ports_value(ports, "value");
- ports_value.set_inner(val);
- }
+ instructions.set_inner("Edit the form, to configure the settings of the IRC server " + server_domain);
+
+ {
+ XmlSubNode ports(x, "field");
+ ports["var"] = "ports";
+ ports["type"] = "text-multi";
+ ports["label"] = "Ports";
+ ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
+ {
+ XmlSubNode ports_value(ports, "value");
+ ports_value.set_inner(val);
+ }
+ }
#ifdef BOTAN_FOUND
- XmlSubNode tls_ports(x, "field");
- tls_ports["var"] = "tls_ports";
- tls_ports["type"] = "text-multi";
- tls_ports["label"] = "TLS ports";
- tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
- for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
- {
- XmlSubNode tls_ports_value(tls_ports, "value");
- tls_ports_value.set_inner(val);
- }
+ {
+ XmlSubNode tls_ports(x, "field");
+ tls_ports["var"] = "tls_ports";
+ tls_ports["type"] = "text-multi";
+ tls_ports["label"] = "TLS ports";
+ tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
+ {
+ XmlSubNode tls_ports_value(tls_ports, "value");
+ tls_ports_value.set_inner(val);
+ }
+ }
- XmlSubNode verify_cert(x, "field");
- verify_cert["var"] = "verify_cert";
- verify_cert["type"] = "boolean";
- verify_cert["label"] = "Verify certificate";
- verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
- XmlSubNode verify_cert_value(verify_cert, "value");
- if (options.col<Database::VerifyCert>())
- verify_cert_value.set_inner("true");
- else
- verify_cert_value.set_inner("false");
+ {
+ XmlSubNode verify_cert(x, "field");
+ verify_cert["var"] = "verify_cert";
+ verify_cert["type"] = "boolean";
+ verify_cert["label"] = "Verify certificate";
+ verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
+ XmlSubNode verify_cert_value(verify_cert, "value");
+ if (options.col<Database::VerifyCert>())
+ verify_cert_value.set_inner("true");
+ else
+ verify_cert_value.set_inner("false");
+ }
- XmlSubNode fingerprint(x, "field");
- fingerprint["var"] = "fingerprint";
- fingerprint["type"] = "text-single";
- fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
- if (!options.col<Database::TrustedFingerprint>().empty())
- {
- XmlSubNode fingerprint_value(fingerprint, "value");
- fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>());
- }
+ {
+ XmlSubNode fingerprint(x, "field");
+ fingerprint["var"] = "fingerprint";
+ fingerprint["type"] = "text-single";
+ fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
+ if (!options.col<Database::TrustedFingerprint>().empty())
+ {
+ XmlSubNode fingerprint_value(fingerprint, "value");
+ fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>());
+ }
+ }
#endif
+ {
+ XmlSubNode pass(x, "field");
+ pass["var"] = "pass";
+ pass["type"] = "text-private";
+ pass["label"] = "Server password";
+ pass["desc"] = "Will be used in a PASS command when connecting";
+ if (!options.col<Database::Pass>().empty())
+ {
+ XmlSubNode pass_value(pass, "value");
+ pass_value.set_inner(options.col<Database::Pass>());
+ }
+ }
- XmlSubNode pass(x, "field");
- pass["var"] = "pass";
- pass["type"] = "text-private";
- pass["label"] = "Server password";
- pass["desc"] = "Will be used in a PASS command when connecting";
- if (!options.col<Database::Pass>().empty())
- {
- XmlSubNode pass_value(pass, "value");
- pass_value.set_inner(options.col<Database::Pass>());
- }
-
- 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())
- {
- XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
- after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
- }
+ {
+ 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())
+ {
+ XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
+ after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
+ }
+ }
if (Config::get("realname_customization", "true") == "true")
{
- XmlSubNode username(x, "field");
- username["var"] = "username";
- username["type"] = "text-single";
- username["label"] = "Username";
- if (!options.col<Database::Username>().empty())
- {
- XmlSubNode username_value(username, "value");
- username_value.set_inner(options.col<Database::Username>());
- }
-
- XmlSubNode realname(x, "field");
- realname["var"] = "realname";
- realname["type"] = "text-single";
- realname["label"] = "Realname";
- if (!options.col<Database::Realname>().empty())
- {
- XmlSubNode realname_value(realname, "value");
- realname_value.set_inner(options.col<Database::Realname>());
- }
+ {
+ XmlSubNode username(x, "field");
+ username["var"] = "username";
+ username["type"] = "text-single";
+ username["label"] = "Username";
+ if (!options.col<Database::Username>().empty())
+ {
+ XmlSubNode username_value(username, "value");
+ username_value.set_inner(options.col<Database::Username>());
+ }
+ }
+
+ {
+ XmlSubNode realname(x, "field");
+ realname["var"] = "realname";
+ realname["type"] = "text-single";
+ realname["label"] = "Realname";
+ if (!options.col<Database::Realname>().empty())
+ {
+ XmlSubNode realname_value(realname, "value");
+ realname_value.set_inner(options.col<Database::Realname>());
+ }
+ }
}
+ {
XmlSubNode encoding_out(x, "field");
encoding_out["var"] = "encoding_out";
encoding_out["type"] = "text-single";
@@ -306,17 +343,20 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode encoding_out_value(encoding_out, "value");
encoding_out_value.set_inner(options.col<Database::EncodingOut>());
}
+ }
- XmlSubNode encoding_in(x, "field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
- encoding_in["label"] = "In encoding";
- if (!options.col<Database::EncodingIn>().empty())
- {
- XmlSubNode encoding_in_value(encoding_in, "value");
- encoding_in_value.set_inner(options.col<Database::EncodingIn>());
- }
+ {
+ XmlSubNode encoding_in(x, "field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
+ encoding_in["label"] = "In encoding";
+ if (!options.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
}
void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
@@ -359,24 +399,20 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
options.col<Database::VerifyCert>() = val;
}
- else if (field->get_tag("var") == "fingerprint" && value &&
- !value->get_inner().empty())
+ else if (field->get_tag("var") == "fingerprint" && value)
{
options.col<Database::TrustedFingerprint>() = value->get_inner();
}
#endif // BOTAN_FOUND
- else if (field->get_tag("var") == "pass" &&
- value && !value->get_inner().empty())
+ 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 && !value->get_inner().empty())
+ else if (field->get_tag("var") == "after_connect_command")
options.col<Database::AfterConnectionCommand>() = value->get_inner();
- else if (field->get_tag("var") == "username" &&
- value && !value->get_inner().empty())
+ else if (field->get_tag("var") == "username" && value)
{
auto username = value->get_inner();
// The username must not contain spaces
@@ -384,16 +420,13 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
options.col<Database::Username>() = username;
}
- else if (field->get_tag("var") == "realname" &&
- value && !value->get_inner().empty())
+ else if (field->get_tag("var") == "realname" && value)
options.col<Database::Realname>() = value->get_inner();
- else if (field->get_tag("var") == "encoding_out" &&
- value && !value->get_inner().empty())
+ else if (field->get_tag("var") == "encoding_out" && value)
options.col<Database::EncodingOut>() = value->get_inner();
- else if (field->get_tag("var") == "encoding_in" &&
- value && !value->get_inner().empty())
+ else if (field->get_tag("var") == "encoding_in" && value)
options.col<Database::EncodingIn>() = value->get_inner();
}
@@ -426,68 +459,74 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain,
iid.get_server(), iid.get_local());
-
XmlSubNode x(node, "jabber:x:data:x");
x["type"] = "form";
XmlSubNode title(x, "title");
- title.set_inner("Configure the IRC channel "s + iid.get_local() + " on server "s + iid.get_server());
+ title.set_inner("Configure the IRC channel " + iid.get_local() + " on server " + iid.get_server());
XmlSubNode instructions(x, "instructions");
- instructions.set_inner("Edit the form, to configure the settings of the IRC channel "s + iid.get_local());
-
- XmlSubNode record_history(x, "field");
- record_history["var"] = "record_history";
- record_history["type"] = "list-single";
- record_history["label"] = "Record history for this channel";
- record_history["desc"] = "If unset, the value is the one configured globally";
+ instructions.set_inner("Edit the form, to configure the settings of the IRC channel " + iid.get_local());
{
- // Value selected by default
- XmlSubNode value(record_history, "value");
- value.set_inner(options.col<Database::RecordHistoryOptional>().to_string());
+ XmlSubNode record_history(x, "field");
+ record_history["var"] = "record_history";
+ record_history["type"] = "list-single";
+ record_history["label"] = "Record history for this channel";
+ record_history["desc"] = "If unset, the value is the one configured globally";
+ {
+ // Value selected by default
+ XmlSubNode value(record_history, "value");
+ value.set_inner(options.col<Database::RecordHistoryOptional>().to_string());
+ }
+ // All three possible values
+ for (const auto& val: {"unset", "true", "false"})
+ {
+ XmlSubNode option(record_history, "option");
+ option["label"] = val;
+ XmlSubNode value(option, "value");
+ value.set_inner(val);
+ }
}
- // All three possible values
- for (const auto& val: {"unset", "true", "false"})
+
{
- XmlSubNode option(record_history, "option");
- option["label"] = val;
- XmlSubNode value(option, "value");
- value.set_inner(val);
+ XmlSubNode encoding_out(x, "field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
+ encoding_out["label"] = "Out encoding";
+ if (!options.col<Database::EncodingOut>().empty())
+ {
+ XmlSubNode encoding_out_value(encoding_out, "value");
+ encoding_out_value.set_inner(options.col<Database::EncodingOut>());
+ }
}
- XmlSubNode encoding_out(x, "field");
- encoding_out["var"] = "encoding_out";
- encoding_out["type"] = "text-single";
- encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
- encoding_out["label"] = "Out encoding";
- if (!options.col<Database::EncodingOut>().empty())
- {
- XmlSubNode encoding_out_value(encoding_out, "value");
- encoding_out_value.set_inner(options.col<Database::EncodingOut>());
- }
+ {
+ XmlSubNode encoding_in(x, "field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
+ encoding_in["label"] = "In encoding";
+ if (!options.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
- XmlSubNode encoding_in(x, "field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
- encoding_in["label"] = "In encoding";
- if (!options.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode persistent(x, "field");
+ persistent["var"] = "persistent";
+ persistent["type"] = "boolean";
+ persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command.";
+ persistent["label"] = "Persistent";
{
- XmlSubNode encoding_in_value(encoding_in, "value");
- encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
}
-
- XmlSubNode persistent(x, "field");
- persistent["var"] = "persistent";
- persistent["type"] = "boolean";
- persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command.";
- persistent["label"] = "Persistent";
- {
- XmlSubNode value(persistent, "value");
- value.set_name("value");
- if (options.col<Database::Persistent>())
- value.set_inner("true");
- else
- value.set_inner("false");
}
}
@@ -526,16 +565,13 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
{
const XmlNode *value = field->get_child("value", "jabber:x:data");
- if (field->get_tag("var") == "encoding_out" &&
- value && !value->get_inner().empty())
+ if (field->get_tag("var") == "encoding_out" && value)
options.col<Database::EncodingOut>() = value->get_inner();
- else if (field->get_tag("var") == "encoding_in" &&
- value && !value->get_inner().empty())
+ else if (field->get_tag("var") == "encoding_in" && value)
options.col<Database::EncodingIn>() = value->get_inner();
- else if (field->get_tag("var") == "persistent" &&
- value)
+ else if (field->get_tag("var") == "persistent" && value)
options.col<Database::Persistent>() = to_bool(value->get_inner());
else if (field->get_tag("var") == "record_history" &&
value && !value->get_inner().empty())
@@ -647,7 +683,7 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession&
{
XmlSubNode note(command_node, "note");
note["type"] = "info";
- note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server.");
+ note.set_inner("User " + jid_to_disconnect + " is not connected to any IRC server.");
session.terminate();
return ;
}
@@ -751,7 +787,7 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session,
IrcClient* irc = bridge->find_irc_client(hostname);
if (!irc || !irc->is_connected())
{
- message = "You are not connected to the IRC server "s + hostname;
+ message = "You are not connected to the IRC server " + hostname;
return;
}
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index 32f3968..0e1d270 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -83,6 +83,14 @@ void BiboumiComponent::shutdown()
{
for (auto& pair: this->bridges)
pair.second->shutdown("Gateway shutdown");
+#ifdef USE_DATABASE
+ for (const Database::RosterItem& roster_item: Database::get_full_roster())
+ {
+ this->send_presence_to_contact(roster_item.col<Database::LocalJid>(),
+ roster_item.col<Database::RemoteJid>(),
+ "unavailable");
+ }
+#endif
}
void BiboumiComponent::clean()
@@ -160,16 +168,50 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
{
if (type == "subscribe")
{ // Auto-accept any subscription request for an IRC server
- this->accept_subscription(to_str, from.bare());
- this->ask_subscription(to_str, from.bare());
+ this->send_presence_to_contact(to_str, from.bare(), "subscribed", id);
+ if (iid.type == Iid::Type::None || bridge->find_irc_client(iid.get_server()))
+ this->send_presence_to_contact(to_str, from.bare(), "");
+ this->send_presence_to_contact(to_str, from.bare(), "subscribe");
+#ifdef USE_DATABASE
+ if (!Database::has_roster_item(to_str, from.bare()))
+ Database::add_roster_item(to_str, from.bare());
+#endif
+ }
+ else if (type == "unsubscribe")
+ {
+ this->send_presence_to_contact(to_str, from.bare(), "unavailable", id);
+ this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
+ this->send_presence_to_contact(to_str, from.bare(), "unsubscribe");
+#ifdef USE_DATABASE
+ const bool res = Database::has_roster_item(to_str, from.bare());
+ if (res)
+ Database::delete_roster_item(to_str, from.bare());
+#endif
+ }
+ else if (type == "probe")
+ {
+ if ((iid.type == Iid::Type::Server && bridge->find_irc_client(iid.get_server()))
+ || iid.type == Iid::Type::None)
+ {
+#ifdef USE_DATABASE
+ if (Database::has_roster_item(to_str, from.bare()))
+#endif
+ this->send_presence_to_contact(to_str, from.bare(), "");
+#ifdef USE_DATABASE
+ else // rfc 6121 4.3.2.1
+ this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
+#endif
+ }
+ }
+ else if (type.empty())
+ { // We just receive a presence from someone (as the result of a probe,
+ // or a directed presence, or a normal presence change)
+ if (iid.type == Iid::Type::None)
+ this->send_presence_to_contact(to_str, from.bare(), "");
}
-
}
- else
- {
- // A user wants to join an invalid IRC channel, return a presence error to him/her
- if (type.empty())
- this->send_invalid_room_error(to.local, to.resource, from_str);
+ else if (iid.type == Iid::Type::User)
+ { // Do nothing yet
}
}
catch (const IRCNotConnected& ex)
@@ -177,7 +219,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
if (type != "unavailable")
this->send_stanza_error("presence", from_str, to_str, id,
"cancel", "remote-server-not-found",
- "Not connected to IRC server "s + ex.hostname,
+ "Not connected to IRC server " + ex.hostname,
true);
}
stanza_error.disable();
@@ -268,7 +310,11 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
const auto invite_to = invite->get_tag("to");
if (!invite_to.empty())
{
- bridge->send_irc_invitation(iid, invite_to);
+ Jid invited_jid{invite_to};
+ if (invited_jid.domain == this->get_served_hostname() || invited_jid.local.empty())
+ bridge->send_irc_invitation(iid, invite_to);
+ else
+ this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str);
}
}
@@ -277,7 +323,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
{
this->send_stanza_error("message", from_str, to_str, id,
"cancel", "remote-server-not-found",
- "Not connected to IRC server "s + ex.hostname,
+ "Not connected to IRC server " + ex.hostname,
true);
}
stanza_error.disable();
@@ -586,7 +632,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
{
this->send_stanza_error("iq", from, to_str, id,
"cancel", "remote-server-not-found",
- "Not connected to IRC server "s + ex.hostname,
+ "Not connected to IRC server " + ex.hostname,
true);
stanza_error.disable();
return;
@@ -806,7 +852,7 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s
XmlSubNode identity(query, "identity");
identity["category"] = "conference";
identity["type"] = "irc";
- identity["name"] = "IRC server "s + from.local + " over Biboumi";
+ 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})
{
XmlSubNode feature(query, "feature");
@@ -849,7 +895,7 @@ 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 "s + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
+ 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})
{
XmlSubNode feature(query, "feature");
@@ -945,6 +991,16 @@ void BiboumiComponent::send_invitation(const std::string& room_target,
const std::string& jid_to,
const std::string& author_nick)
{
+ if (author_nick.empty())
+ this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname);
+ else
+ this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname + "/" + author_nick);
+}
+
+void BiboumiComponent::send_invitation_from_fulljid(const std::string& room_target,
+ const std::string& jid_to,
+ const std::string& from)
+{
Stanza message("message");
{
message["from"] = room_target + "@" + this->served_hostname;
@@ -952,10 +1008,7 @@ void BiboumiComponent::send_invitation(const std::string& room_target,
XmlSubNode x(message, "x");
x["xmlns"] = MUC_USER_NS;
XmlSubNode invite(x, "invite");
- if (author_nick.empty())
- invite["from"] = room_target + "@" + this->served_hostname;
- else
- invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick;
+ invite["from"] = from;
}
this->send_stanza(message);
}
@@ -979,3 +1032,55 @@ void BiboumiComponent::ask_subscription(const std::string& from, const std::stri
presence["type"] = "subscribe";
this->send_stanza(presence);
}
+
+void BiboumiComponent::send_presence_to_contact(const std::string& from, const std::string& to,
+ const std::string& type, const std::string& id)
+{
+ Stanza presence("presence");
+ presence["from"] = from;
+ presence["to"] = to;
+ if (!type.empty())
+ presence["type"] = type;
+ if (!id.empty())
+ presence["id"] = id;
+ this->send_stanza(presence);
+}
+
+void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname, const std::string& jid)
+{
+#ifdef USE_DATABASE
+ const auto local_jid = irc_hostname + "@" + this->served_hostname;
+ if (Database::has_roster_item(local_jid, jid))
+ this->send_presence_to_contact(local_jid, jid, "");
+#endif
+}
+
+void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid)
+{
+#ifdef USE_DATABASE
+ const auto local_jid = irc_hostname + "@" + this->served_hostname;
+ if (Database::has_roster_item(local_jid, jid))
+ this->send_presence_to_contact(irc_hostname + "@" + this->served_hostname, jid, "unavailable");
+#endif
+}
+
+void BiboumiComponent::after_handshake()
+{
+ XmppComponent::after_handshake();
+
+#ifdef USE_DATABASE
+ const auto contacts = Database::get_contact_list(this->get_served_hostname());
+
+ for (const Database::RosterItem& roster_item: contacts)
+ {
+ const auto remote_jid = roster_item.col<Database::RemoteJid>();
+ // In response, we will receive a presence indicating the
+ // contact is online, to which we will respond with our own
+ // presence.
+ // If the contact removed us from their roster while we were
+ // offline, we will receive an unsubscribed presence, letting us
+ // stay in sync.
+ this->send_presence_to_contact(this->get_served_hostname(), remote_jid, "probe");
+ }
+#endif
+}
diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp
index 87311f9..caf990e 100644
--- a/src/xmpp/biboumi_component.hpp
+++ b/src/xmpp/biboumi_component.hpp
@@ -36,6 +36,8 @@ public:
BiboumiComponent& operator=(const BiboumiComponent&) = delete;
BiboumiComponent& operator=(BiboumiComponent&&) = delete;
+ void after_handshake() override final;
+
/**
* Returns the bridge for the given user. If it does not exist, return
* nullptr.
@@ -85,8 +87,15 @@ public:
const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin,
std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info);
void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick);
+private:
+ void send_invitation_from_fulljid(const std::string& room_target, const std::string& jid_to, const std::string& from);
+public:
void accept_subscription(const std::string& from, const std::string& to);
void ask_subscription(const std::string& from, const std::string& to);
+ void send_presence_to_contact(const std::string& from, const std::string& to, const std::string& type, const std::string& id="");
+ void on_irc_client_connected(const std::string& irc_hostname, const std::string& jid);
+ void on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid);
+
/**
* Handle the various stanza types
*/
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
index b138ed9..42a5392 100644
--- a/src/xmpp/xmpp_component.cpp
+++ b/src/xmpp/xmpp_component.cpp
@@ -92,7 +92,7 @@ void XmppComponent::on_connected()
{
log_info("connected to XMPP server");
this->first_connection_try = true;
- auto data = "<stream:stream to='"s + this->served_hostname + \
+ auto data = "<stream:stream to='" + this->served_hostname + \
"' xmlns:stream='http://etherx.jabber.org/streams' xmlns='" COMPONENT_NS "'>";
log_debug("XMPP SENDING: ", data);
this->send_data(std::move(data));
@@ -142,7 +142,7 @@ void XmppComponent::on_remote_stream_open(const XmlNode& node)
}
// Try to authenticate
- auto data = "<handshake xmlns='" COMPONENT_NS "'>"s + get_handshake_digest(this->stream_id, this->secret) + "</handshake>";
+ auto data = "<handshake xmlns='" COMPONENT_NS "'>" + get_handshake_digest(this->stream_id, this->secret) + "</handshake>";
log_debug("XMPP SENDING: ", data);
this->send_data(std::move(data));
}
@@ -261,7 +261,6 @@ void XmppComponent::handle_error(const Stanza& stanza)
if (!this->ever_auth)
sd_notifyf(0, "STATUS=Failed to authenticate to the XMPP server: %s", error_message.data());
#endif
-
}
void* XmppComponent::get_receive_buffer(const size_t size) const
@@ -338,35 +337,6 @@ void XmppComponent::send_user_join(const std::string& from,
this->send_stanza(presence);
}
-void XmppComponent::send_invalid_room_error(const std::string& muc_name,
- const std::string& nick,
- const std::string& to)
-{
- Stanza presence("presence");
- {
- if (!muc_name.empty ())
- presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
- else
- presence["from"] = this->served_hostname;
- presence["to"] = to;
- presence["type"] = "error";
- XmlSubNode x(presence, "x");
- x["xmlns"] = MUC_NS;
- XmlSubNode error(presence, "error");
- error["by"] = muc_name + "@" + this->served_hostname;
- error["type"] = "cancel";
- XmlSubNode item_not_found(error, "item-not-found");
- item_not_found["xmlns"] = STANZA_NS;
- XmlSubNode text(error, "text");
- text["xmlns"] = STANZA_NS;
- text["xml:lang"] = "en";
- text.set_inner(muc_name +
- " is not a valid IRC channel name. A correct room jid is of the form: #<chan>%<server>@" +
- this->served_hostname);
- }
- this->send_stanza(presence);
-}
-
void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to, const std::string& who)
{
Stanza message("message");
@@ -441,7 +411,8 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std:
this->send_stanza(message);
}
-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)
+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)
{
Stanza presence("presence");
{
@@ -453,8 +424,15 @@ void XmppComponent::send_muc_leave(const std::string& muc_name, const std::strin
x["xmlns"] = MUC_USER_NS;
if (self)
{
- XmlSubNode status(x, "status");
- status["code"] = "110";
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "110";
+ }
+ if (!user_requested)
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "332";
+ }
}
if (!message_str.empty())
{
@@ -642,7 +620,7 @@ void XmppComponent::send_iq_version_request(const std::string& from,
Stanza iq("iq");
{
iq["type"] = "get";
- iq["id"] = "version_"s + XmppComponent::next_id();
+ iq["id"] = "version_" + XmppComponent::next_id();
iq["from"] = from + "@" + this->served_hostname;
iq["to"] = jid_to;
XmlSubNode query(iq, "query");
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
index ebe3ec8..22d5c48 100644
--- a/src/xmpp/xmpp_component.hpp
+++ b/src/xmpp/xmpp_component.hpp
@@ -124,12 +124,6 @@ public:
const std::string& to,
const bool self);
/**
- * Send an error to indicate that the user tried to join an invalid room
- */
- void send_invalid_room_error(const std::string& muc_jid,
- const std::string& nick,
- const std::string& to);
- /**
* Send the MUC topic to the user
*/
void send_topic(const std::string& from, Xmpp::body&& xmpp_topic, const std::string& to, const std::string& who);
@@ -146,7 +140,12 @@ public:
/**
* Send an unavailable presence for this nick
*/
- void send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message, const std::string& jid_to, const bool self);
+ void 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);
/**
* Indicate that a participant changed his nick
*/
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index 19dc270..c6607d0 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -49,6 +49,8 @@ class XMPPComponent(slixmpp.BaseXMPP):
def __init__(self, scenario, biboumi):
super().__init__(jid="biboumi.localhost", default_ns="jabber:component:accept")
self.is_component = True
+ self.auto_authorize = None # Do not accept or reject subscribe requests automatically
+ self.auto_subscribe = False
self.stream_header = '<stream:stream %s %s from="%s" id="%s">' % (
'xmlns="jabber:component:accept"',
'xmlns:stream="%s"' % self.stream_ns,
@@ -401,11 +403,11 @@ def handshake_sequence():
partial(send_stanza, "<handshake xmlns='jabber:component:accept'/>"))
-def connection_begin_sequence(irc_host, jid):
+def connection_begin_sequence(irc_host, jid, expected_irc_presence=False):
jid = jid.format_map(common_replacements)
xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]"
- return (
+ result = (
partial(expect_stanza,
xpath % ('Connecting to %s:6697 (encrypted)' % irc_host)),
partial(expect_stanza,
@@ -417,8 +419,13 @@ def connection_begin_sequence(irc_host, jid):
partial(expect_stanza,
xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)),
partial(expect_stanza,
- xpath % 'Connected to IRC server.'),
+ xpath % 'Connected to IRC server.'))
+
+ if expected_irc_presence:
+ result += (partial(expect_stanza, "/presence[@from='" + irc_host + "@biboumi.localhost']"),)
+
# These five messages can be receive in any order
+ result += (
partial(expect_stanza,
xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
partial(expect_stanza,
@@ -431,6 +438,8 @@ def connection_begin_sequence(irc_host, jid):
xpath_re % (r'^%s: (\*\*\* Checking Ident|\*\*\* Looking up your hostname\.\.\.|\*\*\* Found your hostname: .*|ACK multi-prefix|\*\*\* Got Ident response)$' % 'irc.localhost')),
)
+ return result
+
def connection_tls_begin_sequence(irc_host, jid):
jid = jid.format_map(common_replacements)
xpath = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[text()='%s']"
@@ -492,8 +501,8 @@ def connection_middle_sequence(irc_host, jid):
)
-def connection_sequence(irc_host, jid):
- return connection_begin_sequence(irc_host, jid) +\
+def connection_sequence(irc_host, jid, expected_irc_presence=False):
+ return connection_begin_sequence(irc_host, jid, expected_irc_presence) +\
connection_middle_sequence(irc_host, jid) +\
connection_end_sequence(irc_host, jid)
@@ -565,6 +574,27 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
+ Scenario("quit",
+ [
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send a raw QUIT message
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>QUIT bye bye</body></message>"),
+ partial(expect_stanza, ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
+ "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",)),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"),
+ ]),
Scenario("multiple_channels_join",
[
handshake_sequence(),
@@ -1086,6 +1116,65 @@ if __name__ == '__main__':
("/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='second']",),
]),
]),
+ Scenario("persistent_channel",
+ [
+ # Join the channel with user 1
+ handshake_sequence(),
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat'][@to='{jid_one}/{resource_one}']/subject[not(text())]"),
+
+ # Make it persistent for user 1
+ partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='false']"),
+ partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf2' to='#foo%{irc_server_one}' type='set'><query xmlns='http://jabber.org/protocol/muc#owner'><x type='submit' xmlns='jabber:x:data'><field var='persistent' xmlns='jabber:x:data'><value>true</value></field></x></query></iq>"),
+ partial(expect_stanza, "/iq[@type='result']"),
+
+ # Check that the value is now effectively true
+ partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='conf1' to='#foo%{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/muc#owner'/></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/muc_owner:query/dataform:x/dataform:field[@var='persistent'][@type='boolean']/dataform:value[text()='true']"),
+
+ # A second user joins the same channel
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+
+ partial(expect_unordered, [
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",),
+ ]
+ ),
+
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # First user leaves the room (but biboumi will stay in the channel)
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ # Only user 1 receives the unavailable presence
+ partial(expect_stanza,
+ "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
+
+ # Second user sends a channel message
+ partial(send_stanza, "<message type='groupchat' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}'><body>coucou</body></message>"),
+
+ # Message should only be received by user 2, since user 1 has no resource in the room
+ partial(expect_stanza, "/message[@type='groupchat'][@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']"),
+
+ # Second user leaves the channel
+ partial(send_stanza, "<presence type='unavailable' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_two}']"),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"),
+ ]),
Scenario("channel_join_with_different_nick",
[
handshake_sequence(),
@@ -2317,6 +2406,9 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='{nick_two}'/></x></message>"),
partial(expect_stanza, "/message/body[text()='{nick_two} has been invited to #foo']"),
partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='bertrand@example.com'/></x></message>"),
+ partial(expect_stanza, "/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"),
]),
Scenario("virtual_channel_multisession",
[
@@ -2392,6 +2484,7 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='42']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='false']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']",
"/iq/commands:command/commands:actions/commands:next",
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
@@ -2458,6 +2551,41 @@ if __name__ == '__main__':
),
partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+
+ # Same thing, but try to empty some values
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']",
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>"
+ "<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='username'><value></value></field>"
+ "<field var='realname'><value></value></field>"
+ "<field var='encoding_out'><value></value></field>"
+ "<field var='encoding_in'><value></value></field>"
+ "</x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']",
+ "/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='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",
+ "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_out']/dataform:value",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
+ ),
+ partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
+
]),
Scenario("irc_channel_configure",
[
@@ -2606,22 +2734,6 @@ if __name__ == '__main__':
partial(send_stanza, "<iq type='set' id='command2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='get-irc-connection-info' action='execute' /></iq>"),
partial(expect_stanza, r"/iq/commands:command/commands:note[re:test(text(), 'Connected to IRC server irc.localhost on port 6667 since \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \(\d+ seconds ago\)\.\n#foo from 1 resource: {resource_one}.*')]"),
], conf='fixed_server'),
- Scenario("invalid_room_jid",
- [
- handshake_sequence(),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='invalid%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, ("/presence[@type='error'][@to='{jid_one}/{resource_one}'][@from='invalid%{irc_server_one}/{nick_one}']/error[@type='cancel']/stanza:item-not-found",
- "/presence/muc:x",
- "/presence/error/stanza:text")),
- ]),
- Scenario("invalid_room_jid_fixed",
- [
- handshake_sequence(),
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='invalid@{biboumi_host}/{nick_one}' />"),
- partial(expect_stanza, ("/presence[@type='error'][@to='{jid_one}/{resource_one}'][@from='invalid@{biboumi_host}/{nick_one}']/error[@type='cancel']/stanza:item-not-found",
- "/presence/muc:x",
- "/presence/error/stanza:text")),
- ], conf='fixed_server'),
Scenario("irc_server_presence_subscription",
[
handshake_sequence(),
@@ -2649,7 +2761,62 @@ if __name__ == '__main__':
partial(expect_stanza, "/message[@to='{jid_two}/{resource_two}'][@type='chat']/body[text()='irc.localhost: {nick_one}: Nickname is already in use.']"),
partial(expect_stanza, "/presence[@type='error']/error[@type='cancel'][@code='409']/stanza:conflict"),
partial(send_stanza, "<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />")
- ])
+ ]),
+ Scenario("basic_subscribe_unsubscribe",
+ [
+ handshake_sequence(),
+
+ # Mutual subscription exchange
+ partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribe' id='subid1' />"),
+ partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"),
+
+ # Get the current presence of the biboumi gateway
+ partial(expect_stanza, "/presence"),
+
+ partial(expect_stanza, "/presence[@type='subscribe']"),
+ partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='subscribed' />"),
+
+
+ # Unsubscribe
+ partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribe' id='unsubid1' />"),
+ partial(expect_stanza, "/presence[@type='unavailable']"),
+ partial(expect_stanza, "/presence[@type='unsubscribed']"),
+ partial(expect_stanza, "/presence[@type='unsubscribe']"),
+ partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unavailable' />"),
+ partial(send_stanza, "<presence from='{jid_one}' to='{biboumi_host}' type='unsubscribed' />"),
+ ]),
+ Scenario("irc_server_presence_in_roster",
+ [
+ handshake_sequence(),
+
+ # Mutual subscription exchange
+ partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribe' id='subid1' />"),
+ partial(expect_stanza, "/presence[@type='subscribed'][@id='subid1']"),
+
+ partial(expect_stanza, "/presence[@type='subscribe']"),
+ partial(send_stanza, "<presence from='{jid_one}' to='{irc_server_one}' type='subscribed' />"),
+
+ # Join a channel on that server
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+
+ # We must receive the IRC server presence, in the connection sequence
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}', True),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Leave the channel, and thus the IRC server
+ partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}']"),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Closing Link: localhost (Client Quit)']"),
+ partial(expect_stanza, "/message[@from='{irc_server_one}']/body[text()='ERROR: Connection closed.']"),
+ partial(expect_stanza, "/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='unavailable']"),
+ ])
)
failures = 0
diff --git a/tests/iid.cpp b/tests/iid.cpp
index 3da0396..63b2ba3 100644
--- a/tests/iid.cpp
+++ b/tests/iid.cpp
@@ -125,4 +125,10 @@ TEST_CASE("Iid creation in fixed_server mode")
CHECK(iid6.get_local() == "##channel%");
CHECK(iid6.get_server() == "fixed.example.com");
CHECK(iid6.type == Iid::Type::Channel);
+
+ Iid iid7("", chantypes);
+ CHECK(std::to_string(iid7) == "");
+ CHECK(iid7.get_local() == "");
+ CHECK(iid7.get_server() == "fixed.example.com");
+ CHECK(iid7.type == Iid::Type::None);
}