summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codecov.yml3
-rw-r--r--.gitlab-ci.yml32
-rw-r--r--CHANGELOG.rst22
-rw-r--r--CMakeLists.txt16
-rw-r--r--INSTALL.rst2
-rw-r--r--doc/biboumi.1.rst65
-rw-r--r--docker/packaging/archlinux/Dockerfile (renamed from docker/biboumi-test/archlinux/Dockerfile)0
-rw-r--r--packaging/biboumi.spec.cmake3
-rw-r--r--src/bridge/bridge.cpp98
-rw-r--r--src/bridge/bridge.hpp13
-rw-r--r--src/config/config.cpp17
-rw-r--r--src/config/config.hpp5
-rw-r--r--src/database/column.hpp6
-rw-r--r--src/database/database.cpp160
-rw-r--r--src/database/database.hpp37
-rw-r--r--src/database/delete_query.hpp33
-rw-r--r--src/database/insert_query.hpp17
-rw-r--r--src/database/postgresql_engine.cpp13
-rw-r--r--src/database/postgresql_engine.hpp3
-rw-r--r--src/database/postgresql_statement.hpp20
-rw-r--r--src/database/query.cpp3
-rw-r--r--src/database/query.hpp7
-rw-r--r--src/database/row.hpp40
-rw-r--r--src/database/save.hpp31
-rw-r--r--src/database/select_query.hpp10
-rw-r--r--src/database/sqlite3_engine.cpp1
-rw-r--r--src/database/sqlite3_engine.hpp3
-rw-r--r--src/database/sqlite3_statement.hpp1
-rw-r--r--src/database/table.hpp11
-rw-r--r--src/database/update_query.hpp11
-rw-r--r--src/irc/irc_channel.cpp17
-rw-r--r--src/irc/irc_channel.hpp33
-rw-r--r--src/irc/irc_client.cpp89
-rw-r--r--src/irc/irc_client.hpp14
-rw-r--r--src/main.cpp13
-rw-r--r--src/utils/is_one_of.hpp9
-rw-r--r--src/utils/time.cpp3
-rw-r--r--src/utils/time.hpp5
-rw-r--r--src/utils/uuid.cpp14
-rw-r--r--src/utils/uuid.hpp8
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp2
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp71
-rw-r--r--src/xmpp/biboumi_component.cpp89
-rw-r--r--src/xmpp/xmpp_component.cpp22
-rw-r--r--src/xmpp/xmpp_component.hpp12
-rw-r--r--tests/database.cpp60
-rw-r--r--tests/end_to_end/__main__.py370
-rw-r--r--tests/utils.cpp14
48 files changed, 983 insertions, 545 deletions
diff --git a/.codecov.yml b/.codecov.yml
deleted file mode 100644
index 74ba41b..0000000
--- a/.codecov.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-codecov:
- ignore:
- - "tests"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 31fa61e..8807fbd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -30,7 +30,7 @@ variables:
- "echo Running cmake with the following parameters: -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}"
- mkdir build/
- cd build/
- - cmake .. -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}
+ - cmake .. -DCMAKE_CXX_FLAGS="-Werror -Wno-psabi" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}
- make everything -j$(nproc || echo 1)
- make coverage_check -j$(nproc || echo 1)
artifacts:
@@ -42,24 +42,28 @@ variables:
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
-.template:debian_build: &debian_build
+build:fedora:
+ <<: *fedora_build
+
+build:debian:
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-debian:latest
-.template:alpine_build: &alpine_build
+build:alpine:
variables:
SYSTEMD: "-DWITHOUT_SYSTEMD=1"
<<: *basic_build
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
-build:fedora:
- <<: *fedora_build
-
-build:debian:
- <<: *debian_build
-
-build:alpine:
- <<: *alpine_build
+build:archlinux:
+ <<: *basic_build
+ only:
+ - branches@louiz/biboumi
+ when: manual
+ tags:
+ - armv7l
+ artifacts:
+ paths: []
build:1:
variables:
@@ -101,7 +105,7 @@ build:6:
UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
-build:7:
+build:without_udns:
variables:
UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
@@ -144,7 +148,7 @@ test:without_udns:
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
<<: *basic_test
dependencies:
- - build:7
+ - build:without_udns
test:alpine:
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
@@ -248,7 +252,7 @@ packaging:archlinux:
tags:
- docker
allow_failure: true
- image: docker.louiz.org/louiz/biboumi/test-archlinux:latest
+ image: docker.louiz.org/louiz/biboumi/packaging-archlinux:latest
before_script: []
script:
- sudo pacman -Syuu --noconfirm
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 338b6ac..4966a3c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,25 @@
+Version 8.0
+===========
+
+- Add a complete='true' in MAM’s iq result when appropriate
+- The archive ordering now only relies on the value of the ID, not the
+ date. This means that if you manually import archives in your database (or
+ mess with it somehow), biboumi will not work properly anymore, if you
+ don’t make sure the ID of everything in the muclogline table is
+ consistent.
+- The “virtual” channel with an empty name (for example
+ %irc.freenode.net@biboumi) has been entirely removed.
+- Add an “Address” field in the servers’ configure form. This lets
+ the user customize the address to use when connecting to a server.
+ See https://lab.louiz.org/louiz/biboumi/issues/3273 for more details.
+- Messages id are properly reflected to the sender
+- We now properly deal with a PostgreSQL server restart: whenever the
+ connection is lost with the server, we try to reconnect and re-execute the
+ query once.
+- A Nick field has been added in the IRC server configuration form, to let
+ the user force a nickname whenever a channel on the server is joined.
+- Multiple admins can now be listed in the admin field, separated with a colon.
+
Version 7.3
===========
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1fe8010..6d36e1c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,9 +2,19 @@ cmake_minimum_required(VERSION 3.0)
project(biboumi)
-set(${PROJECT_NAME}_VERSION_MAJOR 7)
-set(${PROJECT_NAME}_VERSION_MINOR 2)
-set(${PROJECT_NAME}_VERSION_SUFFIX "")
+set(${PROJECT_NAME}_VERSION_MAJOR 8)
+set(${PROJECT_NAME}_VERSION_MINOR 0)
+set(${PROJECT_NAME}_VERSION_SUFFIX "~dev")
+
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0)
+ message(FATAL_ERROR "GCC version must be at least 5.0.")
+ endif()
+elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.4)
+ message(FATAL_ERROR "Clang version must be at least 3.4.")
+ endif()
+endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
diff --git a/INSTALL.rst b/INSTALL.rst
index 4c02c7b..45a860d 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -16,7 +16,7 @@ Build and runtime dependencies:
Tools:
~~~~~~
-- A C++14 compiler (clang >= 3.4 or gcc >= 4.9 for example)
+- A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example)
- CMake
- pandoc (optional) to build the man page
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
index 89f8627..03e8a36 100644
--- a/doc/biboumi.1.rst
+++ b/doc/biboumi.1.rst
@@ -98,21 +98,22 @@ The bare JID of the gateway administrator. This JID will have more
privileges than other standard users, for example some administration
ad-hoc commands will only be available to that JID.
+If you need more than one administrator, separate them with a colon (:).
+
fixed_irc_server
----------------
If this option contains the hostname of an IRC server (for example
irc.example.org), then biboumi will enforce the connexion to that IRC
-server only. This means that a JID like ``#chan@biboumi.example.com`` must
-be used instead of ``#chan%irc.example.org@biboumi.example.com``. In that
-mode, the virtual channel (see `Connect to an IRC server`_) is not
-available. The `%` character loses any meaning in the JIDs. It can appear
-in the JID but will not be interpreted as a separator (thus the JID
+server only. This means that a JID like ``#chan@biboumi.example.com``
+must be used instead of ``#chan%irc.example.org@biboumi.example.com``. The
+`%` character loses any meaning in the JIDs. It can appear in the JID but
+will not be interpreted as a separator (thus the JID
``#channel%hello@biboumi.example.com`` points to the channel named
-``#channel%hello`` on the configured IRC server) This option can for example
-be used by an administrator that just wants to let their users join their own
-IRC server using an XMPP client, while forbidding access to any other IRC
-server.
+``#channel%hello`` on the configured IRC server) This option can for
+example be used by an administrator that just wants to let their users
+join their own IRC server using an XMPP client, while forbidding access to
+any other IRC server.
persistent_by_default
---------------------
@@ -206,6 +207,8 @@ this identd server, moderation is a lot harder, because all the different
users of a single biboumi instance all share the same IP, and they can’t be
distinguished by the IRC servers.
+To disable the built-in identd, you may set identd_port to 0.
+
policy_directory
----------------
@@ -295,10 +298,6 @@ the two is based on the first character: by default, if the name starts with
``'#'`` or ``'&'`` (but this can be overridden by the server, using the
ISUPPORT extension) then it’s a channel name, otherwise this is a nickname.
-As a special case, the channel name can also be empty (for example
-``%irc.example.com``), in that case this represents the virtual channel
-provided by biboumi. See `Connect to an IRC server`_ for more details.
-
There is two ways to address an IRC user, using a local part like this:
``nickname`` % ``irc_server`` or by using the in-room address of the
participant, like this:
@@ -336,9 +335,6 @@ Examples:
* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com.
-* ``%irc.example.com@biboumi.example.com`` is the virtual channel provided by
- biboumi, for the IRC server irc.example.com.
-
Note: Some JIDs are valid but make no sense in the context of
biboumi:
@@ -362,16 +358,7 @@ Connect to an IRC server
The connection to the IRC server is automatically made when the user tries
to join any channel on that IRC server. The connection is closed whenever
-the last channel on that server is left by the user. To be able to stay
-connected to an IRC server without having to be in a real IRC channel,
-biboumi provides a virtual channel on the jid
-``%irc.example.com@biboumi.example.com``. For example if you want to join the
-channel ``#foo`` on the server ``irc.example.com``, but you need to authenticate
-to a bot of the server before you’re allowed to join it, you can first join
-the room ``%irc.example.com@biboumi.example.com`` (this will effectively
-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``.
+the last channel on that server is left by the user.
Roster
------
@@ -638,6 +625,17 @@ On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
- configure: Lets each user configure some options that applies to the
concerned IRC server. The provided configuration form contains these
fields:
+
+ * Address: This address (IPv4, IPv6 or hostname) will be used, when
+ biboumi connects to this server. This is a very handy way to have a
+ custom name for a network, and be able to edit the address to use
+ if one endpoint for that server is dead, but continue using the same
+ JID. For example, a user could configure the server
+ “freenode@biboumi.example.com”, set “chat.freenode.net” in its
+ “Address” field, and then they would be able to use “freenode” as
+ the network name forever: if “chat.freenode.net” breaks for some
+ reason, it can be changed to “irc.freenode.org” instead, and the user
+ would not need to change all their bookmarks and settings.
* Realname: The customized “real name” as it will appear on the
user’s whois. This option is not available if biboumi is configured
with realname_customization to false.
@@ -649,10 +647,11 @@ On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
In encoding into UTF-8. If the conversion fails at some point, some
characters will be replaced by the placeholders.
* Out encoding: Currently ignored.
- * After-connection IRC command: A raw IRC command that will be sent to
- the server immediately after the connection has been successful. It
- can for example be used to identify yourself using NickServ, with a
- command like this: `PRIVMSG NickServ :identify PASSWORD`.
+ * After-connection IRC commands: Raw IRC commands that will be sent
+ one by one to the server immediately after the connection has been
+ successful. It can for example be used to identify yourself using
+ NickServ, with a command like this: `PRIVMSG NickServ :identify
+ PASSWORD`.
* Ports: The list of TCP ports to use when connecting to this IRC server.
This list will be tried in sequence, until the connection succeeds for
one of them. The connection made on these ports will not use TLS, the
@@ -671,6 +670,12 @@ On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
* SHA-1 fingerprint of the TLS certificate to trust: if you know the hash
of the certificate that the server is supposed to use, and you only want
to accept this one, set its SHA-1 hash in this field.
+ * Nickname: A nickname that will be used instead of the nickname provided
+ in the initial presence sent to join a channel. This can be used if the
+ user always wants to have the same nickname on a given server, and not
+ have to bother with setting that nick in all the bookmarks on that
+ server. The nickname can still manually be changed with a standard nick
+ change presence.
* Server password: A password that will be sent just after the connection,
in a PASS command. This is usually used in private servers, where you’re
only allowed to connect if you have the password. Note that, although
diff --git a/docker/biboumi-test/archlinux/Dockerfile b/docker/packaging/archlinux/Dockerfile
index 20f0343..20f0343 100644
--- a/docker/biboumi-test/archlinux/Dockerfile
+++ b/docker/packaging/archlinux/Dockerfile
diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake
index 8b9ad17..d07ed74 100644
--- a/packaging/biboumi.spec.cmake
+++ b/packaging/biboumi.spec.cmake
@@ -63,6 +63,9 @@ make check %{?_smp_mflags}
%changelog
+* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
+- Build latest git revision
+
* Wed Jan 24 2018 Le Coz Florent <louiz@louiz.org> - 7.2-1
Update to version 7.2
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index 54bee84..1c646fe 100644
--- a/src/bridge/bridge.cpp
+++ b/src/bridge/bridge.cpp
@@ -5,6 +5,7 @@
#include <utils/empty_if_fixed_server.hpp>
#include <utils/encoding.hpp>
#include <utils/tolower.hpp>
+#include <utils/uuid.hpp>
#include <logger/logger.hpp>
#include <utils/revstr.hpp>
#include <utils/split.hpp>
@@ -63,7 +64,6 @@ void Bridge::shutdown(const std::string& exit_message)
for (auto& pair: this->irc_clients)
{
pair.second->send_quit_command(exit_message);
- pair.second->leave_dummy_channel(exit_message, {});
}
}
@@ -103,7 +103,7 @@ const std::string& Bridge::get_jid() const
std::string Bridge::get_bare_jid() const
{
Jid jid(this->user_jid);
- return jid.local + "@" + jid.domain;
+ return jid.bare();
}
Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding)
@@ -166,56 +166,36 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const
}
}
-bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password,
- const std::string& resource, HistoryLimit history_limit)
+bool Bridge::join_irc_channel(const Iid& iid, std::string nickname,
+ const std::string& password,
+ const std::string& resource,
+ HistoryLimit history_limit,
+ const bool force_join)
{
const auto& hostname = iid.get_server();
+#ifdef USE_DATABASE
+ auto soptions = Database::get_irc_server_options(this->get_bare_jid(), hostname);
+ if (!soptions.col<Database::Nick>().empty())
+ nickname = soptions.col<Database::Nick>();
+#endif
IrcClient* irc = this->make_irc_client(hostname, nickname);
irc->history_limit = history_limit;
this->add_resource_to_server(hostname, resource);
auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource);
if (!res_in_chan)
this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource);
- if (iid.get_local().empty())
- { // Join the dummy channel
- if (irc->is_welcomed())
- {
- if (res_in_chan)
- return false;
- // Immediately simulate a message coming from the IRC server saying that we
- // joined the channel
- if (irc->get_dummy_channel().joined)
- {
- this->generate_channel_join_for_resource(iid, resource);
- }
- else
- {
- const IrcMessage join_message(irc->get_nick(), "JOIN", {""});
- irc->on_channel_join(join_message);
- const IrcMessage end_join_message(std::string(iid.get_server()), "366",
- {irc->get_nick(),
- "", "End of NAMES list"});
- irc->on_channel_completely_joined(end_join_message);
- }
- }
- else
- {
- irc->get_dummy_channel().joining = true;
- irc->start();
- }
- return true;
- }
if (irc->is_channel_joined(iid.get_local()) == false)
{
irc->send_join_command(iid.get_local(), password);
return true;
- } else if (!res_in_chan) {
+ } else if (!res_in_chan || force_join) {
+ // See https://github.com/xsf/xeps/pull/499 for the force_join argument
this->generate_channel_join_for_resource(iid, resource);
}
return false;
}
-void Bridge::send_channel_message(const Iid& iid, const std::string& body)
+void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id)
{
if (iid.get_server().empty())
{
@@ -240,6 +220,7 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
std::vector<std::string> lines = utils::split(body, '\n', true);
if (lines.empty())
return ;
+ bool first = true;
for (const std::string& line: lines)
{
if (line.substr(0, 5) == "/mode")
@@ -261,9 +242,12 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
std::get<0>(xmpp_body), irc->get_own_nick());
#endif
+ if (!first || id.empty())
+ id = utils::gen_uuid();
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line),
- this->user_jid + "/" + resource, uuid);
+ this->user_jid + "/" + resource, uuid, id);
+ first = false;
}
}
@@ -445,15 +429,11 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
#endif
if (channel->joined && !channel->parting && !persistent)
{
- const auto& chan_name = iid.get_local();
- if (chan_name.empty())
- irc->leave_dummy_channel(status_message, resource);
- else
- irc->send_part_command(iid.get_local(), status_message);
+ irc->send_part_command(iid.get_local(), status_message);
}
else if (channel->joined)
{
- this->send_muc_leave(iid, channel->get_self()->nick, "", true, true, resource);
+ this->send_muc_leave(iid, *channel->get_self(), "", true, true, resource, irc);
}
if (persistent)
this->remove_resource_from_chan(key, resource);
@@ -464,9 +444,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
else
{
if (channel && channel->joined)
- this->send_muc_leave(iid, channel->get_self()->nick,
+ this->send_muc_leave(iid, *channel->get_self(),
"Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.",
- true, true, resource);
+ true, true, resource, irc);
this->remove_resource_from_chan(key, resource);
}
if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
@@ -839,19 +819,19 @@ void Bridge::send_irc_version_request(const std::string& irc_hostname, const std
void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc)
{
const auto encoding = in_encoding_for(*this, iid);
+ std::string uuid{};
if (muc)
{
#ifdef USE_DATABASE
const auto xmpp_body = this->make_xmpp_body(body, encoding);
if (!nick.empty() && this->record_history)
- Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
- std::get<0>(xmpp_body), nick);
+ uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
+ std::get<0>(xmpp_body), nick);
#endif
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
{
this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding),
- this->user_jid + "/" + resource, {});
-
+ this->user_jid + "/" + resource, uuid, utils::gen_uuid());
}
}
else
@@ -881,19 +861,24 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick,
this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text);
}
-void Bridge::send_muc_leave(const Iid& iid, const std::string& nick,
+void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user,
const std::string& message, const bool self,
const bool user_requested,
- const std::string& resource)
+ const std::string& resource,
+ const IrcClient* client)
{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user.get_most_significant_mode(client->get_sorted_user_modes()));
+
if (!resource.empty())
- this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
- this->user_jid + "/" + resource, self, user_requested);
+ this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
+ this->user_jid + "/" + resource, self, user_requested, affiliation, role);
else
{
for (const auto &res: this->resources_in_chan[iid.to_tuple()])
- this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
- this->user_jid + "/" + res, self, user_requested);
+ this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
+ this->user_jid + "/" + res, self, user_requested, affiliation, role);
if (self)
{
// Copy the resources currently in that channel
@@ -1020,7 +1005,7 @@ void Bridge::send_room_history(const std::string& hostname, std::string chan_nam
auto limit = coptions.col<Database::MaxHistoryLength>();
if (history_limit.stanzas >= 0 && history_limit.stanzas < limit)
limit = history_limit.stanzas;
- const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since);
+ const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since, {}, Id::unset_value, Database::Paging::last);
chan_name.append(utils::empty_if_fixed_server("%" + hostname));
for (const auto& line: lines)
{
@@ -1254,9 +1239,6 @@ std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc
res++;
}
- IrcClient* irc = this->find_irc_client(irc_hostname);
- if (irc && (irc->get_dummy_channel().joined || irc->get_dummy_channel().joining))
- res++;
return res;
}
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index c2f0233..8e7d9d7 100644
--- a/src/bridge/bridge.hpp
+++ b/src/bridge/bridge.hpp
@@ -75,9 +75,13 @@ public:
* Try to join an irc_channel, does nothing and return true if the channel
* was already joined.
*/
- bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource, HistoryLimit history_limit);
+ bool join_irc_channel(const Iid& iid, std::string nickname,
+ const std::string& password,
+ const std::string& resource,
+ HistoryLimit history_limit,
+ const bool force_join);
- void send_channel_message(const Iid& iid, const std::string& body);
+ void send_channel_message(const Iid& iid, const std::string& body, std::string id);
void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG");
void send_raw_message(const std::string& hostname, const std::string& body);
void leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource);
@@ -170,10 +174,11 @@ public:
/**
* Send an unavailable presence from this participant
*/
- void send_muc_leave(const Iid& iid, const std::string& nick,
+ void send_muc_leave(const Iid& iid, const IrcUser& nick,
const std::string& message, const bool self,
const bool user_requested,
- const std::string& resource="");
+ const std::string& resource,
+ const IrcClient* client);
/**
* Send presences to indicate that an user old_nick (ourself if self ==
* true) changed his nick to new_nick. The user_mode is needed because
diff --git a/src/config/config.cpp b/src/config/config.cpp
index 412b170..2f64b9e 100644
--- a/src/config/config.cpp
+++ b/src/config/config.cpp
@@ -1,10 +1,12 @@
#include <config/config.hpp>
#include <utils/tolower.hpp>
+#include <utils/split.hpp>
-#include <iostream>
-#include <cstring>
-
+#include <algorithm>
#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <vector>
using namespace std::string_literals;
@@ -40,6 +42,15 @@ int Config::get_int(const std::string& option, const int& def)
return def;
}
+bool Config::is_in_list(const std::string& option, const std::string& value)
+{
+ std::string res = Config::get(option, "");
+ if (res.empty())
+ return false;
+ std::vector<std::string> list = utils::split(res, ':');
+ return std::find(list.cbegin(), list.cend(), value) != list.cend();
+}
+
void Config::set(const std::string& option, const std::string& value, bool save)
{
Config::values[option] = value;
diff --git a/src/config/config.hpp b/src/config/config.hpp
index c5ef15d..9c28e8c 100644
--- a/src/config/config.hpp
+++ b/src/config/config.hpp
@@ -46,6 +46,11 @@ public:
static int get_int(const std::string&, const int&);
static bool get_bool(const std::string&, const bool);
/**
+ * Returns true if value is present in a colon-separated list, otherwise
+ * false.
+ */
+ static bool is_in_list(const std::string& option, const std::string& value);
+ /**
* Set a value for the given option. And write all the config
* in the file from which it was read if save is true.
*/
diff --git a/src/database/column.hpp b/src/database/column.hpp
index 1f16bcf..50c9c14 100644
--- a/src/database/column.hpp
+++ b/src/database/column.hpp
@@ -13,10 +13,14 @@ struct Column
T value{};
};
+struct ForeignKey: Column<std::size_t> {
+ static constexpr auto name = "fk_";
+};
+
struct Id: Column<std::size_t> {
static constexpr std::size_t unset_value = static_cast<std::size_t>(-1);
static constexpr auto name = "id_";
static constexpr auto options = "PRIMARY KEY";
- Id(): Column<std::size_t>(-1) {}
+ Id(): Column<std::size_t>(unset_value) {}
};
diff --git a/src/database/database.cpp b/src/database/database.cpp
index 3622963..3b3e890 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -1,10 +1,12 @@
#include "biboumi.h"
#ifdef USE_DATABASE
+#include <database/select_query.hpp>
+#include <database/save.hpp>
#include <database/database.hpp>
-#include <uuid/uuid.h>
#include <utils/get_first_non_empty.hpp>
#include <utils/time.hpp>
+#include <utils/uuid.hpp>
#include <config/config.hpp>
#include <database/sqlite3_engine.hpp>
@@ -21,6 +23,7 @@ Database::GlobalOptionsTable Database::global_options("globaloptions_");
Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_");
Database::RosterTable Database::roster("roster");
+Database::AfterConnectionCommandsTable Database::after_connection_commands("after_connection_commands_");
std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{};
Database::GlobalPersistent::GlobalPersistent():
@@ -53,57 +56,80 @@ void Database::open(const std::string& filename)
Database::irc_channel_options.upgrade(*Database::db);
Database::roster.create(*Database::db);
Database::roster.upgrade(*Database::db);
+ Database::after_connection_commands.create(*Database::db);
+ Database::after_connection_commands.upgrade(*Database::db);
create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name());
}
Database::GlobalOptions Database::get_global_options(const std::string& owner)
{
- auto request = Database::global_options.select();
+ auto request = select(Database::global_options);
request.where() << Owner{} << "=" << owner;
- Database::GlobalOptions options{Database::global_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
- options.col<Owner>() = owner;
+ return result.front();
+ Database::GlobalOptions options{Database::global_options.get_name()};
+ options.col<Owner>() = owner;
return options;
}
Database::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server)
{
- auto request = Database::irc_server_options.select();
+ auto request = select(Database::irc_server_options);
request.where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;
- Database::IrcServerOptions options{Database::irc_server_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
+ return result.front();
+ Database::IrcServerOptions options{Database::irc_server_options.get_name()};
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ return options;
+}
+
+Database::AfterConnectionCommands Database::get_after_connection_commands(const IrcServerOptions& server_options)
+{
+ const auto id = server_options.col<Id>();
+ if (id == Id::unset_value)
+ return {};
+ auto request = select(Database::after_connection_commands);
+ request.where() << ForeignKey{} << "=" << id;
+ return request.execute(*Database::db);
+}
+
+void Database::set_after_connection_commands(const Database::IrcServerOptions& server_options, Database::AfterConnectionCommands& commands)
+{
+ const auto id = server_options.col<Id>();
+ if (id == Id::unset_value)
+ return ;
+
+ Transaction transaction;
+ auto query = Database::after_connection_commands.del();
+ query.where() << ForeignKey{} << "=" << id;
+ query.execute(*Database::db);
+
+ for (auto& command: commands)
{
- options.col<Owner>() = owner;
- options.col<Server>() = server;
+ command.col<ForeignKey>() = server_options.col<Id>();
+ save(command, *Database::db);
}
- return options;
}
Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel)
{
- auto request = Database::irc_channel_options.select();
+ auto request = select(Database::irc_channel_options);
request.where() << Owner{} << "=" << owner <<\
" and " << Server{} << "=" << server <<\
" and " << Channel{} << "=" << channel;
- Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
- {
- options.col<Owner>() = owner;
- options.col<Server>() = server;
- options.col<Channel>() = channel;
- }
+ return result.front();
+ Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ options.col<Channel>() = channel;
return options;
}
@@ -159,15 +185,18 @@ std::string Database::store_muc_message(const std::string& owner, const std::str
line.col<Body>() = body;
line.col<Nick>() = nick;
- line.save(Database::db);
+ save(line, *Database::db);
return uuid;
}
std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
- int limit, const std::string& start, const std::string& end)
+ int limit, const std::string& start, const std::string& end, const Id::real_type reference_record_id, Database::Paging paging)
{
- auto request = Database::muc_log_lines.select();
+ if (limit == 0)
+ return {};
+
+ auto request = select(Database::muc_log_lines);
request.where() << Database::Owner{} << "=" << owner << \
" and " << Database::IrcChanName{} << "=" << chan_name << \
" and " << Database::IrcServerName{} << "=" << server;
@@ -184,15 +213,59 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne
if (end_time != -1)
request << " and " << Database::Date{} << "<=" << end_time;
}
+ if (reference_record_id != Id::unset_value)
+ {
+ request << " and " << Id{};
+ if (paging == Database::Paging::first)
+ request << ">";
+ else
+ request << "<";
+ request << reference_record_id;
+ }
- request.order_by() << Id{} << " DESC ";
+ if (paging == Database::Paging::first)
+ request.order_by() << Id{} << " ASC ";
+ else
+ request.order_by() << Id{} << " DESC ";
if (limit >= 0)
request.limit() << limit;
auto result = request.execute(*Database::db);
- return {result.crbegin(), result.crend()};
+ if (paging == Database::Paging::first)
+ return result;
+ else
+ return {result.crbegin(), result.crend()};
+}
+
+Database::MucLogLine Database::get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server,
+ const std::string& uuid, const std::string& start, const std::string& end)
+{
+ auto request = select(Database::muc_log_lines);
+ request.where() << Database::Owner{} << "=" << owner << \
+ " and " << Database::IrcChanName{} << "=" << chan_name << \
+ " and " << Database::IrcServerName{} << "=" << server << \
+ " and " << Database::Uuid{} << "=" << uuid;
+
+ if (!start.empty())
+ {
+ const auto start_time = utils::parse_datetime(start);
+ if (start_time != -1)
+ request << " and " << Database::Date{} << ">=" << start_time;
+ }
+ if (!end.empty())
+ {
+ const auto end_time = utils::parse_datetime(end);
+ if (end_time != -1)
+ request << " and " << Database::Date{} << "<=" << end_time;
+ }
+
+ auto result = request.execute(*Database::db);
+
+ if (result.empty())
+ throw Database::RecordNotFound{};
+ return result.front();
}
void Database::add_roster_item(const std::string& local, const std::string& remote)
@@ -202,7 +275,7 @@ void Database::add_roster_item(const std::string& local, const std::string& remo
roster_item.col<Database::LocalJid>() = local;
roster_item.col<Database::RemoteJid>() = remote;
- roster_item.save(Database::db);
+ save(roster_item, *Database::db);
}
void Database::delete_roster_item(const std::string& local, const std::string& remote)
@@ -216,7 +289,7 @@ void Database::delete_roster_item(const std::string& local, const std::string& r
bool Database::has_roster_item(const std::string& local, const std::string& remote)
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
query.where() << Database::LocalJid{} << "=" << local << \
" and " << Database::RemoteJid{} << "=" << remote;
@@ -227,7 +300,7 @@ bool Database::has_roster_item(const std::string& local, const std::string& remo
std::vector<Database::RosterItem> Database::get_contact_list(const std::string& local)
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
query.where() << Database::LocalJid{} << "=" << local;
return query.execute(*Database::db);
@@ -235,7 +308,7 @@ std::vector<Database::RosterItem> Database::get_contact_list(const std::string&
std::vector<Database::RosterItem> Database::get_full_roster()
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
return query.execute(*Database::db);
}
@@ -247,11 +320,26 @@ void Database::close()
std::string Database::gen_uuid()
{
- char uuid_str[37];
- uuid_t uuid;
- uuid_generate(uuid);
- uuid_unparse(uuid, uuid_str);
- return uuid_str;
+ return utils::gen_uuid();
+}
+
+Transaction::Transaction()
+{
+ const auto result = Database::raw_exec("BEGIN");
+ if (std::get<bool>(result) == false)
+ log_error("Failed to create SQL transaction: ", std::get<std::string>(result));
+ else
+ this->success = true;
+
}
+Transaction::~Transaction()
+{
+ if (this->success)
+ {
+ const auto result = Database::raw_exec("END");
+ if (std::get<bool>(result) == false)
+ log_error("Failed to end SQL transaction: ", std::get<std::string>(result));
+ }
+}
#endif
diff --git a/src/database/database.hpp b/src/database/database.hpp
index ec44543..d986ecc 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -22,6 +22,8 @@ class Database
{
public:
using time_point = std::chrono::system_clock::time_point;
+ struct RecordNotFound: public std::exception {};
+ enum class Paging { first, last };
struct Uuid: Column<std::string> { static constexpr auto name = "uuid_"; };
@@ -82,6 +84,7 @@ class Database
struct RemoteJid: Column<std::string> { static constexpr auto name = "remote"; };
+ struct Address: Column<std::string> { static constexpr auto name = "address_"; };
using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
using MucLogLine = MucLogLineTable::RowType;
@@ -89,7 +92,7 @@ class Database
using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>;
using GlobalOptions = GlobalOptionsTable::RowType;
- using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength>;
+ using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick>;
using IrcServerOptions = IrcServerOptionsTable::RowType;
using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>;
@@ -98,6 +101,9 @@ class Database
using RosterTable = Table<LocalJid, RemoteJid>;
using RosterItem = RosterTable::RowType;
+ using AfterConnectionCommandsTable = Table<Id, ForeignKey, AfterConnectionCommand>;
+ using AfterConnectionCommands = std::vector<AfterConnectionCommandsTable::RowType>;
+
Database() = default;
~Database() = default;
@@ -118,8 +124,22 @@ class Database
static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
const std::string& server,
const std::string& channel);
+ static AfterConnectionCommands get_after_connection_commands(const IrcServerOptions& server_options);
+ static void set_after_connection_commands(const IrcServerOptions& server_options, AfterConnectionCommands& commands);
+
+ /**
+ * Get all the lines between (optional) start and end dates, with a (optional) limit.
+ * If after_id is set, only the records after it will be returned.
+ */
static std::vector<MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
- int limit=-1, const std::string& start="", const std::string& end="");
+ int limit=-1, const std::string& start="", const std::string& end="",
+ const Id::real_type reference_record_id=Id::unset_value, Paging=Paging::first);
+
+ /**
+ * Get just one single record matching the given uuid, between (optional) end and start.
+ * If it does not exist (or is not between end and start), throw a RecordNotFound exception.
+ */
+ static MucLogLine get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server, const std::string& uuid, const std::string& start="", const std::string& end="");
static std::string store_muc_message(const std::string& owner, const std::string& chan_name, const std::string& server_name,
time_point date, const std::string& body, const std::string& nick);
@@ -144,6 +164,8 @@ class Database
static IrcServerOptionsTable irc_server_options;
static IrcChannelOptionsTable irc_channel_options;
static RosterTable roster;
+ static AfterConnectionCommandsTable after_connection_commands;
+
static std::unique_ptr<DatabaseEngine> db;
/**
@@ -181,11 +203,20 @@ class Database
static auto raw_exec(const std::string& query)
{
- Database::db->raw_exec(query);
+ return Database::db->raw_exec(query);
}
private:
static std::string gen_uuid();
static std::map<CacheKey, EncodingIn::real_type> encoding_in_cache;
};
+
+class Transaction
+{
+public:
+ Transaction();
+ ~Transaction();
+ bool success{false};
+};
+
#endif /* USE_DATABASE */
diff --git a/src/database/delete_query.hpp b/src/database/delete_query.hpp
new file mode 100644
index 0000000..dce705b
--- /dev/null
+++ b/src/database/delete_query.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <database/query.hpp>
+#include <database/engine.hpp>
+
+class DeleteQuery: public Query
+{
+public:
+ DeleteQuery(const std::string& name):
+ Query("DELETE")
+ {
+ this->body += " from " + name;
+ }
+
+ DeleteQuery& where()
+ {
+ this->body += " WHERE ";
+ return *this;
+ };
+
+ void execute(DatabaseEngine& db)
+ {
+ auto statement = db.prepare(this->body);
+ if (!statement)
+ return;
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+ statement->bind(std::move(this->params));
+ if (statement->step() != StepResult::Done)
+ log_error("Failed to execute DELETE command");
+ }
+};
diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp
index 9726424..230e873 100644
--- a/src/database/insert_query.hpp
+++ b/src/database/insert_query.hpp
@@ -1,10 +1,15 @@
#pragma once
#include <database/statement.hpp>
+#include <database/database.hpp>
#include <database/column.hpp>
#include <database/query.hpp>
+#include <database/row.hpp>
+
#include <logger/logger.hpp>
+#include <utils/is_one_of.hpp>
+
#include <type_traits>
#include <vector>
#include <string>
@@ -22,7 +27,7 @@ update_autoincrement_id(std::tuple<T...>& columns, Statement& statement)
template <std::size_t N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
-update_autoincrement_id(std::tuple<T...>&, Statement& statement)
+update_autoincrement_id(std::tuple<T...>&, Statement&)
{}
struct InsertQuery: public Query
@@ -127,3 +132,13 @@ struct InsertQuery: public Query
insert_col_name(const std::tuple<T...>&)
{}
};
+
+template <typename... T>
+void insert(Row<T...>& row, DatabaseEngine& db)
+{
+ InsertQuery query(row.table_name, row.columns);
+ // Ugly workaround for non portable stuff
+ if (is_one_of<Id, T...>)
+ query.body += db.get_returning_id_sql_string(Id::name);
+ query.execute(db, row.columns);
+}
diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp
index 984a959..59bc885 100644
--- a/src/database/postgresql_engine.cpp
+++ b/src/database/postgresql_engine.cpp
@@ -11,6 +11,8 @@
#include <logger/logger.hpp>
+#include <cstring>
+
PostgresqlEngine::PostgresqlEngine(PGconn*const conn):
conn(conn)
{}
@@ -20,6 +22,15 @@ PostgresqlEngine::~PostgresqlEngine()
PQfinish(this->conn);
}
+static void logging_notice_processor(void*, const char* original)
+{
+ if (original && std::strlen(original) > 0)
+ {
+ std::string message{original, std::strlen(original) - 1};
+ log_warning("PostgreSQL: ", message);
+ }
+}
+
std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& conninfo)
{
PGconn* con = PQconnectdb(conninfo.data());
@@ -34,8 +45,10 @@ std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& connin
{
const char* errmsg = PQerrorMessage(con);
log_error("Postgresql connection failed: ", errmsg);
+ PQfinish(con);
throw std::runtime_error("failed to open connection.");
}
+ PQsetNoticeProcessor(con, &logging_notice_processor, nullptr);
return std::make_unique<PostgresqlEngine>(con);
}
diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp
index fe4fb53..1a9c249 100644
--- a/src/database/postgresql_engine.hpp
+++ b/src/database/postgresql_engine.hpp
@@ -36,12 +36,15 @@ private:
#else
+using namespace std::string_literals;
+
class PostgresqlEngine
{
public:
static std::unique_ptr<DatabaseEngine> open(const std::string& string)
{
throw std::runtime_error("Cannot open postgresql database "s + string + ": biboumi is not compiled with libpq.");
+ return {};
}
};
diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp
index 571c8f1..37e8ea0 100644
--- a/src/database/postgresql_statement.hpp
+++ b/src/database/postgresql_statement.hpp
@@ -6,6 +6,8 @@
#include <libpq-fe.h>
+#include <cstring>
+
class PostgresqlStatement: public Statement
{
public:
@@ -90,7 +92,7 @@ class PostgresqlStatement: public Statement
private:
private:
- bool execute()
+ bool execute(const bool second_attempt=false)
{
std::vector<const char*> params;
params.reserve(this->params.size());
@@ -108,8 +110,20 @@ private:
const auto status = PQresultStatus(this->result);
if (status != PGRES_TUPLES_OK && status != PGRES_COMMAND_OK)
{
- log_error("Failed to execute command: ", PQresultErrorMessage(this->result));
- return false;
+ const char* original = PQerrorMessage(this->conn);
+ if (original && std::strlen(original) > 0)
+ log_error("Failed to execute command: ", std::string{original, std::strlen(original) - 1});
+ if (PQstatus(this->conn) != CONNECTION_OK && !second_attempt)
+ {
+ log_info("Trying to reconnect to PostgreSQL server and execute the query again.");
+ PQreset(this->conn);
+ return this->execute(true);
+ }
+ else
+ {
+ log_error("Givin up.");
+ return false;
+ }
}
return true;
}
diff --git a/src/database/query.cpp b/src/database/query.cpp
index 4054007..d72066e 100644
--- a/src/database/query.cpp
+++ b/src/database/query.cpp
@@ -6,7 +6,7 @@ void actual_bind(Statement& statement, const std::string& value, int index)
statement.bind_text(index, value);
}
-void actual_bind(Statement& statement, const std::size_t value, int index)
+void actual_bind(Statement& statement, const std::int64_t& value, int index)
{
statement.bind_int64(index, value);
}
@@ -21,7 +21,6 @@ void actual_bind(Statement& statement, const OptionalBool& value, int index)
statement.bind_int64(index, -1);
}
-
void actual_add_param(Query& query, const std::string& val)
{
query.params.push_back(val);
diff --git a/src/database/query.hpp b/src/database/query.hpp
index 547138f..ba28b1a 100644
--- a/src/database/query.hpp
+++ b/src/database/query.hpp
@@ -12,7 +12,12 @@
#include <string>
void actual_bind(Statement& statement, const std::string& value, int index);
-void actual_bind(Statement& statement, const std::size_t value, int index);
+void actual_bind(Statement& statement, const std::int64_t& value, int index);
+template <typename T, typename std::enable_if_t<std::is_integral<T>::value>* = 0>
+void actual_bind(Statement& statement, const T& value, int index)
+{
+ actual_bind(statement, static_cast<std::int64_t>(value), index);
+}
void actual_bind(Statement& statement, const OptionalBool& value, int index);
#ifdef DEBUG_SQL_QUERIES
diff --git a/src/database/row.hpp b/src/database/row.hpp
index 2d55897..27caf43 100644
--- a/src/database/row.hpp
+++ b/src/database/row.hpp
@@ -1,9 +1,5 @@
#pragma once
-#include <database/insert_query.hpp>
-#include <database/update_query.hpp>
-#include <logger/logger.hpp>
-
#include <utils/is_one_of.hpp>
#include <type_traits>
@@ -29,43 +25,7 @@ struct Row
return col.value;
}
- template <bool Coucou=true>
- void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<!is_one_of<Id, T...>::value && Coucou>::type* = nullptr)
- {
- this->insert(*db);
- }
-
- template <bool Coucou=true>
- void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<is_one_of<Id, T...>::value && Coucou>::type* = nullptr)
- {
- const Id& id = std::get<Id>(this->columns);
- if (id.value == Id::unset_value)
- {
- this->insert(*db);
- std::get<Id>(this->columns).value = db->last_inserted_rowid;
- }
- else
- this->update(*db);
- }
-
- private:
- void insert(DatabaseEngine& db)
- {
- InsertQuery query(this->table_name, this->columns);
- // Ugly workaround for non portable stuff
- query.body += db.get_returning_id_sql_string(Id::name);
- query.execute(db, this->columns);
- }
-
- void update(DatabaseEngine& db)
- {
- UpdateQuery query(this->table_name, this->columns);
-
- query.execute(db, this->columns);
- }
-
public:
std::tuple<T...> columns;
std::string table_name;
-
};
diff --git a/src/database/save.hpp b/src/database/save.hpp
new file mode 100644
index 0000000..4362110
--- /dev/null
+++ b/src/database/save.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <database/update_query.hpp>
+#include <database/insert_query.hpp>
+
+#include <database/engine.hpp>
+
+#include <database/row.hpp>
+
+#include <utils/is_one_of.hpp>
+
+template <typename... T, bool Coucou=true>
+void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr)
+{
+ insert(row, db);
+}
+
+template <typename... T, bool Coucou=true>
+void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr)
+{
+ const Id& id = std::get<Id>(row.columns);
+ if (id.value == Id::unset_value)
+ {
+ insert(row, db);
+ if (db.last_inserted_rowid >= 0)
+ std::get<Id>(row.columns).value = static_cast<Id::real_type>(db.last_inserted_rowid);
+ }
+ else
+ update(row, db);
+}
+
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
index 5a17f38..b9fdc06 100644
--- a/src/database/select_query.hpp
+++ b/src/database/select_query.hpp
@@ -2,6 +2,8 @@
#include <database/engine.hpp>
+#include <database/table.hpp>
+#include <database/database.hpp>
#include <database/statement.hpp>
#include <database/query.hpp>
#include <logger/logger.hpp>
@@ -115,6 +117,8 @@ struct SelectQuery: public Query
#endif
auto statement = db.prepare(this->body);
+ if (!statement)
+ return rows;
statement->bind(std::move(this->params));
while (statement->step() == StepResult::Row)
@@ -130,3 +134,9 @@ struct SelectQuery: public Query
const std::string table_name;
};
+template <typename... T>
+auto select(const Table<T...> table)
+{
+ SelectQuery<T...> query(table.name);
+ return query;
+}
diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp
index ae4a146..5e3bba1 100644
--- a/src/database/sqlite3_engine.cpp
+++ b/src/database/sqlite3_engine.cpp
@@ -3,7 +3,6 @@
#ifdef SQLITE3_FOUND
#include <database/sqlite3_engine.hpp>
-
#include <database/sqlite3_statement.hpp>
#include <database/query.hpp>
diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp
index 5b8176c..a7bfcdb 100644
--- a/src/database/sqlite3_engine.hpp
+++ b/src/database/sqlite3_engine.hpp
@@ -35,12 +35,15 @@ private:
#else
+using namespace std::string_literals;
+
class Sqlite3Engine
{
public:
static std::unique_ptr<DatabaseEngine> open(const std::string& string)
{
throw std::runtime_error("Cannot open sqlite3 database "s + string + ": biboumi is not compiled with sqlite3 lib.");
+ return {};
}
};
diff --git a/src/database/sqlite3_statement.hpp b/src/database/sqlite3_statement.hpp
index 7738fa6..3ed60c0 100644
--- a/src/database/sqlite3_statement.hpp
+++ b/src/database/sqlite3_statement.hpp
@@ -88,5 +88,4 @@ class Sqlite3Statement: public Statement
private:
sqlite3_stmt* stmt;
- int last_step_result{SQLITE_OK};
};
diff --git a/src/database/table.hpp b/src/database/table.hpp
index 680e7cc..0b8bfc0 100644
--- a/src/database/table.hpp
+++ b/src/database/table.hpp
@@ -2,7 +2,7 @@
#include <database/engine.hpp>
-#include <database/select_query.hpp>
+#include <database/delete_query.hpp>
#include <database/row.hpp>
#include <algorithm>
@@ -79,10 +79,10 @@ class Table
return {this->name};
}
- auto select()
+ auto del()
{
- SelectQuery<T...> select(this->name);
- return select;
+ DeleteQuery query(this->name);
+ return query;
}
const std::string& get_name() const
@@ -90,6 +90,8 @@ class Table
return this->name;
}
+ const std::string name;
+
private:
template <std::size_t N=0>
@@ -124,5 +126,4 @@ class Table
add_column_create(DatabaseEngine&, std::string&)
{ }
- const std::string name;
};
diff --git a/src/database/update_query.hpp b/src/database/update_query.hpp
index a29ac3f..c2b819d 100644
--- a/src/database/update_query.hpp
+++ b/src/database/update_query.hpp
@@ -1,7 +1,8 @@
#pragma once
-#include <database/query.hpp>
#include <database/engine.hpp>
+#include <database/query.hpp>
+#include <database/row.hpp>
using namespace std::string_literals;
@@ -102,3 +103,11 @@ struct UpdateQuery: public Query
actual_bind(statement, value.value, sizeof...(T));
}
};
+
+template <typename... T>
+void update(Row<T...>& row, DatabaseEngine& db)
+{
+ UpdateQuery query(row.table_name, row.columns);
+
+ query.execute(db, row.columns);
+}
diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp
index 53043c7..2dd20fe 100644
--- a/src/irc/irc_channel.cpp
+++ b/src/irc/irc_channel.cpp
@@ -33,8 +33,9 @@ IrcUser* IrcChannel::find_user(const std::string& name) const
return nullptr;
}
-void IrcChannel::remove_user(const IrcUser* user)
+std::unique_ptr<IrcUser> IrcChannel::remove_user(const IrcUser* user)
{
+ std::unique_ptr<IrcUser> result{};
const auto nick = user->nick;
const bool is_self = (user == this->self);
const auto it = std::find_if(this->users.begin(), this->users.end(),
@@ -44,6 +45,7 @@ void IrcChannel::remove_user(const IrcUser* user)
});
if (it != this->users.end())
{
+ result = std::move(*it);
this->users.erase(it);
if (is_self)
{
@@ -51,16 +53,5 @@ void IrcChannel::remove_user(const IrcUser* user)
this->joined = false;
}
}
-}
-
-void IrcChannel::remove_all_users()
-{
- this->users.clear();
- this->self = nullptr;
-}
-
-DummyIrcChannel::DummyIrcChannel():
- IrcChannel(),
- joining(false)
-{
+ return result;
}
diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp
index 8f85edb..7000ada 100644
--- a/src/irc/irc_channel.hpp
+++ b/src/irc/irc_channel.hpp
@@ -32,8 +32,7 @@ public:
IrcUser* add_user(const std::string& name,
const std::map<char, char>& prefix_to_mode);
IrcUser* find_user(const std::string& name) const;
- void remove_user(const IrcUser* user);
- void remove_all_users();
+ std::unique_ptr<IrcUser> remove_user(const IrcUser* user);
const std::vector<std::unique_ptr<IrcUser>>& get_users() const
{ return this->users; }
@@ -42,33 +41,3 @@ protected:
IrcUser* self{nullptr};
std::vector<std::unique_ptr<IrcUser>> users{};
};
-
-/**
- * A special channel that is not actually linked to any real irc
- * channel. This is just a channel representing a connection to the
- * server. If an user wants to maintain the connection to the server without
- * having to be on any irc channel of that server, he can just join this
- * dummy channel.
- * It’s not actually dummy because it’s useful and it does things, but well.
- */
-class DummyIrcChannel: public IrcChannel
-{
-public:
- explicit DummyIrcChannel();
- DummyIrcChannel(const DummyIrcChannel&) = delete;
- DummyIrcChannel(DummyIrcChannel&&) = delete;
- DummyIrcChannel& operator=(const DummyIrcChannel&) = delete;
- DummyIrcChannel& operator=(DummyIrcChannel&&) = delete;
-
- /**
- * This flag is at true whenever the user wants to join this channel, but
- * he is not yet connected to the server. When the connection is made, we
- * check that flag and if it’s true, we inform the user that he has just
- * joined that channel.
- * If the user is already connected to the server when he tries to join
- * the channel, we don’t use that flag, we just join it immediately.
- */
- bool joining;
-};
-
-
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index 40078d9..8f77e0d 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -145,14 +145,6 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
chanmodes({"", "", "", ""}),
chantypes({'#', '&'})
{
- this->dummy_channel.topic = "This is a virtual channel provided for "
- "convenience by biboumi, it is not connected "
- "to any actual IRC channel of the server '" + this->hostname +
- "', and sending messages in it has no effect. "
- "Its main goal is to keep the connection to the IRC server "
- "alive without having to join a real channel of that server. "
- "To disconnect from the IRC server, leave this room and all "
- "other IRC channels of that server.";
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
@@ -194,20 +186,22 @@ void IrcClient::start()
bool tls;
std::tie(port, tls) = this->ports_to_try.top();
this->ports_to_try.pop();
- this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " +
- this->hostname + ":" + port + " (" +
- (tls ? "encrypted" : "not encrypted") + ")");
-
this->bind_addr = Config::get("outgoing_bind", "");
+ std::string address = this->hostname;
-#ifdef BOTAN_FOUND
-# ifdef USE_DATABASE
+#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
+# ifdef BOTAN_FOUND
this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
+ if (!options.col<Database::Address>().empty())
+ address = options.col<Database::Address>();
#endif
- this->connect(this->hostname, port, tls);
+ this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " +
+ address + ":" + port + " (" +
+ (tls ? "encrypted" : "not encrypted") + ")");
+ this->connect(address, port, tls);
}
void IrcClient::on_connection_failed(const std::string& reason)
@@ -315,8 +309,6 @@ void IrcClient::on_connection_close(const std::string& error_msg)
IrcChannel* IrcClient::get_channel(const std::string& n)
{
- if (n.empty())
- return &this->dummy_channel;
const std::string name = utils::tolower(n);
try
{
@@ -670,10 +662,7 @@ void IrcClient::on_channel_join(const IrcMessage& message)
{
const std::string chan_name = utils::tolower(message.arguments[0]);
IrcChannel* channel;
- if (chan_name.empty())
- channel = &this->dummy_channel;
- else
- channel = this->get_channel(chan_name);
+ channel = this->get_channel(chan_name);
const std::string nick = message.prefix;
IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
if (channel->joined == false)
@@ -900,8 +889,9 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- if (!options.col<Database::AfterConnectionCommand>().empty())
- this->send_raw(options.col<Database::AfterConnectionCommand>());
+ const auto commands = Database::get_after_connection_commands(options);
+ for (const auto& command: commands)
+ this->send_raw(command.col<Database::AfterConnectionCommand>());
#endif
// Install a repeated events to regularly send a PING
TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
@@ -948,18 +938,6 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
if (!channels_with_key.empty())
this->send_join_command(channels_with_key, keys);
this->channels_to_join.clear();
- // Indicate that the dummy channel is joined as well, if needed
- if (this->dummy_channel.joining)
- {
- // Simulate a message coming from the IRC server saying that we joined
- // the channel
- const IrcMessage join_message(this->get_nick(), "JOIN", {""});
- this->on_channel_join(join_message);
- const IrcMessage end_join_message(std::string(this->hostname), "366",
- {this->get_nick(),
- "", "End of NAMES list"});
- this->on_channel_completely_joined(end_join_message);
- }
}
void IrcClient::on_part(const IrcMessage& message)
@@ -976,18 +954,18 @@ void IrcClient::on_part(const IrcMessage& message)
{
std::string nick = user->nick;
bool self = channel->get_self() && channel->get_self()->nick == nick;
- channel->remove_user(user);
- Iid iid;
- iid.set_local(chan_name);
- iid.set_server(this->hostname);
- iid.type = Iid::Type::Channel;
+ auto user_ptr = channel->remove_user(user);
if (self)
{
this->channels.erase(utils::tolower(chan_name));
// channel pointer is now invalid
channel = nullptr;
}
- this->bridge.send_muc_leave(iid, std::move(nick), txt, self, true);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.type = Iid::Type::Channel;
+ this->bridge.send_muc_leave(iid, *user_ptr, txt, self, true, {}, this);
}
}
@@ -1004,8 +982,7 @@ void IrcClient::on_error(const IrcMessage& message)
IrcChannel* channel = pair.second.get();
if (!channel->joined)
continue;
- std::string own_nick = channel->get_self()->nick;
- this->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true, false);
+ this->bridge.send_muc_leave(iid, *channel->get_self(), leave_message, true, false, {}, this);
}
this->channels.clear();
this->send_gateway_message("ERROR: " + leave_message);
@@ -1030,7 +1007,7 @@ void IrcClient::on_quit(const IrcMessage& message)
iid.set_local(chan_name);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
- this->bridge.send_muc_leave(iid, user->nick, txt, self, false);
+ this->bridge.send_muc_leave(iid, *user, txt, self, false, {}, this);
channel->remove_user(user);
}
}
@@ -1062,10 +1039,6 @@ void IrcClient::on_nick(const IrcMessage& message)
}
};
- if (this->get_dummy_channel().joined)
- {
- change_nick_func("", &this->get_dummy_channel());
- }
for (const auto& pair: this->channels)
{
change_nick_func(pair.first, pair.second.get());
@@ -1248,25 +1221,7 @@ void IrcClient::on_unknown_message(const IrcMessage& message)
size_t IrcClient::number_of_joined_channels() const
{
- if (this->dummy_channel.joined)
- return this->channels.size() + 1;
- else
- return this->channels.size();
-}
-
-DummyIrcChannel& IrcClient::get_dummy_channel()
-{
- return this->dummy_channel;
-}
-
-void IrcClient::leave_dummy_channel(const std::string& exit_message, const std::string& resource)
-{
- if (!this->dummy_channel.joined)
- return;
- this->dummy_channel.joined = false;
- this->dummy_channel.joining = false;
- this->dummy_channel.remove_all_users();
- this->bridge.send_muc_leave(Iid("%" + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, true, resource);
+ return this->channels.size();
}
#ifdef BOTAN_FOUND
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
index de5c520..fd97fe6 100644
--- a/src/irc/irc_client.hpp
+++ b/src/irc/irc_client.hpp
@@ -279,15 +279,6 @@ public:
* Return the number of joined channels
*/
size_t number_of_joined_channels() const;
- /**
- * Get a reference to the unique dummy channel
- */
- DummyIrcChannel& get_dummy_channel();
- /**
- * Leave the dummy channel: forward a message to the user to indicate that
- * he left it, and mark it as not joined.
- */
- void leave_dummy_channel(const std::string& exit_message, const std::string& resource);
const std::string& get_hostname() const { return this->hostname; }
std::string get_nick() const { return this->current_nick; }
@@ -340,11 +331,6 @@ private:
*/
std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
/**
- * A single channel with a iid of the form "hostname" (normal channel have
- * an iid of the form "chan%hostname".
- */
- DummyIrcChannel dummy_channel;
- /**
* A list of chan we want to join (tuples with the channel name and the
* password, if any), but we need a response 001 from the server before
* sending the actual JOIN commands. So we just keep the channel names in
diff --git a/src/main.cpp b/src/main.cpp
index c877e43..59fda4e 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -135,7 +135,9 @@ int main(int ac, char** av)
std::make_shared<BiboumiComponent>(p, hostname, password);
xmpp_component->start();
- IdentdServer identd(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
+ std::unique_ptr<IdentdServer> identd;
+ if (Config::get_int("identd_port", 113) != 0)
+ identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
auto timeout = TimedEventsManager::instance().get_timeout();
while (p->poll(timeout) != -1)
@@ -144,7 +146,8 @@ int main(int ac, char** av)
// Check for empty irc_clients (not connected, or with no joined
// channel) and remove them
xmpp_component->clean();
- identd.clean();
+ if (identd)
+ identd->clean();
if (stop)
{
log_info("Signal received, exiting...");
@@ -157,7 +160,8 @@ int main(int ac, char** av)
#ifdef UDNS_FOUND
dns_handler.destroy();
#endif
- identd.shutdown();
+ if (identd)
+ identd->shutdown();
// Cancel the timer for a potential reconnection
TimedEventsManager::instance().cancel("XMPP reconnection");
}
@@ -199,7 +203,8 @@ int main(int ac, char** av)
#ifdef UDNS_FOUND
dns_handler.destroy();
#endif
- identd.shutdown();
+ if (identd)
+ identd->shutdown();
}
}
// If the only existing connection is the one to the XMPP component:
diff --git a/src/utils/is_one_of.hpp b/src/utils/is_one_of.hpp
index c706421..4d6770e 100644
--- a/src/utils/is_one_of.hpp
+++ b/src/utils/is_one_of.hpp
@@ -3,12 +3,15 @@
#include <type_traits>
template <typename...>
-struct is_one_of {
+struct is_one_of_implem {
static constexpr bool value = false;
};
template <typename F, typename S, typename... T>
-struct is_one_of<F, S, T...> {
+struct is_one_of_implem<F, S, T...> {
static constexpr bool value =
- std::is_same<F, S>::value || is_one_of<F, T...>::value;
+ std::is_same<F, S>::value || is_one_of_implem<F, T...>::value;
};
+
+template<typename... T>
+constexpr bool is_one_of = is_one_of_implem<T...>::value;
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
index bc2c18d..71306fd 100644
--- a/src/utils/time.cpp
+++ b/src/utils/time.cpp
@@ -9,9 +9,10 @@
namespace utils
{
-std::string to_string(const std::time_t& timestamp)
+std::string to_string(const std::chrono::system_clock::time_point::rep& time)
{
constexpr std::size_t stamp_size = 21;
+ const std::time_t timestamp = static_cast<std::time_t>(time);
char date_buf[stamp_size];
if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(&timestamp)) != stamp_size - 1)
return "";
diff --git a/src/utils/time.hpp b/src/utils/time.hpp
index c71cd9c..4b19634 100644
--- a/src/utils/time.hpp
+++ b/src/utils/time.hpp
@@ -2,9 +2,10 @@
#include <ctime>
#include <string>
+#include <chrono>
namespace utils
{
-std::string to_string(const std::time_t& timestamp);
+std::string to_string(const std::chrono::system_clock::time_point::rep& timestamp);
std::time_t parse_datetime(const std::string& stamp);
-} \ No newline at end of file
+}
diff --git a/src/utils/uuid.cpp b/src/utils/uuid.cpp
new file mode 100644
index 0000000..23b71fe
--- /dev/null
+++ b/src/utils/uuid.cpp
@@ -0,0 +1,14 @@
+#include <utils/uuid.hpp>
+#include <uuid/uuid.h>
+
+namespace utils
+{
+std::string gen_uuid()
+{
+ char uuid_str[37];
+ uuid_t uuid;
+ uuid_generate(uuid);
+ uuid_unparse(uuid, uuid_str);
+ return uuid_str;
+}
+}
diff --git a/src/utils/uuid.hpp b/src/utils/uuid.hpp
new file mode 100644
index 0000000..d550475
--- /dev/null
+++ b/src/utils/uuid.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <string>
+
+namespace utils
+{
+std::string gen_uuid();
+}
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
index bb48781..bc4c108 100644
--- a/src/xmpp/adhoc_commands_handler.cpp
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -41,7 +41,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
XmlSubNode condition(error, STANZA_NS":item-not-found");
}
else if (command_it->second.is_admin_only() &&
- Config::get("admin", "") != jid.local + "@" + jid.domain)
+ !Config::is_in_list("admin", jid.bare()))
{
XmlSubNode error(command_node, ADHOC_NS":error");
error["type"] = "cancel";
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index bcdac39..53806d6 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -14,6 +14,7 @@
#ifdef USE_DATABASE
#include <database/database.hpp>
+#include <database/save.hpp>
#endif
#ifndef HAS_PUT_TIME
@@ -196,7 +197,7 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
options.col<Database::GlobalPersistent>() = to_bool(value->get_inner());
}
- options.save(Database::db);
+ save(options, *Database::db);
command_node.delete_all_children();
XmlSubNode note(command_node, "note");
@@ -219,6 +220,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
server_domain = target.local;
auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
server_domain);
+ auto commands = Database::get_after_connection_commands(options);
XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
@@ -228,6 +230,19 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
instructions.set_inner("Edit the form, to configure the settings of the IRC server " + server_domain);
{
+ XmlSubNode field(x, "field");
+ field["var"] = "address";
+ field["type"] = "text-single";
+ field["label"] = "Address";
+ field["desc"] = "The address (hostname or IP) to connect to.";
+ XmlSubNode value(field, "value");
+ if (options.col<Database::Address>().empty())
+ value.set_inner(server_domain);
+ else
+ value.set_inner(options.col<Database::Address>());
+ }
+
+ {
XmlSubNode ports(x, "field");
ports["var"] = "ports";
ports["type"] = "text-multi";
@@ -279,6 +294,20 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
}
}
#endif
+
+ {
+ XmlSubNode field(x, "field");
+ field["var"] = "nick";
+ field["type"] = "text-single";
+ field["label"] = "Nickname";
+ field["desc"] = "If set, will override the nickname provided in the initial presence sent to join the first server channel";
+ if (!options.col<Database::Nick>().empty())
+ {
+ XmlSubNode value(field, "value");
+ value.set_inner(options.col<Database::Nick>());
+ }
+ }
+
{
XmlSubNode pass(x, "field");
pass["var"] = "pass";
@@ -294,14 +323,14 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
{
XmlSubNode after_cnt_cmd(x, "field");
- after_cnt_cmd["var"] = "after_connect_command";
- after_cnt_cmd["type"] = "text-single";
- after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
- after_cnt_cmd["label"] = "After-connection IRC command";
- if (!options.col<Database::AfterConnectionCommand>().empty())
+ after_cnt_cmd["var"] = "after_connect_commands";
+ after_cnt_cmd["type"] = "text-multi";
+ after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server.";
+ after_cnt_cmd["label"] = "After-connection IRC commands";
+ for (const auto& command: commands)
{
XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
- after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
+ after_cnt_cmd_value.set_inner(command.col<Database::AfterConnectionCommand>());
}
}
@@ -371,10 +400,16 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
server_domain = target.local;
auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
server_domain);
+ auto commands = Database::get_after_connection_commands(options);
+
for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
{
const XmlNode* value = field->get_child("value", "jabber:x:data");
const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data");
+
+ if (field->get_tag("var") == "address" && value)
+ options.col<Database::Address>() = value->get_inner();
+
if (field->get_tag("var") == "ports")
{
std::string ports;
@@ -406,11 +441,22 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
#endif // BOTAN_FOUND
+ else if (field->get_tag("var") == "nick" && value)
+ options.col<Database::Nick>() = value->get_inner();
+
else if (field->get_tag("var") == "pass" && value)
options.col<Database::Pass>() = value->get_inner();
- else if (field->get_tag("var") == "after_connect_command" && value)
- options.col<Database::AfterConnectionCommand>() = value->get_inner();
+ else if (field->get_tag("var") == "after_connect_commands")
+ {
+ commands.clear();
+ for (const auto& val: values)
+ {
+ auto command = Database::after_connection_commands.row();
+ command.col<Database::AfterConnectionCommand>() = val->get_inner();
+ commands.push_back(std::move(command));
+ }
+ }
else if (field->get_tag("var") == "username" && value)
{
@@ -431,7 +477,8 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
}
Database::invalidate_encoding_in_cache();
- options.save(Database::db);
+ save(options, *Database::db);
+ Database::set_after_connection_commands(options, commands);
command_node.delete_all_children();
XmlSubNode note(command_node, "note");
@@ -600,7 +647,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
}
Database::invalidate_encoding_in_cache(requester.bare(), iid.get_server(), iid.get_local());
- options.save(Database::db);
+ save(options, *Database::db);
}
return true;
}
@@ -611,7 +658,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
{
const Jid owner(session.get_owner_jid());
- if (owner.bare() != Config::get("admin", ""))
+ if (!Config::is_in_list("admin", owner.bare()))
{ // A non-admin is not allowed to disconnect other users, only
// him/herself, so we just skip this step
auto next_step = session.get_next_step();
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index 8775869..6dc5fc5 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -7,6 +7,7 @@
#include <xmpp/adhoc_command.hpp>
#include <xmpp/biboumi_adhoc_commands.hpp>
#include <bridge/list_element.hpp>
+#include <utils/encoding.hpp>
#include <config/config.hpp>
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
@@ -147,13 +148,10 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
try {
if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
- { // presence toward a MUC that corresponds to an irc channel, or a
- // dummy channel if iid.chan is empty
+ { // presence toward a MUC that corresponds to an irc channel
if (type.empty())
{
const std::string own_nick = bridge->get_own_nick(iid);
- if (!own_nick.empty() && own_nick != to.resource)
- bridge->send_irc_nick_change(iid, to.resource, from.resource);
const XmlNode* x = stanza.get_child("x", MUC_NS);
const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr;
@@ -181,7 +179,9 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
history_limit.stanzas = 0;
}
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
- from.resource, history_limit);
+ from.resource, history_limit, x != nullptr);
+ if (!own_nick.empty() && own_nick != to.resource)
+ bridge->send_irc_nick_change(iid, to.resource, from.resource);
}
else if (type == "unavailable")
{
@@ -279,7 +279,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
{
if (body && !body->get_inner().empty())
{
- bridge->send_channel_message(iid, body->get_inner());
+ bridge->send_channel_message(iid, body->get_inner(), id);
}
const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
if (subject)
@@ -466,8 +466,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
#ifdef USE_DATABASE
else if ((query = stanza.get_child("query", MAM_NS)))
{
- if (this->handle_mam_request(stanza))
- stanza_error.disable();
+ try {
+ if (this->handle_mam_request(stanza))
+ stanza_error.disable();
+ } catch (const Database::RecordNotFound& exc) {
+ error_name = "item-not-found";
+ return;
+ }
}
else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
{
@@ -546,24 +551,21 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
if (to.local.empty())
{ // Get biboumi's adhoc commands
this->send_adhoc_commands_list(id, from, this->served_hostname,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->adhoc_commands_handler);
stanza_error.disable();
}
else if (iid.type == Iid::Type::Server)
{ // Get the server's adhoc commands
this->send_adhoc_commands_list(id, from, to_str,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->irc_server_adhoc_commands_handler);
stanza_error.disable();
}
else if (iid.type == Iid::Type::Channel && to.resource.empty())
{ // Get the channel's adhoc commands
this->send_adhoc_commands_list(id, from, to_str,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->irc_channel_adhoc_commands_handler);
stanza_error.disable();
}
@@ -714,29 +716,58 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
}
const XmlNode* set = query->get_child("set", RSM_NS);
int limit = -1;
+ Id::real_type reference_record_id{Id::unset_value};
+ Database::Paging paging_order{Database::Paging::first};
if (set)
{
const XmlNode* max = set->get_child("max", RSM_NS);
if (max)
limit = std::atoi(max->get_inner().data());
+ const XmlNode* after = set->get_child("after", RSM_NS);
+ if (after)
+ {
+ auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(),
+ after->get_inner(), start, end);
+ reference_record_id = after_record.col<Id>();
+ }
+ const XmlNode* before = set->get_child("before", RSM_NS);
+ if (before)
+ {
+ paging_order = Database::Paging::last;
+ if (!before->get_inner().empty())
+ {
+ auto before_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), before->get_inner(), start, end);
+ reference_record_id = before_record.col<Id>();
+ }
+ }
}
- // If the archive is really big, and the client didn’t specify any
- // limit, we avoid flooding it: we set an arbitrary max limit.
- if (limit == -1 && start.empty() && end.empty())
+ // Do not send more than 100 messages, even if the client asked for more,
+ // or if it didn’t specify any limit.
+ // 101 is just a trick to know if there are more available messages.
+ // If our query returns 101 message, we know it’s incomplete, but we
+ // still send only 100
+ if ((limit == -1 && start.empty() && end.empty())
+ || limit > 100)
+ limit = 101;
+ auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end, reference_record_id, paging_order);
+ bool complete = true;
+ if (lines.size() > 100)
{
- limit = 100;
+ complete = false;
+ lines.erase(lines.begin(), std::prev(lines.end(), 100));
}
- const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
for (const Database::MucLogLine& line: lines)
- {
- if (!line.col<Database::Nick>().empty())
- this->send_archived_message(line, to.full(), from.full(), query_id);
- }
+ {
+ if (!line.col<Database::Nick>().empty())
+ this->send_archived_message(line, to.full(), from.full(), query_id);
+ }
{
auto fin_ptr = std::make_unique<XmlNode>("fin");
{
XmlNode& fin = *(fin_ptr.get());
fin["xmlns"] = MAM_NS;
+ if (complete)
+ fin["complete"] = "true";
XmlSubNode set(fin, "set");
set["xmlns"] = RSM_NS;
if (!lines.empty())
@@ -881,7 +912,7 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st
identity["category"] = "conference";
identity["type"] = "irc";
identity["name"] = "Biboumi XMPP-IRC gateway";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
@@ -905,7 +936,7 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s
identity["category"] = "conference";
identity["type"] = "irc";
identity["name"] = "IRC server " + from.local + " over Biboumi";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
@@ -947,8 +978,8 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const
XmlSubNode identity(query, "identity");
identity["category"] = "conference";
identity["type"] = "irc";
- identity["name"] = "IRC channel " + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ identity["name"] = ""s + iid.get_local() + " on " + iid.get_server();
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
@@ -1010,7 +1041,9 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std
for (auto it = begin; it != end; ++it)
{
XmlSubNode item(query, "item");
- item["jid"] = it->channel + "@" + this->served_hostname;
+ std::string channel_name = it->channel;
+ xep0106::encode(channel_name);
+ item["jid"] = channel_name + "@" + this->served_hostname;
}
if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
index 8f6826e..b3d925e 100644
--- a/src/xmpp/xmpp_component.cpp
+++ b/src/xmpp/xmpp_component.cpp
@@ -2,6 +2,7 @@
#include <utils/scopeguard.hpp>
#include <utils/tolower.hpp>
#include <logger/logger.hpp>
+#include <utils/uuid.hpp>
#include <xmpp/xmpp_component.hpp>
#include <config/config.hpp>
@@ -14,8 +15,6 @@
#include <iostream>
#include <set>
-#include <uuid/uuid.h>
-
#include <cstdlib>
#include <set>
@@ -364,10 +363,11 @@ void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, cons
this->send_stanza(message);
}
-void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid)
+void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid, std::string id)
{
Stanza message("message");
message["to"] = jid_to;
+ message["id"] = std::move(id);
if (!nick.empty())
message["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
else // Message from the room itself
@@ -398,7 +398,8 @@ void XmppComponent::send_muc_message(const std::string& muc_name, const std::str
this->send_stanza(message);
}
-void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, std::time_t timestamp)
+#ifdef USE_DATABASE
+void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, Database::time_point::rep timestamp)
{
Stanza message("message");
message["to"] = jid_to;
@@ -421,9 +422,11 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std:
this->send_stanza(message);
}
+#endif
void XmppComponent::send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message,
- const std::string& jid_to, const bool self, const bool user_requested)
+ const std::string& jid_to, const bool self, const bool user_requested,
+ const std::string& affiliation, const std::string& role)
{
Stanza presence("presence");
{
@@ -445,6 +448,9 @@ void XmppComponent::send_muc_leave(const std::string& muc_name, const std::strin
status["code"] = "332";
}
}
+ XmlSubNode item(x, "item");
+ item["affiliation"] = affiliation;
+ item["role"] = role;
if (!message_str.empty())
{
XmlSubNode status(presence, "status");
@@ -667,9 +673,5 @@ void XmppComponent::send_iq_result(const std::string& id, const std::string& to_
std::string XmppComponent::next_id()
{
- char uuid_str[37];
- uuid_t uuid;
- uuid_generate(uuid);
- uuid_unparse(uuid, uuid_str);
- return uuid_str;
+ return utils::gen_uuid();
}
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
index 3950863..e18da40 100644
--- a/src/xmpp/xmpp_component.hpp
+++ b/src/xmpp/xmpp_component.hpp
@@ -1,8 +1,10 @@
#pragma once
+#include "biboumi.h"
#include <xmpp/adhoc_commands_handler.hpp>
#include <network/tcp_client_socket_handler.hpp>
+#include <database/database.hpp>
#include <xmpp/xmpp_parser.hpp>
#include <xmpp/body.hpp>
@@ -35,6 +37,7 @@
#define RSM_NS "http://jabber.org/protocol/rsm"
#define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic"
#define STABLE_ID_NS "urn:xmpp:sid:0"
+#define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id"
/**
* An XMPP component, communicating with an XMPP server using the protocole
@@ -132,12 +135,14 @@ public:
* Send a (non-private) message to the MUC
*/
void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to,
- std::string uuid);
+ std::string uuid, std::string id);
+#ifdef USE_DATABASE
/**
* Send a message, with a <delay/> element, part of a MUC history
*/
void send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body,
- const std::string& jid_to, const std::time_t timestamp);
+ const std::string& jid_to, Database::time_point::rep timestamp);
+#endif
/**
* Send an unavailable presence for this nick
*/
@@ -146,7 +151,8 @@ public:
Xmpp::body&& message,
const std::string& jid_to,
const bool self,
- const bool user_requested);
+ const bool user_requested,
+ const std::string& affiliation, const std::string& role);
/**
* Indicate that a participant changed his nick
*/
diff --git a/tests/database.cpp b/tests/database.cpp
index 7ab6da8..15c117b 100644
--- a/tests/database.cpp
+++ b/tests/database.cpp
@@ -7,6 +7,7 @@
#include <cstdlib>
#include <database/database.hpp>
+#include <database/save.hpp>
#include <config/config.hpp>
@@ -28,11 +29,11 @@ TEST_CASE("Database")
{
auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
CHECK(Database::count(Database::irc_server_options) == 0);
- o.save(Database::db);
+ save(o, *Database::db);
CHECK(Database::count(Database::irc_server_options) == 1);
o.col<Database::Realname>() = "Different realname";
CHECK(o.col<Database::Realname>() == "Different realname");
- o.save(Database::db);
+ save(o, *Database::db);
CHECK(o.col<Database::Realname>() == "Different realname");
CHECK(Database::count(Database::irc_server_options) == 1);
@@ -44,7 +45,7 @@ TEST_CASE("Database")
// inserted
CHECK(1 == Database::count(Database::irc_server_options));
- b.save(Database::db);
+ save(b, *Database::db);
CHECK(2 == Database::count(Database::irc_server_options));
CHECK(b.col<Database::Pass>() == "");
@@ -58,7 +59,7 @@ TEST_CASE("Database")
o.col<Database::EncodingIn>() = "ISO-8859-1";
CHECK(o.col<Database::RecordHistoryOptional>().is_set == false);
o.col<Database::RecordHistoryOptional>().set_value(false);
- o.save(Database::db);
+ save(o, *Database::db);
auto b = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
CHECK(o.col<Database::EncodingIn>() == "ISO-8859-1");
CHECK(o.col<Database::RecordHistoryOptional>().is_set == true);
@@ -77,7 +78,7 @@ TEST_CASE("Database")
GIVEN("An option defined for the channel but not the server")
{
c.col<Database::EncodingIn>() = "channelEncoding";
- c.save(Database::db);
+ save(c, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -88,7 +89,7 @@ TEST_CASE("Database")
GIVEN("An option defined for the server but not the channel")
{
s.col<Database::EncodingIn>() = "serverEncoding";
- s.save(Database::db);
+ save(s, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -99,9 +100,9 @@ TEST_CASE("Database")
GIVEN("An option defined for both the server and the channel")
{
s.col<Database::EncodingIn>() = "serverEncoding";
- s.save(Database::db);
+ save(s, *Database::db);
c.col<Database::EncodingIn>() = "channelEncoding";
- c.save(Database::db);
+ save(c, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -117,6 +118,49 @@ TEST_CASE("Database")
}
}
+ SECTION("Server options")
+ {
+ const std::string owner{"toto@example.com"};
+ const std::string owner2{"toto2@example.com"};
+ const std::string server{"irc.example.com"};
+
+ auto soptions = Database::get_irc_server_options(owner, server);
+ auto soptions2 = Database::get_irc_server_options(owner2, server);
+
+ auto after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.empty());
+
+ save(soptions, *Database::db);
+ save(soptions2, *Database::db);
+ auto com = Database::after_connection_commands.row();
+ com.col<Database::AfterConnectionCommand>() = "first";
+ after_connection_commands.push_back(com);
+ com.col<Database::AfterConnectionCommand>() = "second";
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions, after_connection_commands);
+
+ after_connection_commands.clear();
+ com.col<Database::AfterConnectionCommand>() = "first";
+ after_connection_commands.push_back(com);
+ com.col<Database::AfterConnectionCommand>() = "second";
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions2, after_connection_commands);
+
+ after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.size() == 2);
+ after_connection_commands = Database::get_after_connection_commands(soptions2);
+ CHECK(after_connection_commands.size() == 2);
+
+ after_connection_commands.clear();
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions, after_connection_commands);
+
+ after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.size() == 1);
+ after_connection_commands = Database::get_after_connection_commands(soptions2);
+ CHECK(after_connection_commands.size() == 2);
+ }
+
Database::close();
}
#endif
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index 649e073..82321eb 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -152,7 +152,7 @@ def check_xpath(xpaths, xmpp, after, stanza):
xpath = xpath[1:]
matched = match(stanza, xpath)
if (expected and not matched) or (not expected and matched):
- raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, real_xpath))
+ raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath))
if after:
if isinstance(after, collections.Iterable):
for af in after:
@@ -270,11 +270,13 @@ def send_stanza(stanza, xmpp, biboumi):
def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None):
+ replacements = common_replacements
+ replacements.update(xmpp.saved_values)
check_func = check_xpath if not optional else check_xpath_optional
if isinstance(xpaths, str):
- xmpp.stanza_checker = partial(check_func, [xpaths.format_map(common_replacements)], xmpp, after)
+ xmpp.stanza_checker = partial(check_func, [xpaths.format_map(replacements)], xmpp, after)
elif isinstance(xpaths, tuple):
- xmpp.stanza_checker = partial(check_func, [xpath.format_map(common_replacements) for xpath in xpaths], xmpp, after)
+ xmpp.stanza_checker = partial(check_func, [xpath.format_map(replacements) for xpath in xpaths], xmpp, after)
else:
print("Warning, from argument type passed to expect_stanza: %s" % (type(xpaths)))
@@ -661,23 +663,6 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#baz%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
- Scenario("virtual_channel",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_one}']"),
- ]),
Scenario("not_connected_error",
[
handshake_sequence(),
@@ -694,40 +679,39 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
- Scenario("irc_server_disconnection",
+ Scenario("channel_join_with_two_users",
[
handshake_sequence(),
+ # First user joins
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
+ "/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'][@jid='~nick@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ # Second user joins
+ 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[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='{nick_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']",
- "/presence/muc_user:x/muc_user:status[@code='303']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
]),
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"),
]),
- Scenario("channel_join_with_two_users",
+ Scenario("channel_force_join",
[
handshake_sequence(),
# First user joins
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
@@ -739,7 +723,7 @@ if __name__ == '__main__':
# Second user joins
partial(send_stanza,
- "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
partial(expect_unordered, [
("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
@@ -748,6 +732,31 @@ if __name__ == '__main__':
"/presence/muc_user:x/muc_user:status[@code='110']",),
("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
]),
+
+ # Here we simulate a desynchronization of a client: The client thinks it’s
+ # disconnected from the room, but biboumi still thinks it’s in the room. The
+ # client thus sends a join presence, and biboumi should send everything
+ # (user list, history, etc) in response.
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
+
+ partial(expect_unordered, [
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
+ ("/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']",),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
+ ]),
+ # And also, that was not the same nickname
+ partial(expect_unordered, [
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']",
+ "/presence/muc_user:x/muc_user:status[@code='303']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']",),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']",
+ "/presence/muc_user:x/muc_user:status[@code='303']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ]),
]),
Scenario("channel_join_with_password",
[
@@ -1192,7 +1201,8 @@ if __name__ == '__main__':
"<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']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
+ "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']")),
# 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>"),
@@ -1244,6 +1254,64 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>NOTICE {nick_one} :[#foo] Hello in a notice.</body></message>"),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='[notice] [#foo] Hello in a notice.']"),
]),
+ Scenario("multiline_message",
+ [
+ handshake_sequence(),
+ # First user joins
+ 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 multi-line channel message
+ partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"),
+ # Receive multiple messages, in order
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"),
+
+ # Send a simple message, with no id
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"),
+
+ # Expect a non-empty id as a result (should be a uuid)
+ partial(expect_stanza,
+ "!/message[@id='']/body[text()='hello']"),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ partial(expect_unordered, [
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",)
+ ]),
+
+ # Send a multi-line channel message
+ partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"),
+ # Receive multiple messages, for each user
+ partial(expect_unordered, [
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']",),
+
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='un']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='deux']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='trois']",),
+ ])
+ ]),
Scenario("channel_messages",
[
handshake_sequence(),
@@ -1276,8 +1344,10 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
# Receive the message, forwarded to the two users
partial(expect_unordered, [
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']",)
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]"),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]")
]),
# Send a private message, to a in-room JID
@@ -1294,27 +1364,27 @@ if __name__ == '__main__':
## Do the exact same thing, from a different chan,
# to check if the response comes from the right JID
- # Join the virtual channel
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
partial(expect_stanza,
"/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject"),
+ partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}'][@type='groupchat']/subject"),
# Send a private message, to a in-room JID
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"),
# Message is received with a server-wide JID
partial(expect_stanza, "/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='re in private']"),
# Respond to the message, to the server-wide JID
partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>re</body></message>"),
# The response is received from the in-room JID
- partial(expect_stanza, "/message[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"),
+ partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"),
# Now we leave the room, to check if the subsequent private messages are still received properly
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"),
partial(expect_stanza,
"/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
@@ -1861,7 +1931,8 @@ if __name__ == '__main__':
partial(expect_stanza,
("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
"/iq/mam:fin/rms:set/rsm:last",
- "/iq/mam:fin/rsm:set/rsm:first")),
+ "/iq/mam:fin/rsm:set/rsm:first",
+ "/iq/mam:fin[@complete='true']")),
# Retrieve an empty archive by specifying an early “end” date
partial(send_stanza, """<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'>
@@ -1873,7 +1944,8 @@ if __name__ == '__main__':
</query></iq>"""),
partial(expect_stanza,
- "/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set",)),
# Retrieve an empty archive by specifying a late “start” date
# (note that this test will break in ~1000 years)
@@ -1886,18 +1958,20 @@ if __name__ == '__main__':
</query></iq>"""),
partial(expect_stanza,
- "/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
# Retrieve a limited archive
partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4'><set xmlns='http://jabber.org/protocol/rsm'><max>1</max></set></query></iq>"),
partial(expect_stanza,
("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']")
),
partial(expect_stanza,
- "/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
]),
Scenario("mam_with_timestamps",
@@ -1957,10 +2031,9 @@ if __name__ == '__main__':
),
partial(expect_stanza,
- "/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin[@complete='true']/rsm:set")),
]),
-
-
Scenario("join_history_limits",
[
handshake_sequence(),
@@ -1995,9 +2068,10 @@ if __name__ == '__main__':
partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
- # join the virtual channel, to stay connected to the server even after leaving #foo
+ # join some other channel, to stay connected to the server even after leaving #foo
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#DUMMY%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
partial(expect_stanza, "/message/subject"),
@@ -2075,8 +2149,6 @@ if __name__ == '__main__':
partial(expect_stanza, "/presence[@type='unavailable']"),
]),
-
-
Scenario("mam_on_fixed_server",
[
handshake_sequence(),
@@ -2134,10 +2206,10 @@ if __name__ == '__main__':
# Retrieve the archive, without any restriction
partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"),
# Since we should only receive the last 100 messages from the archive,
- # it should start with message "50"
+ # it should start with message "1"
partial(expect_stanza,
("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']")
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='1']")
),
] + [
# followed by 98 more messages
@@ -2146,14 +2218,87 @@ if __name__ == '__main__':
"/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
),
] * 98 + [
- # and finally the message "149"
+ # and finally the message "99"
partial(expect_stanza,
("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']")
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='100']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
),
+ # And it should not be marked as complete
partial(expect_stanza,
- "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
+ ("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "!/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
+ # Retrieve the next page, using the “after” thingy
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'><query xmlns='urn:xmpp:mam:2' queryid='qid2' ><set xmlns='http://jabber.org/protocol/rsm'><after>{last_uuid}</after></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='101']")
+ ),
+ ] + 47 * [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
+ ),
+ ] + [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
+
+ # Send a request with a non-existing ID set as the “after” value.
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><after>DUMMY_ID</after></set></query></iq>"),
+ partial(expect_stanza, "/iq[@id='id3'][@type='error']/error[@type='cancel']/stanza:item-not-found"),
+
+ # Request the last page just BEFORE the last message in the archive
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><before></before></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']")
+ ),
+ ] + 98 * [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
+ ),
+ ] + [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "!/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
+
+ # Do the same thing, but with a limit value.
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4' ><set xmlns='http://jabber.org/protocol/rsm'><before>{last_uuid}</before><max>2</max></set></query></iq>"),
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='147']")
+ ),
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='148']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "/iq/mam:fin[@complete='true']",
+ "/iq/mam:fin")),
]),
Scenario("channel_history_on_fixed_server",
[
@@ -2212,9 +2357,6 @@ if __name__ == '__main__':
# Second user joins
partial(send_stanza,
"<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
- # connection_sequence("irc.localhost", '{jid_one}/{resource_two}'),
- # partial(expect_stanza,
- # "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']")
@@ -2254,6 +2396,21 @@ if __name__ == '__main__':
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']"
))
]),
+ Scenario("channel_list_escaping",
+ [
+ handshake_sequence(),
+
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#true\\2ffalse%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #true/false [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#true\\2ffalse%{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='#true\\2ffalse%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ ]),
Scenario("channel_list_with_rsm",
[
handshake_sequence(),
@@ -2516,7 +2673,7 @@ if __name__ == '__main__':
"<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
partial(expect_stanza,
("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query",
- "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']",
+ "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='#foo on {irc_host_one}']",
"/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']",
"/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']",
@@ -2532,7 +2689,7 @@ if __name__ == '__main__':
"<iq from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
partial(expect_stanza,
("/iq[@from='#foo@{biboumi_host}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query",
- "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']",
+ "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='#foo on {irc_host_one}']",
"/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']",
"/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']",
@@ -2598,56 +2755,6 @@ 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='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",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_one}' />"),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
-
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
-
- partial(expect_unordered, [
- ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']",
- "/presence/muc_user:x/muc_user:status[@code='303']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
-
- ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']",
- "/presence/muc_user:x/muc_user:status[@code='303']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ]),
-
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, ("/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']",
- "/presence/status[text()='Biboumi note: 1 resources are still in this channel.']",)
- ),
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"),
- ]),
Scenario("global_configure",
[
handshake_sequence(),
@@ -2706,7 +2813,8 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
@@ -2723,7 +2831,8 @@ if __name__ == '__main__':
"<field var='verify_cert'><value>1</value></field>"
"<field var='fingerprint'><value>12:12:12</value></field>"
"<field var='pass'><value>coucou</value></field>"
- "<field var='after_connect_command'><value>INVALID command</value></field>"
+ "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>"
+ "<field var='nick'><value>my_nickname</value></field>"
"<field var='username'><value>username</value></field>"
"<field var='realname'><value>realname</value></field>"
"<field var='encoding_out'><value>UTF-8</value></field>"
@@ -2741,7 +2850,9 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']/dataform:value[text()='INVALID command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']/dataform:value[text()='my_nickname']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']",
@@ -2762,7 +2873,7 @@ if __name__ == '__main__':
"<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
"<x xmlns='jabber:x:data' type='submit'>"
"<field var='pass'><value></value></field>"
- "<field var='after_connect_command'><value></value></field>"
+ "<field var='after_connect_commands'></field>"
"<field var='username'><value></value></field>"
"<field var='realname'><value></value></field>"
"<field var='encoding_out'><value></value></field>"
@@ -2775,7 +2886,7 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']",
"/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value",
- "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_command']/dataform:value",
+ "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value",
@@ -2887,6 +2998,7 @@ if __name__ == '__main__':
"<field var='ports' />"
"<field var='tls_ports'><value>7778</value></field>"
"<field var='verify_cert'><value>0</value></field>"
+ "<field var='nick'><value>my_special_nickname</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.']"),
@@ -2896,7 +3008,7 @@ if __name__ == '__main__':
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[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/my_special_nickname']/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())]"),
diff --git a/tests/utils.cpp b/tests/utils.cpp
index 99c7040..6de19f0 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -175,11 +175,11 @@ TEST_CASE("dirname")
TEST_CASE("is_in")
{
- CHECK((is_one_of<int, float, std::string, int>::value) == true);
- CHECK((is_one_of<int, float, std::string>::value) == false);
- CHECK((is_one_of<int>::value) == false);
- CHECK((is_one_of<int, int>::value) == true);
- CHECK((is_one_of<bool, int>::value) == false);
- CHECK((is_one_of<bool, bool>::value) == true);
- CHECK((is_one_of<bool, bool, bool, bool, bool, int>::value) == true);
+ CHECK((is_one_of<int, float, std::string, int>) == true);
+ CHECK((is_one_of<int, float, std::string>) == false);
+ CHECK((is_one_of<int>) == false);
+ CHECK((is_one_of<int, int>) == true);
+ CHECK((is_one_of<bool, int>) == false);
+ CHECK((is_one_of<bool, bool>) == true);
+ CHECK((is_one_of<bool, bool, bool, bool, bool, int>) == true);
}