From 0061298dd0945f7f67e7fa340c6649b179c804d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?louiz=E2=80=99?= Date: Thu, 10 Mar 2022 23:23:47 +0100 Subject: [PATCH 01/65] Do not use ':' as a namespace separator with expat Instead use \1, and build our own nodes by explicitely separating the namespace and the node name. --- src/xmpp/adhoc_command.cpp | 6 +++--- src/xmpp/adhoc_commands_handler.cpp | 12 ++++++------ src/xmpp/biboumi_adhoc_commands.cpp | 28 ++++++++++++++-------------- src/xmpp/xmpp_component.cpp | 2 +- src/xmpp/xmpp_parser.cpp | 2 +- src/xmpp/xmpp_parser.hpp | 4 ++-- src/xmpp/xmpp_stanza.cpp | 14 +++++++++++++- src/xmpp/xmpp_stanza.hpp | 6 ++++++ tests/xmpp.cpp | 2 ++ 9 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp index fbf4ce2..f8c8e4f 100644 --- a/src/xmpp/adhoc_command.cpp +++ b/src/xmpp/adhoc_command.cpp @@ -26,7 +26,7 @@ void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node) { - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure your name."); @@ -65,9 +65,9 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) } } command_node.delete_all_children(); - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index ff4c1e5..7a84b2e 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -36,16 +36,16 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co auto command_it = this->commands.find(node); if (command_it == this->commands.end()) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":item-not-found"); + XmlSubNode condition(error, STANZA_NS, "item-not-found"); } else if (command_it->second.is_admin_only() && !Config::is_in_list("admin", jid.bare())) { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "cancel"; - XmlSubNode condition(error, STANZA_NS":forbidden"); + XmlSubNode condition(error, STANZA_NS, "forbidden"); } else { @@ -94,9 +94,9 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co } else // unsupported action { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); } } return command_node; diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 792955c..aea316d 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -34,7 +34,7 @@ void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& { auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from the gateway"); @@ -108,9 +108,9 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X return; } } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -124,7 +124,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman auto options = Database::get_global_options(owner.bare()); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure some global default settings."); @@ -220,9 +220,9 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -238,7 +238,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com auto commands = Database::get_after_connection_commands(options); command_node.delete_all_children(); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC server " + server_domain); @@ -565,9 +565,9 @@ void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& sessio note.set_inner("Configuration successfully applied."); return; } - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } @@ -586,7 +586,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain, iid.get_server(), iid.get_local()); node.delete_all_children(); - XmlSubNode x(node, "jabber:x:data:x"); + XmlSubNode x(node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Configure the IRC channel " + iid.get_local() + " on server " + iid.get_server()); @@ -671,9 +671,9 @@ void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& sessi } else { - XmlSubNode error(command_node, ADHOC_NS":error"); + XmlSubNode error(command_node, ADHOC_NS, "error"); error["type"] = "modify"; - XmlSubNode condition(error, STANZA_NS":bad-request"); + XmlSubNode condition(error, STANZA_NS, "bad-request"); session.terminate(); } } @@ -749,7 +749,7 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& { // Send a form to select the user to disconnect auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); @@ -794,7 +794,7 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& command_node.delete_all_children(); auto& biboumi_component = dynamic_cast(xmpp_component); - XmlSubNode x(command_node, "jabber:x:data:x"); + XmlSubNode x(command_node, "jabber:x:data", "x"); x["type"] = "form"; XmlSubNode title(x, "title"); title.set_inner("Disconnect a user from selected IRC servers"); diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index de9a7a6..62a98ce 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -175,7 +175,7 @@ void XmppComponent::on_stanza(const Stanza& stanza) void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation) { - Stanza node("stream:error"); + Stanza node("stream", "error"); { XmlSubNode error(node, name); error["xmlns"] = STREAM_NS; diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp index 781fe4c..1f25fa6 100644 --- a/src/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp @@ -38,7 +38,7 @@ XmppParser::XmppParser(): void XmppParser::init_xml_parser() { // Create the expat parser - this->parser = XML_ParserCreateNS("UTF-8", ':'); + this->parser = XML_ParserCreateNS("UTF-8", '\1'); XML_SetUserData(this->parser, static_cast(this)); // Install Expat handlers diff --git a/src/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp index ec42f9a..1e5e4e5 100644 --- a/src/xmpp/xmpp_parser.hpp +++ b/src/xmpp/xmpp_parser.hpp @@ -18,9 +18,9 @@ * stanza is reasonnably short. * * The element names generated by expat contain the namespace of the - * element, a colon (':') and then the actual name of the element. To get + * element, a \1 separator and then the actual name of the element. To get * an element "x" with a namespace of "http://jabber.org/protocol/muc", you - * just look for an XmlNode named "http://jabber.org/protocol/muc:x" + * just look for an XmlNode named "http://jabber.org/protocol/muc\1x" * * TODO: enforce the size-limit for the stanza (limit the number of childs * it can contain). For example forbid the parser going further than level diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp index 435f333..0103dd7 100644 --- a/src/xmpp/xmpp_stanza.cpp +++ b/src/xmpp/xmpp_stanza.cpp @@ -52,7 +52,7 @@ XmlNode::XmlNode(const std::string& name, XmlNode* parent): parent(parent) { // split the namespace and the name - auto n = name.rfind(':'); + auto n = name.rfind('\1'); if (n == std::string::npos) this->name = name; else @@ -67,6 +67,18 @@ XmlNode::XmlNode(const std::string& name): { } +XmlNode::XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent): + name(name), + parent(parent) +{ + this->attributes["xmlns"] = xmlns; +} + +XmlNode::XmlNode(const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name, nullptr) +{ +} + void XmlNode::delete_all_children() { this->children.clear(); diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index f4b3948..a706337 100644 --- a/src/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -25,6 +25,8 @@ class XmlNode public: explicit XmlNode(const std::string& name, XmlNode* parent); explicit XmlNode(const std::string& name); + explicit XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent); + explicit XmlNode(const std::string& xmlns, const std::string& name); /** * The copy constructor does not copy the parent attribute. The children * nodes are all copied recursively. @@ -150,6 +152,10 @@ public: XmlNode(name), parent_to_add(parent_ref) {} + XmlSubNode(XmlNode& parent_ref, const std::string& xmlns, const std::string& name): + XmlNode(xmlns, name), + parent_to_add(parent_ref) + {} ~XmlSubNode() { diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index 14c51da..c49c2fd 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -67,6 +67,8 @@ TEST_CASE("substanzas") CHECK(!d.has_children()); } CHECK(b.has_children()); + XmlSubNode e(a, "namespace", "name"); + CHECK(e.get_tag("xmlns") == "namespace"); } CHECK(a.has_children()); } From 049da90ded02476fedaf09da725122fc18fa4500 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 23 Apr 2023 12:44:59 +0200 Subject: [PATCH 02/65] utils/sha1.cpp: add missing include Fixes compilation with GCC 13. Downstream Gentoo bug: https://bugs.gentoo.org/902471 Signed-off-by: mathieui --- src/utils/sha1.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp index 1a0d185..47378c9 100644 --- a/src/utils/sha1.cpp +++ b/src/utils/sha1.cpp @@ -13,6 +13,7 @@ # include # include # include +# include #endif std::string sha1(const std::string& input) From 2bd91dcf6f49601193bc82e16f356cc3114aca96 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Sun, 24 Apr 2022 12:29:30 +0200 Subject: [PATCH 03/65] Replace inexistant lightmagenta CSS colour with hotpink This improves compatibility with clients respecting the CSS specification. Thanks PulkoMandy, who found it using libcss in Renga! --- src/bridge/colors.cpp | 2 +- tests/colors.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bridge/colors.cpp b/src/bridge/colors.cpp index 7662425..9c82096 100644 --- a/src/bridge/colors.cpp +++ b/src/bridge/colors.cpp @@ -24,7 +24,7 @@ static const char* irc_colors_to_css[IRC_NUM_COLORS] = { "cyan", "lightcyan", "lightblue", - "lightmagenta", + "hotpink", "gray", "white", }; diff --git a/tests/colors.cpp b/tests/colors.cpp index bf52989..9a6bf9c 100644 --- a/tests/colors.cpp +++ b/tests/colors.cpp @@ -41,7 +41,7 @@ TEST_CASE("IRC colors parsing") CHECK(cleaned_up.empty()); std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("[\x1D13dolphin-emu/dolphin\x1D] 03foo commented on #283 (Add support for the guide button to XInput): 02http://example.com"); - CHECK(xhtml->to_string() == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); + CHECK(xhtml->to_string() == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); CHECK(cleaned_up == "[dolphin-emu/dolphin] foo commented on #283 (Add support for the guide button to XInput): http://example.com"); std::tie(cleaned_up, xhtml) = irc_format_to_xhtmlim("0e46ab by 03Pierre Dindon [090|091|040] 02http://example.net/Ojrh4P media: avoid pop-in effect when loading thumbnails by specifying an explicit size"); From 5874a68a9e905e85a3a27f7e3129fb18d8181bd0 Mon Sep 17 00:00:00 2001 From: poVoq Date: Fri, 15 Nov 2024 01:32:13 +0000 Subject: [PATCH 04/65] update code repo url --- docker/biboumi/alpine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index 9ceb1e2..a5ae301 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -11,7 +11,7 @@ RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev botan-dev -RUN git clone https://lab.louiz.org/louiz/biboumi && \ +RUN git clone https://codeberg.org/poezio/biboumi && \ mkdir ./biboumi/build && \ cd ./biboumi/build && \ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \ From 8e8c97d540842a1b3055afb1de477617c5a30852 Mon Sep 17 00:00:00 2001 From: poVoq Date: Sun, 24 Nov 2024 00:56:49 +0000 Subject: [PATCH 05/65] update python to version 3 --- docker/biboumi/alpine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index a5ae301..67be80f 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -8,7 +8,7 @@ FROM docker.io/alpine:latest as builder RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ - make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev botan-dev + make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan-dev RUN git clone https://codeberg.org/poezio/biboumi && \ From d5e988cfdd7a55be83256d2df19cb2ad4c2ca4ee Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Thu, 17 Apr 2025 14:55:22 +0200 Subject: [PATCH 06/65] feat: add documentation generation workflow To fully automate deployment of documentation pages to codeberg pages a woodpecker CI file containing the build instruction is made. This needs to install all of biboumi's dependencies as to work with cmake, then generate the makefiles, and finally the actual documentation as HTML. Once that's done, push everything to a `pages` branch. --- .woodpecker/docs.yaml | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .woodpecker/docs.yaml diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml new file mode 100644 index 0000000..bcde03e --- /dev/null +++ b/.woodpecker/docs.yaml @@ -0,0 +1,52 @@ +# Exclude page pipeline to be run on "pages" branch +when: + branch: + exclude: pages + event: [push, pull_request] + +clone: + - name: git + image: woodpeckerci/plugin-git + settings: + partial: false + depth: 1 + +steps: + # Build sphinx static files + build: + image: docker.io/alpine:3.21 + commands: + # Must install all of biboumi's dependencies to satisfy cmake + - apk add --no-cache --virtual .build py3-sphinx py3-sphinx_rtd_theme cmake expat-dev g++ git libidn-dev make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan-dev + - mkdir -p build + - cd build + - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DWITH_BOTAN=1 -DWITH_SQLITE3=1 -DWITH_LIBIDN=1 -DWITH_POSTGRESQL=1 + - make html + when: + event: [pull_request, push] + + publish: + image: docker.io/bitnami/git + environment: + # secrets must be set in Woodpecker configuration + MAIL: + from_secret: mail + CODEBERG_TOKEN: + from_secret: codeberg_token + commands: + # Git configuration + - git config --global user.email $MAIL + - git config --global user.name "CI" + - git clone -b pages https://$CODEBERG_TOKEN@codeberg.org/$CI_REPO.git $CI_REPO_NAME + # Copy build step output to repository folder + - cp -ar build/html $CI_REPO_NAME/ + # Needed for custom domains + - cp .domains $CI_REPO_NAME || true # Ignore if it doesn't exist + # Commit and push all static files with pipeline started timestamp + - cd $CI_REPO_NAME + - git add . + - git commit -m "CI ${CI_COMMIT_SHA}" + - git push + when: + event: push + From 4046d58c0470cddfd7f9b5dc464a303e73bc0d2b Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Thu, 17 Apr 2025 17:54:12 +0200 Subject: [PATCH 07/65] fix: hardcode codeberg pages git commit email --- .woodpecker/docs.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index bcde03e..d228bbd 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -29,13 +29,11 @@ steps: image: docker.io/bitnami/git environment: # secrets must be set in Woodpecker configuration - MAIL: - from_secret: mail CODEBERG_TOKEN: from_secret: codeberg_token commands: # Git configuration - - git config --global user.email $MAIL + - git config --global user.email "ci-test@example.com" - git config --global user.name "CI" - git clone -b pages https://$CODEBERG_TOKEN@codeberg.org/$CI_REPO.git $CI_REPO_NAME # Copy build step output to repository folder From 8030116ca1a12af73e52d139ffeae430fe185b8f Mon Sep 17 00:00:00 2001 From: Porkepix Date: Thu, 14 Nov 2024 14:02:09 +0100 Subject: [PATCH 08/65] Update issues location --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f08fba0..b23d46d 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Florent Le Coz (louiz’) Contact/Support --------------- * XMPP ChatRoom: biboumi@muc.poez.io -* Report a bug: https://lab.louiz.org/louiz/biboumi/issues/new +* Report a bug: https://codeberg.org/poezio/biboumi/issues Also, see the `contributing`_ page. From fc0312b272c45ab5a46fd088f1619473c67faf42 Mon Sep 17 00:00:00 2001 From: poVoq Date: Sat, 2 Nov 2024 18:04:22 +0000 Subject: [PATCH 09/65] Fix WEBIRC for IRC servers like Ergo --- src/irc/irc_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 8a64727..719ab6a 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -493,7 +493,7 @@ void IrcClient::send_pass_command(const std::string& password) void IrcClient::send_webirc_command(const std::string& password, const std::string& user_ip) { - this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip})); + this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip, "secure"})); } void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason) From f8061fb79d0bd243164f4bc7b85985816e5875f2 Mon Sep 17 00:00:00 2001 From: mathieui Date: Thu, 17 Apr 2025 21:05:16 +0200 Subject: [PATCH 10/65] fix: correct minor details for doc generation --- .woodpecker/docs.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index d228bbd..6153bbd 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -1,8 +1,6 @@ # Exclude page pipeline to be run on "pages" branch when: - branch: - exclude: pages - event: [push, pull_request] + event: [pull_request, push] clone: - name: git @@ -35,16 +33,17 @@ steps: # Git configuration - git config --global user.email "ci-test@example.com" - git config --global user.name "CI" - - git clone -b pages https://$CODEBERG_TOKEN@codeberg.org/$CI_REPO.git $CI_REPO_NAME + - git clone https://$CODEBERG_TOKEN@codeberg.org/biboumi/pages.git pages # Copy build step output to repository folder - - cp -ar build/html $CI_REPO_NAME/ + - cp -ar build/html/* pages/doc/ # Needed for custom domains - - cp .domains $CI_REPO_NAME || true # Ignore if it doesn't exist + - cp .domains pages || true # Ignore if it doesn't exist # Commit and push all static files with pipeline started timestamp - - cd $CI_REPO_NAME + - cd pages - git add . - git commit -m "CI ${CI_COMMIT_SHA}" - git push when: - event: push + - event: push + branch: master From 04275627336e5476d3ab9cd2101e94e05a5df266 Mon Sep 17 00:00:00 2001 From: mathieui Date: Thu, 17 Apr 2025 22:06:29 +0200 Subject: [PATCH 11/65] fix: do not fail the pipeline if there are no docs to update --- .woodpecker/docs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index 6153bbd..d59c125 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -41,8 +41,8 @@ steps: # Commit and push all static files with pipeline started timestamp - cd pages - git add . - - git commit -m "CI ${CI_COMMIT_SHA}" - - git push + - git commit -m "CI ${CI_COMMIT_SHA}" || true + - git push || true when: - event: push branch: master From 64f1b1b9ececef1867f935b9a5ff80737904e220 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 00:16:48 +0200 Subject: [PATCH 12/65] ci: update dockerfiles oragono got renamed, and distros have been updated in the last decade --- docker/biboumi/alpine/Dockerfile | 2 +- docker/test/alpine/Dockerfile | 8 +++++--- docker/test/debian/Dockerfile | 21 +++++++-------------- docker/test/fedora/Dockerfile | 6 +++--- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index 67be80f..48d5fbc 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -33,7 +33,7 @@ RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ COPY --from=builder /etc/biboumi /etc/biboumi COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi -COPY ./biboumi.cfg /etc/biboumi/biboumi.cfg +COPY ./docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg RUN adduser biboumi -D -h /home/biboumi && \ mkdir /var/lib/biboumi && \ diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile index 60f7499..ac1ecb1 100644 --- a/docker/test/alpine/Dockerfile +++ b/docker/test/alpine/Dockerfile @@ -1,7 +1,7 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/alpine:latest +FROM alpine:latest ENV LC_ALL C.UTF-8 @@ -26,10 +26,12 @@ py3-lxml \ python3-dev \ libffi-dev \ go \ +rust \ +cargo \ wget # Install oragono, for e2e tests -RUN wget "https://github.com/oragono/oragono/archive/v2.0.0.tar.gz" && tar xvf "v2.0.0.tar.gz" && cd "oragono-2.0.0" && make && cp ~/go/bin/oragono /usr/local/bin +RUN wget "https://github.com/ergochat/ergo/archive/v2.0.0.tar.gz" && tar xvf "v2.0.0.tar.gz" && cd "ergo-2.0.0" && make && cp ~/go/bin/oragono /usr/local/bin/oragono # Install slixmpp, for e2e tests -RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install +RUN pip3 install slixmpp --break-system-packages diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile index d1254c5..98e8dd8 100644 --- a/docker/test/debian/Dockerfile +++ b/docker/test/debian/Dockerfile @@ -1,18 +1,16 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/debian:buster +FROM debian:bookworm -ENV LC_ALL C.UTF-8 +ENV LC_ALL=C.UTF-8 -RUN apt update - -# Needed to build biboumi -RUN apt install -y --no-install-recommends \ +RUN apt update && apt install -y --no-install-recommends \ git \ make \ cmake \ g++ \ +ca-certificates \ libuuid1 \ libudns-dev \ libexpat1-dev \ @@ -24,15 +22,10 @@ uuid-dev \ libgcrypt20-dev \ libpq-dev \ valgrind \ -libasan5 \ -libubsan0 \ -python3-pip \ +libasan6 \ +libubsan1 \ python3-lxml \ -python3-dev \ +python3-slixmpp \ wget RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin - -# Install slixmpp, for e2e tests -RUN git clone https://github.com/saghul/aiodns.git && cd aiodns && git checkout 7ee13f9bea25784322~ && python3 setup.py build && python3 setup.py install && git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install - diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index cd41741..237af15 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -1,9 +1,9 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/fedora:32 +FROM fedora:latest -ENV LC_ALL C.UTF-8 +ENV LC_ALL=C.UTF-8 RUN dnf --refresh install -y \ git \ @@ -35,7 +35,7 @@ rpmdevtools \ && dnf clean all # Install slixmpp, for e2e tests -RUN git clone https://lab.louiz.org/poezio/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install +RUN pip3 install --break-system-packages slixmpp # Install oragono, for e2e tests RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin From efbf9f1a0c96f6583f422514b6cbc9b4bcb93f74 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 00:35:49 +0200 Subject: [PATCH 13/65] ci: only build doc when needed --- .woodpecker/docs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index d59c125..c1c936a 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -1,5 +1,7 @@ # Exclude page pipeline to be run on "pages" branch when: + path: + include: ["CMakeLists.txt", ".woodpecker/docs.yaml", "doc/**/*"] event: [pull_request, push] clone: From b4153550a0055592bdd7e8342b1c3bb79e31d384 Mon Sep 17 00:00:00 2001 From: mathieui Date: Thu, 17 Apr 2025 23:30:55 +0200 Subject: [PATCH 14/65] ci: build packaging, test & prod containers --- .woodpecker/containers-packaging.yaml | 22 ++++++++++++++++++++++ .woodpecker/containers-prod.yaml | 19 +++++++++++++++++++ .woodpecker/containers-test.yaml | 23 +++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 .woodpecker/containers-packaging.yaml create mode 100644 .woodpecker/containers-prod.yaml create mode 100644 .woodpecker/containers-test.yaml diff --git a/.woodpecker/containers-packaging.yaml b/.woodpecker/containers-packaging.yaml new file mode 100644 index 0000000..98b363a --- /dev/null +++ b/.woodpecker/containers-packaging.yaml @@ -0,0 +1,22 @@ +when: + event: [ push ] + branch: master + path: + include: ["docker/packaging/**/*", ".woodpecker/containers-packaging.yaml"] + +matrix: + PACKAGING_IMAGE: + - archlinux + - debian + +steps: + biboumi-package-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/ci-package + registry: codeberg.org + dockerfile: docker/packaging/${PACKAGING_IMAGE}/Dockerfile + tag: ${PACKAGING_IMAGE} + username: bibou + password: + from_secret: codeberg_token diff --git a/.woodpecker/containers-prod.yaml b/.woodpecker/containers-prod.yaml new file mode 100644 index 0000000..0a1a7c9 --- /dev/null +++ b/.woodpecker/containers-prod.yaml @@ -0,0 +1,19 @@ +when: + event: [ push ] + branch: master + +matrix: + PROD_IMAGE: + - alpine + +steps: + biboumi-prod-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/biboumi + registry: codeberg.org + dockerfile: docker/biboumi/${PROD_IMAGE}/Dockerfile + tag: ${PROD_IMAGE} + username: bibou + password: + from_secret: codeberg_token diff --git a/.woodpecker/containers-test.yaml b/.woodpecker/containers-test.yaml new file mode 100644 index 0000000..d5805d6 --- /dev/null +++ b/.woodpecker/containers-test.yaml @@ -0,0 +1,23 @@ +when: + event: [ push ] + branch: master + path: + include: ["docker/test/**/*", ".woodpecker/containers-test.yaml"] + +matrix: + TEST_IMAGE: + - alpine + - debian + - fedora + +steps: + biboumi-test-image: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: codeberg.org/biboumi/ci-test + registry: codeberg.org + dockerfile: docker/test/${TEST_IMAGE}/Dockerfile + tag: ${TEST_IMAGE} + username: bibou + password: + from_secret: codeberg_token From b1d1308443d90ae227dad1bd13b053908364ee53 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 19:08:48 +0200 Subject: [PATCH 15/65] doc: update lab.louiz.org links --- CHANGELOG.rst | 2 +- doc/contributing.rst | 21 ++++++++------------- doc/index.rst | 4 ++-- tests/end_to_end/scenarios/simple_kick.py | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10b6eb4..3b0df04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -93,7 +93,7 @@ Version 8.0 - 2018-05-02 %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. + See https://codeberg.org/poezio/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 diff --git a/doc/contributing.rst b/doc/contributing.rst index 8f01c82..c805ced 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -2,10 +2,7 @@ Contributing to biboumi ####################### -Biboumi’s main workplace is at https://lab.louiz.org/louiz/biboumi - -The repository is also mirrored on other websites, for example on github, -but that’s mainly for the convenience of users. +Biboumi’s main workplace is at https://codeberg.org/poezio/biboumi Before doing anything, you can come on the `XMPP chatroom`_ to discuss your changes, issues or ideas. @@ -14,8 +11,7 @@ changes, issues or ideas. Bug reports, feature requests ----------------------------- -To open a bug report, or a feature request, please do so on `our gitlab’s -bug tracker`_. +To open a bug report, or a feature request, please do so on `our bug tracker`_. If the bug you’re reporting is about a bad behaviour of biboumi when some XMPP or IRC events occur, please try to reproduce the issue with a biboumi running @@ -34,10 +30,10 @@ Code To contribute code, you can do so using git: commit your changes on any publicly available git repository and communicate us its address. This can -be done with a `gitlab merge request`_, or a `github pull request`_ or just +be done with a `codeberg merge request`_ or just by sending a message into the `XMPP chatroom`_. -It is suggested that you use gitlab’s merge requests: this will +It is suggested that you use codeberg’s merge requests: this will automatically run our continuous integration tests. It is also recommended to add some unit or end-to-end tests for the proposed @@ -49,7 +45,7 @@ Tests There are two test suites for biboumi: -- unit tests that can be run simply using `make check`. +- unit tests that can be run simply using ``make check``. These tests use the Catch2 test framework, are written in pure C++ and they should always succeed, in all possible build configuration. @@ -59,7 +55,7 @@ There are two test suites for biboumi: Read more about these tests in the specific documentation TODO. All these tests automatically run with various configurations, on various -platforms, using gitlab CI. +platforms, using woodpecker CI. Coding style @@ -74,9 +70,8 @@ Please try to follow the existing style: - Everything else is in snake_case. -.. _our gitlab’s bug tracker: https://lab.louiz.org/louiz/biboumi/issues/new -.. _gitlab merge request: https://lab.louiz.org/louiz/biboumi/merge_requests/new -.. _github pull request: https://github.com/louiz/biboumi/pulls +.. _our bug tracker: https://codeberg.org/poezio/biboumi/issues/new +.. _codeberg merge request: https://codeberg.org/poezio/biboumi/compare/master...master .. _XMPP chatroom: xmpp:biboumi@muc.poez.io .. _Dockerfile.base: docker/biboumi-test/fedora/Dockerfile.base .. _Dockerfile: docker/biboumi-test/fedora/Dockerfile diff --git a/doc/index.rst b/doc/index.rst index 819a3e5..04c6e90 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,9 +6,9 @@ Biboumi – XMPP gateway to IRC ============================= -Homepage: https://biboumi.louiz.org +Homepage: https://biboumi.codeberg.page -Forge: https://lab.louiz.org/louiz/biboumi +Forge: https://codeberg.org/poezio/biboumi Biboumi is an XMPP gateway that connects to IRC servers and translates between the two protocols. It can be used to access IRC channels using any diff --git a/tests/end_to_end/scenarios/simple_kick.py b/tests/end_to_end/scenarios/simple_kick.py index 2949157..d327412 100644 --- a/tests/end_to_end/scenarios/simple_kick.py +++ b/tests/end_to_end/scenarios/simple_kick.py @@ -2,7 +2,7 @@ from scenarios import * scenario = ( scenarios.channel_join_with_two_users.scenario, - # demonstrate bug https://lab.louiz.org/louiz/biboumi/issues/3291 + # demonstrate bug https://codeberg.org/poezio/biboumi/issues/3291 # First user joins an other channel send_stanza(""), expect_stanza("/presence/muc_user:x/muc_user:status[@code='110']"), From 0a9b159581e807c0d9f51737788d2f76c6efd135 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Fri, 18 Apr 2025 22:46:02 +0200 Subject: [PATCH 16/65] fix: remove unneeded dependencies for documentation generation While cmake does need all of biboumi's dependencies to generate documentation, there is an existing Makefile which does not. So we can save some time by not downloading as many dependencies --- .woodpecker/docs.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index c1c936a..b422e9d 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -16,11 +16,8 @@ steps: build: image: docker.io/alpine:3.21 commands: - # Must install all of biboumi's dependencies to satisfy cmake - - apk add --no-cache --virtual .build py3-sphinx py3-sphinx_rtd_theme cmake expat-dev g++ git libidn-dev make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan-dev - - mkdir -p build - - cd build - - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DWITH_BOTAN=1 -DWITH_SQLITE3=1 -DWITH_LIBIDN=1 -DWITH_POSTGRESQL=1 + - apk add --no-cache py3-sphinx py3-sphinx_rtd_theme make + - cd doc - make html when: event: [pull_request, push] @@ -37,7 +34,7 @@ steps: - git config --global user.name "CI" - git clone https://$CODEBERG_TOKEN@codeberg.org/biboumi/pages.git pages # Copy build step output to repository folder - - cp -ar build/html/* pages/doc/ + - cp -ar doc/_build/html/* pages/doc/ # Needed for custom domains - cp .domains pages || true # Ignore if it doesn't exist # Commit and push all static files with pipeline started timestamp From 91b7a0e4b91c2d57dae3205ddb853bdfbce5337c Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 20:11:15 +0200 Subject: [PATCH 17/65] ci: ensure containers run on amd64 --- .woodpecker/containers-packaging.yaml | 3 +++ .woodpecker/containers-prod.yaml | 3 +++ .woodpecker/containers-test.yaml | 3 +++ .woodpecker/docs.yaml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.woodpecker/containers-packaging.yaml b/.woodpecker/containers-packaging.yaml index 98b363a..3f41ba2 100644 --- a/.woodpecker/containers-packaging.yaml +++ b/.woodpecker/containers-packaging.yaml @@ -4,6 +4,9 @@ when: path: include: ["docker/packaging/**/*", ".woodpecker/containers-packaging.yaml"] +labels: + platform: linux/amd64 + matrix: PACKAGING_IMAGE: - archlinux diff --git a/.woodpecker/containers-prod.yaml b/.woodpecker/containers-prod.yaml index 0a1a7c9..550e530 100644 --- a/.woodpecker/containers-prod.yaml +++ b/.woodpecker/containers-prod.yaml @@ -2,6 +2,9 @@ when: event: [ push ] branch: master +labels: + platform: linux/amd64 + matrix: PROD_IMAGE: - alpine diff --git a/.woodpecker/containers-test.yaml b/.woodpecker/containers-test.yaml index d5805d6..3f7fe3c 100644 --- a/.woodpecker/containers-test.yaml +++ b/.woodpecker/containers-test.yaml @@ -4,6 +4,9 @@ when: path: include: ["docker/test/**/*", ".woodpecker/containers-test.yaml"] +labels: + platform: linux/amd64 + matrix: TEST_IMAGE: - alpine diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index b422e9d..1c0000f 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -4,6 +4,9 @@ when: include: ["CMakeLists.txt", ".woodpecker/docs.yaml", "doc/**/*"] event: [pull_request, push] +labels: + platform: linux/amd64 + clone: - name: git image: woodpeckerci/plugin-git From 1a659871e45a2306991a0712ef41b949f5e5aeea Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 19:10:17 +0200 Subject: [PATCH 18/65] build: re-add a fork of Catch For now anyway, but we need to move to catch2 or even catch3 because those are maintained and available. --- CMakeLists.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f07b97f..4755f78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.5) project(biboumi) @@ -292,7 +292,8 @@ endforeach() # include(ExternalProject) ExternalProject_Add(catch - GIT_REPOSITORY "https://lab.louiz.org/louiz/Catch.git" + GIT_REPOSITORY "https://codeberg.org/poezio/Catch.git" + GIT_TAG "v1.5.9" PREFIX "external" UPDATE_COMMAND "" CONFIGURE_COMMAND "" From 25de4c6b7659492e66e48b1095b498eb56d47758 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 19:21:51 +0200 Subject: [PATCH 19/65] ci: add test jobs update container images for split targets between distros (woodpecker would not pull them otherwise for some reason). --- .woodpecker/containers-packaging.yaml | 4 ++-- .woodpecker/containers-test.yaml | 4 ++-- .woodpecker/test.yaml | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .woodpecker/test.yaml diff --git a/.woodpecker/containers-packaging.yaml b/.woodpecker/containers-packaging.yaml index 3f41ba2..17c945b 100644 --- a/.woodpecker/containers-packaging.yaml +++ b/.woodpecker/containers-packaging.yaml @@ -16,10 +16,10 @@ steps: biboumi-package-image: image: woodpeckerci/plugin-docker-buildx settings: - repo: codeberg.org/biboumi/ci-package + repo: codeberg.org/biboumi/ci-package-${PACKAGING_IMAGE} registry: codeberg.org dockerfile: docker/packaging/${PACKAGING_IMAGE}/Dockerfile - tag: ${PACKAGING_IMAGE} + tag: latest username: bibou password: from_secret: codeberg_token diff --git a/.woodpecker/containers-test.yaml b/.woodpecker/containers-test.yaml index 3f7fe3c..76acd13 100644 --- a/.woodpecker/containers-test.yaml +++ b/.woodpecker/containers-test.yaml @@ -17,10 +17,10 @@ steps: biboumi-test-image: image: woodpeckerci/plugin-docker-buildx settings: - repo: codeberg.org/biboumi/ci-test + repo: codeberg.org/biboumi/ci-test-${TEST_IMAGE} registry: codeberg.org dockerfile: docker/test/${TEST_IMAGE}/Dockerfile - tag: ${TEST_IMAGE} + tag: latest username: bibou password: from_secret: codeberg_token diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..c89535e --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,24 @@ +when: + event: [push, tag, pull_request] + path: ['CMakeLists.txt', 'src/**/*', 'tests/**/*', '.woodpecker/test.yaml'] + +labels: + platform: linux/amd64 + +steps: + build-fedora: + image: codeberg.org/biboumi/ci-test-fedora:latest + commands: + - mkdir build/ + - cd build/ + - cmake .. + - make everything -j$(nproc || echo 1) + + test-fedora: + depends_on: [build-fedora] + image: codeberg.org/biboumi/ci-test-fedora:latest + commands: + - cd build/ + - make check_junit || true; make check + # e2e is still broken (12/89 fails) for now + # - make e2e From 2f59303cf9af4054d007c285a8f65209d7cd57ca Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 18 Apr 2025 20:31:27 +0200 Subject: [PATCH 20/65] tests: start to fix e2e tests python and slixmpp have moved a bit in 10 years. --- tests/end_to_end/__main__.py | 39 +++++++++++++---------------------- tests/end_to_end/functions.py | 2 +- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 35edf3e..2ca26c0 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -17,8 +17,6 @@ import os from slixmpp.xmlstream.matcher.base import MatcherBase -if not hasattr(asyncio, "ensure_future"): - asyncio.ensure_future = getattr(asyncio, "async") class MatchAll(MatcherBase): """match everything""" @@ -77,9 +75,6 @@ class XMPPComponent(slixmpp.BaseXMPP): self.register_handler(slixmpp.Callback('Match All', MatchAll(None), self.handle_incoming_stanza)) - - self.add_event_handler("session_end", self.on_end_session) - asyncio.ensure_future(self.accept_routine()) self.scenario = scenario @@ -105,9 +100,6 @@ class XMPPComponent(slixmpp.BaseXMPP): self.error(error_msg) self.run_scenario() - def on_end_session(self, _): - self.loop.stop() - def handle_incoming_stanza(self, stanza): if self.stanza_checker: try: @@ -136,7 +128,6 @@ class XMPPComponent(slixmpp.BaseXMPP): if self.biboumi: self.biboumi.stop() - async def accept_routine(self): self.accepting_server = await self.loop.create_server(lambda: self, "127.0.0.1", 8811, reuse_address=True) @@ -175,10 +166,10 @@ class BiboumiRunner(ProcessRunner): self.create = asyncio.create_subprocess_exec("valgrind", "--suppressions=" + (os.environ.get("E2E_BIBOUMI_SUPP_DIR") or "") + "biboumi.supp", "--leak-check=full", "--show-leak-kinds=all", "--errors-for-leak-kinds=all", "--error-exitcode=16", "./biboumi", "test.conf", stdin=None, stdout=self.fd, - stderr=self.fd, loop=None, limit=None) + stderr=self.fd, limit=None) else: self.create = asyncio.create_subprocess_exec("./biboumi", "test.conf", stdin=None, stdout=self.fd, - stderr=self.fd, loop=None, limit=None) + stderr=self.fd, limit=None) class IrcServerRunner(ProcessRunner): @@ -204,12 +195,12 @@ class BiboumiTest: self.scenario = scenario self.expected_code = expected_code - def run(self): + async def run(self): with_valgrind = os.environ.get("E2E_WITH_VALGRIND") is not None print("Running scenario: %s%s… " % (self.scenario.name, " (with valgrind)" if with_valgrind else ''), end='') sys.stdout.flush() # Redirect the slixmpp logging into a specific file - output_filename = "slixmpp_%s_output.txt" % (self.scenario.name,) + output_filename = f"slixmpp_{self.scenario.name}_output.txt" with open(output_filename, "w"): pass logging.basicConfig(level=logging.DEBUG, @@ -229,12 +220,10 @@ class BiboumiTest: # Start the XMPP component and biboumi biboumi = BiboumiRunner(self.scenario.name) xmpp = XMPPComponent(self.scenario, biboumi) - asyncio.get_event_loop().run_until_complete(biboumi.start()) + await biboumi.start() - asyncio.get_event_loop().call_soon(xmpp.run_scenario) - - xmpp.process() - code = asyncio.get_event_loop().run_until_complete(biboumi.wait()) + xmpp.run_scenario() + code = await biboumi.wait() xmpp.biboumi = None self.scenario.steps.clear() @@ -316,18 +305,16 @@ def get_scenarios(test_path, provided_scenar_names): return scenarios -if __name__ == '__main__': - atexit.register(asyncio.get_event_loop().close) - +async def main(): provided_scenar_names = sys.argv[1:] scenarios = get_scenarios(os.path.abspath(os.path.dirname(__file__)), provided_scenar_names) irc_output = open("irc_output.txt", "w") irc = IrcServerRunner() print("Starting irc server…") - asyncio.get_event_loop().run_until_complete(irc.start()) + await irc.start() while True: - res = asyncio.get_event_loop().run_until_complete(irc.process.stderr.readline()) + res = await irc.process.stderr.readline() irc_output.write(res.decode()) if not res: print("IRC server failed to start, see irc_output.txt for more details. Exiting…") @@ -342,7 +329,7 @@ if __name__ == '__main__': failures = 0 for s in scenarios: test = BiboumiTest(s) - if not test.run(): + if not await test.run(): print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." % (s.name, s.name)) failures += 1 @@ -350,7 +337,7 @@ if __name__ == '__main__': print("Waiting for irc server to exit…") irc.stop() - asyncio.get_event_loop().run_until_complete(irc.wait()) + await irc.wait() if failures: print("%d test%s failed, please fix %s." % (failures, 's' if failures > 1 else '', @@ -359,3 +346,5 @@ if __name__ == '__main__': else: print("All tests passed successfully") +if __name__ == '__main__': + asyncio.run(main()) diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py index 3a21fcf..86ac04e 100644 --- a/tests/end_to_end/functions.py +++ b/tests/end_to_end/functions.py @@ -73,7 +73,7 @@ def check_xpath(xpaths, xmpp, after, stanza): if (expected and not matched) or (not expected and matched): raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath)) if after: - if isinstance(after, collections.Iterable): + if isinstance(after, collections.abc.Iterable): for af in after: af(stanza, xmpp) else: From 0ece4f8316f99e67ba76e37719d88d061d031740 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 20 Apr 2025 11:03:23 +0200 Subject: [PATCH 21/65] ci: build & test in matrix build build in alpine, debian and fedora and run all tests --- .woodpecker/test.yaml | 71 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index c89535e..e4077b8 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -5,13 +5,31 @@ when: labels: platform: linux/amd64 +matrix: + POSTGRESQL: + - "-WITH_POSTGRESQL=1" + - "-WITH_POSTGRESQL=0" + BOTAN: + - "-DWITH_BOTAN=1" + - "-DWITH_BOTAN=0" + steps: build-fedora: image: codeberg.org/biboumi/ci-test-fedora:latest + environment: + COMPILER: "g++" + BUILD_TYPE: "Release" + SQLITE3: "-DWITH_SQLITE3=1" + CXX_FLAGS: "-Werror -Wno-psabi" + LIBIDN: "-DWITH_LIBIDN=1" + POSTGRESQL: "-WITH_POSTGRESQL=1" + UDNS: "-DWITH_UDNS=1" + SYSTEMD: "-DWITH_SYSTEMD=1" commands: + - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" - mkdir build/ - cd build/ - - cmake .. + - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL - make everything -j$(nproc || echo 1) test-fedora: @@ -20,5 +38,52 @@ steps: commands: - cd build/ - make check_junit || true; make check - # e2e is still broken (12/89 fails) for now - # - make e2e + - make e2e + + build-alpine: + image: codeberg.org/biboumi/ci-test-alpine:latest + environment: + COMPILER: "g++" + BUILD_TYPE: "Release" + SQLITE3: "-DWITH_SQLITE3=1" + CXX_FLAGS: "-Werror -Wno-psabi" + LIBIDN: "-DWITH_LIBIDN=1" + SYSTEMD: "-DWITH_SYSTEMD=0" + commands: + - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" + - mkdir build-alpine/ + - cd build-alpine/ + - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL + - make everything -j$(nproc || echo 1) + + test-alpine: + depends_on: [build-alpine] + image: codeberg.org/biboumi/ci-test-alpine:latest + commands: + - cd build-alpine/ + - make check_junit || true; make check + - make e2e + + build-debian: + image: codeberg.org/biboumi/ci-test-debian:latest + environment: + COMPILER: "g++" + BUILD_TYPE: "Release" + SQLITE3: "-DWITH_SQLITE3=1" + CXX_FLAGS: "-Werror -Wno-psabi" + LIBIDN: "-DWITH_LIBIDN=1" + SYSTEMD: "-DWITH_SYSTEMD=1" + commands: + - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" + - mkdir build-debian/ + - cd build-debian/ + - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL + - make everything -j$(nproc || echo 1) + + test-debian: + depends_on: [build-debian] + image: codeberg.org/biboumi/ci-test-debian:latest + commands: + - cd build-debian/ + - make check_junit || true; make check + - make e2e From 40adab8b5664bb5a2436741fc0cb06cdad89691f Mon Sep 17 00:00:00 2001 From: poVoq Date: Wed, 23 Jul 2025 19:51:14 +0200 Subject: [PATCH 22/65] Pin alpine to 3.21, last version that packages botan2 Botan2 is end of life, thus it was dropped in Alpine 3.22 --- docker/biboumi/alpine/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index 48d5fbc..cc58326 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -5,7 +5,7 @@ # This is the prefered way to build the release image, used by the # end users, in production. -FROM docker.io/alpine:latest as builder +FROM docker.io/alpine:3.21 as builder RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan-dev @@ -25,7 +25,7 @@ RUN git clone https://codeberg.org/poezio/biboumi && \ # --- -FROM docker.io/alpine:latest +FROM docker.io/alpine:3.21 RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ sqlite-libs udns expat ca-certificates botan From 8c4769e27b71ead73863c896ef9702add73cba8e Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Tue, 29 Jul 2025 15:52:16 +0200 Subject: [PATCH 23/65] Revert "Replace a useless shared_ptr by a unique_ptr" This reverts commit 248e25c22fc15105d2a9db695ddb93ed5a8e0802. In preparation for the botan 3 update `IrcClient` must be made into a `shared_ptr`, which will then allow us to pass a `shared_ptr` of callbacks to `Botan::TLS::Client` --- src/bridge/bridge.cpp | 10 +++++----- src/bridge/bridge.hpp | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index e7f334f..728a8d7 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -63,7 +63,7 @@ void Bridge::shutdown(const std::string& exit_message) { for (auto& pair: this->irc_clients) { - std::unique_ptr& irc = pair.second; + std::shared_ptr& irc = pair.second; irc->send_quit_command(exit_message); } } @@ -134,11 +134,11 @@ IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::strin realname = this->get_bare_jid(); } this->irc_clients.emplace(hostname, - std::make_unique(this->poller, hostname, + std::make_shared(this->poller, hostname, nickname, username, realname, jid.domain, *this)); - std::unique_ptr& irc = this->irc_clients.at(hostname); + std::shared_ptr irc = this->irc_clients.at(hostname); return irc.get(); } } @@ -1146,12 +1146,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe } } -std::unordered_map>& Bridge::get_irc_clients() +std::unordered_map>& Bridge::get_irc_clients() { return this->irc_clients; } -const std::unordered_map>& Bridge::get_irc_clients() const +const std::unordered_map>& Bridge::get_irc_clients() const { return this->irc_clients; } diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index a7aef3d..e9e5473 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -226,8 +226,8 @@ public: * iq_responder_callback_t and remove the callback from the list. */ void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message); - std::unordered_map>& get_irc_clients(); - const std::unordered_map>& get_irc_clients() const; + std::unordered_map>& get_irc_clients(); + const std::unordered_map>& get_irc_clients() const; std::set get_chantypes(const std::string& hostname) const; #ifdef USE_DATABASE void set_record_history(const bool val); @@ -260,7 +260,7 @@ private: * One IrcClient for each IRC server we need to be connected to. * The pointer is shared by the bridge and the poller. */ - std::unordered_map> irc_clients; + std::unordered_map> irc_clients; /** * To communicate back with the XMPP component */ From e4d32f939240ed726e9981e42c0dc251cd9879da Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 28 Jul 2025 12:10:58 +0200 Subject: [PATCH 24/65] biboumi: Update botan to version 3 The botan dependency has introduced a number of breaking changes with version 3, a couple of which impact biboumi as well This patch makes biboumi compatible with botan 3 and drops support for botan 2 at the same time As a consequence of the botan dependency update, C++ version has to be upgraded to C++ 20 https://botan.randombit.net/handbook/migration_guide.html --- CMakeLists.txt | 2 +- README.rst | 2 +- cmake/Modules/FindBOTAN.cmake | 81 ++++++++++++++++++----------- doc/install.rst | 4 +- docker/test/alpine/Dockerfile | 2 +- docker/test/debian/Dockerfile | 2 +- docker/test/fedora/Dockerfile | 1 + src/irc/irc_client.cpp | 2 +- src/network/credentials_manager.cpp | 2 +- src/network/credentials_manager.hpp | 2 +- src/network/tcp_socket_handler.cpp | 59 +++++++++++---------- src/network/tcp_socket_handler.hpp | 15 +++--- src/network/tls_policy.cpp | 55 +++++++++++++++++++- src/network/tls_policy.hpp | 6 +++ 14 files changed, 161 insertions(+), 74 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4755f78..387d8df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ find_library(LIBUBSAN NAMES ubsan libubsan.so.0) # ## Set various debug flags (instrumentation libs, coverage, …) # -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y -pedantic -Wall -Wextra -Wconversion -fvisibility=hidden -fvisibility-inlines-hidden") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pedantic -Wall -Wextra -Wconversion -fvisibility=hidden -fvisibility-inlines-hidden") if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fprofile-arcs -ftest-coverage --coverage") endif() diff --git a/README.rst b/README.rst index b23d46d..895e277 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Biboumi is an XMPP gateway that connects to IRC servers and translates between the two protocols. It can be used to access IRC channels using any XMPP client as if these channels were XMPP MUCs. -It is written in modern C++14 and makes great efforts to have as little +It is written in modern C++20 and makes great efforts to have as little dependencies and to be as simple as possible. The goal is to provide a way to access most of IRC features using any XMPP diff --git a/cmake/Modules/FindBOTAN.cmake b/cmake/Modules/FindBOTAN.cmake index 3f223e2..d63996b 100644 --- a/cmake/Modules/FindBOTAN.cmake +++ b/cmake/Modules/FindBOTAN.cmake @@ -5,40 +5,63 @@ # BOTAN_FOUND - True if library and include directory are found # If set to TRUE, the following are also defined: # BOTAN_INCLUDE_DIRS - The directory where to find the header file -# BOTAN_LIBRARIES - Where to find the library file -# -# For conveniance, these variables are also set. They have the same values -# than the variables above. The user can thus choose his/her prefered way -# to write them. -# BOTAN_LIBRARY -# BOTAN_INCLUDE_DIR +# BOTAN_LIBRARIES - Where to find the library files # # This file is in the public domain +# Copied and modified from +# https://github.com/vistle/vistle/blob/master/cmake/Modules/FindBOTAN.cmake -include(FindPkgConfig) +include(FindPackageHandleStandardArgs) -if(NOT BOTAN_FOUND) - pkg_check_modules(BOTAN botan-2) +set(BOTAN_VERSIONS botan-3) +set(BOTAN_NAMES botan-3 botan) +set(BOTAN_NAMES_DEBUG botand-3 botand) + +find_path( + BOTAN_INCLUDE_DIR + NAMES botan/build.h + PATH_SUFFIXES ${BOTAN_VERSIONS} + DOC "The Botan include directory") +if(BOTAN_INCLUDE_DIR) + file(READ "${BOTAN_INCLUDE_DIR}/botan/build.h" build) + string(REGEX MATCH "BOTAN_VERSION_MAJOR ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "BOTAN_VERSION_MINOR ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_MINOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "BOTAN_VERSION_PATCH ([0-9]*)" _ ${build}) + set(BOTAN_VERSION_PATCH ${CMAKE_MATCH_1}) + set(BOTAN_VERSION "${BOTAN_VERSION_MAJOR}.${BOTAN_VERSION_MINOR}.${BOTAN_VERSION_PATCH}") endif() -if(NOT BOTAN_FOUND) - find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h - PATH_SUFFIXES botan-2 - DOC "The botan include directory") - - find_library(BOTAN_LIBRARIES NAMES botan botan-2 - DOC "The botan library") - - # Use some standard module to handle the QUIETLY and REQUIRED arguments, and - # set BOTAN_FOUND to TRUE if these two variables are set. - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(BOTAN REQUIRED_VARS BOTAN_LIBRARIES BOTAN_INCLUDE_DIRS) - - if(BOTAN_FOUND) - set(BOTAN_LIBRARY ${BOTAN_LIBRARIES} CACHE INTERNAL "") - set(BOTAN_INCLUDE_DIR ${BOTAN_INCLUDE_DIRS} CACHE INTERNAL "") - set(BOTAN_FOUND ${BOTAN_FOUND} CACHE INTERNAL "") - endif() +find_library( + BOTAN_LIBRARY + NAMES ${BOTAN_NAMES} + PATH_SUFFIXES release/lib lib + DOC "The Botan (release) library") +if(MSVC) + find_library( + BOTAN_LIBRARY_DEBUG + NAMES ${BOTAN_NAMES_DEBUG} + PATH_SUFFIXES debug/lib lib + DOC "The Botan debug library") + find_package_handle_standard_args( + BOTAN + REQUIRED_VARS BOTAN_LIBRARY BOTAN_LIBRARY_DEBUG BOTAN_INCLUDE_DIR + VERSION_VAR BOTAN_VERSION) +else() + find_package_handle_standard_args( + BOTAN + REQUIRED_VARS BOTAN_LIBRARY BOTAN_INCLUDE_DIR + VERSION_VAR BOTAN_VERSION) endif() -mark_as_advanced(BOTAN_INCLUDE_DIRS BOTAN_LIBRARIES) +if(BOTAN_FOUND) + set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR}) + if(MSVC) + set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG}) + else() + set(BOTAN_LIBRARIES ${BOTAN_LIBRARY}) + endif() +endif() + +mark_as_advanced(BOTAN_INCLUDE_DIR BOTAN_LIBRARY BOTAN_LIBRARY_DEBUG) diff --git a/doc/install.rst b/doc/install.rst index 685511d..743647d 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -20,7 +20,7 @@ biboumi by having all dependencies. Tools: ~~~~~~ -- A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example) +- A C++20 compiler (clang >= 14 or gcc >= 11.2 for example) - CMake - sphinx (optional) to build the documentation @@ -51,7 +51,7 @@ udns_ (optional, but recommended) performances when connecting to a big number of IRC servers at the same time. -libbotan_ 2.x (optional, but recommended) +libbotan_ 3.x (optional, but recommended) Provides TLS support. Without it, IRC connections are all made in plain-text mode. diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile index ac1ecb1..8ce5d4d 100644 --- a/docker/test/alpine/Dockerfile +++ b/docker/test/alpine/Dockerfile @@ -16,7 +16,7 @@ udns-dev \ expat-dev \ libidn-dev \ sqlite-dev \ -botan-dev \ +botan3-dev \ util-linux-dev \ libgcrypt-dev \ postgresql-dev \ diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile index 98e8dd8..3483c2d 100644 --- a/docker/test/debian/Dockerfile +++ b/docker/test/debian/Dockerfile @@ -16,7 +16,7 @@ libudns-dev \ libexpat1-dev \ libidn11-dev \ libsqlite3-dev \ -libbotan-2-dev \ +libbotan-3-dev \ libsystemd-dev \ uuid-dev \ libgcrypt20-dev \ diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index 237af15..a74ab9d 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -5,6 +5,7 @@ FROM fedora:latest ENV LC_ALL=C.UTF-8 +# TODO: upgrade to botan3 RUN dnf --refresh install -y \ git \ make \ diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 719ab6a..ce1c945 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -213,7 +213,7 @@ void IrcClient::start() 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()); + this->credential_manager->set_trusted_fingerprint(options.col()); # endif if (Config::get("fixed_irc_server", "").empty() && !options.col().empty()) diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index 47f8514..a692b43 100644 --- a/src/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -39,7 +39,7 @@ const std::string& BasicCredentialsManager::get_trusted_fingerprint() const } void check_tls_certificate(const std::vector& certs, - const std::string& hostname, const std::string& trusted_fingerprint, + const std::string_view hostname, const std::string& trusted_fingerprint, const std::exception_ptr& exc) { diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp index 210a628..4b87d67 100644 --- a/src/network/credentials_manager.hpp +++ b/src/network/credentials_manager.hpp @@ -19,7 +19,7 @@ class TCPSocketHandler; * on a trusted fingerprint. */ void check_tls_certificate(const std::vector& certs, - const std::string& hostname, const std::string& trusted_fingerprint, + const std::string_view hostname, const std::string& trusted_fingerprint, const std::exception_ptr& exc); class BasicCredentialsManager: public Botan::Credentials_Manager diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index 5e4ae30..25adad4 100644 --- a/src/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -16,23 +16,29 @@ # include # include # include +# include +# include # include # include namespace { - Botan::AutoSeeded_RNG& get_rng() + std::shared_ptr& get_rng() { - static Botan::AutoSeeded_RNG rng{}; + static std::shared_ptr rng; + + if (!rng) + rng = std::make_shared(); return rng; } - Botan::TLS::Session_Manager_In_Memory& get_session_manager() + std::shared_ptr& get_session_manager(std::shared_ptr creds) { - static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()}; -#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2,4,0) - // workaround for https://github.com/randombit/botan/issues/1276 - session_manager.remove_all(); -#endif + static std::shared_ptr session_manager; + + if (!session_manager) + session_manager = std::make_shared( + std::make_unique(get_rng()), + creds, get_rng()); return session_manager; } } @@ -50,7 +56,8 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr& poller): SocketHandler(poller, -1), use_tls(false) #ifdef BOTAN_FOUND - ,credential_manager() + ,credential_manager(std::make_shared()) + ,policy(std::make_shared()) #endif {} @@ -242,11 +249,11 @@ void TCPSocketHandler::start_tls(const std::string& address, const std::string& auto policy_directory = Config::get("policy_directory", utils::dirname(Config::get_filename())); if (!policy_directory.empty() && policy_directory[policy_directory.size()-1] != '/') policy_directory += '/'; - this->policy.load(policy_directory + "policy.txt"); - this->policy.load(policy_directory + address + ".policy.txt"); + this->policy->load(policy_directory + "policy.txt"); + this->policy->load(policy_directory + address + ".policy.txt"); this->tls = std::make_unique( - *this, - get_session_manager(), this->credential_manager, this->policy, + shared_from_this(), + get_session_manager(this->credential_manager), this->credential_manager, this->policy, get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version()); } @@ -298,17 +305,16 @@ void TCPSocketHandler::tls_send(std::string&& data) std::make_move_iterator(data.end())); } -void TCPSocketHandler::tls_record_received(uint64_t, const Botan::byte *data, size_t size) +void TCPSocketHandler::tls_record_received(uint64_t, std::span data) { - this->in_buf += std::string(reinterpret_cast(data), - size); + this->in_buf += std::string(data.begin(), data.end()); if (!this->in_buf.empty()) - this->parse_in_buffer(size); + this->parse_in_buffer(data.size()); } -void TCPSocketHandler::tls_emit_data(const Botan::byte *data, size_t size) +void TCPSocketHandler::tls_emit_data(std::span data) { - this->raw_send(std::string(reinterpret_cast(data), size)); + this->raw_send(std::string(data.begin(), data.end())); } void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert) @@ -316,25 +322,24 @@ void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert) log_debug("tls_alert: ", alert.type_string()); } -bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& session) +void TCPSocketHandler::tls_session_established(const Botan::TLS::Session_Summary& session) { log_debug("Handshake with ", session.server_info().hostname(), " complete.", " Version: ", session.version().to_string(), " using ", session.ciphersuite().to_string()); if (!session.session_id().empty()) log_debug("Session ID ", Botan::hex_encode(session.session_id())); - if (!session.session_ticket().empty()) - log_debug("Session ticket ", Botan::hex_encode(session.session_ticket())); - return true; + if (session.session_ticket().has_value()) + log_debug("Session ticket ", Botan::hex_encode(*session.session_ticket())); } void TCPSocketHandler::tls_verify_cert_chain(const std::vector& cert_chain, - const std::vector>& ocsp_responses, + const std::vector>& ocsp_responses, const std::vector& trusted_roots, - Botan::Usage_Type usage, const std::string& hostname, + Botan::Usage_Type usage, std::string_view hostname, const Botan::TLS::Policy& policy) { - if (!this->policy.verify_certificate) + if (!this->policy->verify_certificate) { log_debug("Not verifying certificate due to domain policy "); return; @@ -352,7 +357,7 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vectorabort_on_invalid_cert()) exception_ptr = std::current_exception(); - check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr); + check_tls_certificate(cert_chain, hostname, this->credential_manager->get_trusted_fingerprint(), exception_ptr); } } diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp index b12ae57..3c6f346 100644 --- a/src/network/tcp_socket_handler.hpp +++ b/src/network/tcp_socket_handler.hpp @@ -30,6 +30,7 @@ * But doesn’t do any connect() or accept() or anything else. */ class TCPSocketHandler: public SocketHandler + ,public std::enable_shared_from_this #ifdef BOTAN_FOUND ,public Botan::TLS::Callbacks #endif @@ -137,12 +138,12 @@ private: * Called by the tls object that some data has been decrypt. We call * parse_in_buffer() to handle that unencrypted data. */ - void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) override final; + void tls_record_received(uint64_t rec_no, std::span data) override final; /** * Called by the tls object to indicate that some data has been encrypted * and is now ready to be sent on the socket as is. */ - void tls_emit_data(const Botan::byte* data, size_t size) override final; + void tls_emit_data(std::span data) override final; /** * Called by the tls object to indicate that a TLS alert has been * received. We don’t use it, we just log some message, at the moment. @@ -152,13 +153,13 @@ private: * Called by the tls object at the end of the TLS handshake. We don't do * anything here appart from logging the TLS session information. */ - bool tls_session_established(const Botan::TLS::Session& session) override final; + void tls_session_established(const Botan::TLS::Session_Summary& session) override final; void tls_verify_cert_chain(const std::vector& cert_chain, - const std::vector>& ocsp_responses, + const std::vector>& ocsp_responses, const std::vector& trusted_roots, Botan::Usage_Type usage, - const std::string& hostname, + std::string_view hostname, const Botan::TLS::Policy& policy) override final; /** * Called whenever the tls session goes from inactive to active. This @@ -203,9 +204,9 @@ protected: #ifdef BOTAN_FOUND protected: - BasicCredentialsManager credential_manager; + std::shared_ptr credential_manager; private: - BiboumiTLSPolicy policy; + std::shared_ptr policy; /** * We use a unique_ptr because we may not want to create the object at * all. The Botan::TLS::Client object generates a handshake message and diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp index f32557e..20699cf 100644 --- a/src/network/tls_policy.cpp +++ b/src/network/tls_policy.cpp @@ -8,7 +8,6 @@ #include #include -#include #include bool BiboumiTLSPolicy::load(const std::string& filename) @@ -31,7 +30,7 @@ bool BiboumiTLSPolicy::load(const std::string& filename) void BiboumiTLSPolicy::load(std::istream& is) { - const auto dict = Botan::read_cfg(is); + const auto dict = BiboumiTLSPolicy::read_cfg(is); for (const auto& pair: dict) { // Workaround for options that are not overridden in Botan::TLS::Text_Policy @@ -49,4 +48,56 @@ bool BiboumiTLSPolicy::require_cert_revocation_info() const return this->req_cert_revocation_info; } +std::map BiboumiTLSPolicy::read_cfg(std::istream& is) { + std::map kv; + size_t line = 0; + + while(is.good()) { + std::string s; + + std::getline(is, s); + + ++line; + + if(s.empty() || s[0] == '#') { + continue; + } + + s = clean_ws(s.substr(0, s.find('#'))); + + if(s.empty()) { + continue; + } + + auto eq = s.find('='); + + if(eq == std::string::npos || eq == 0 || eq == s.size() - 1) { + throw Botan::Decoding_Error("Bad read_cfg input '" + s + "' on line " + std::to_string(line)); + } + + const std::string key = BiboumiTLSPolicy::clean_ws(s.substr(0, eq)); + const std::string val = BiboumiTLSPolicy::clean_ws(s.substr(eq + 1, std::string::npos)); + + kv[key] = val; + } + + return kv; +} + +std::string BiboumiTLSPolicy::clean_ws(std::string_view s) { + const char* ws = " \t\n"; + auto start = s.find_first_not_of(ws); + auto end = s.find_last_not_of(ws); + + if(start == std::string::npos) { + return ""; + } + + if(end == std::string::npos) { + return std::string(s.substr(start, end)); + } else { + return std::string(s.substr(start, start + end + 1)); + } +} + #endif diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp index e915646..052dd9c 100644 --- a/src/network/tls_policy.hpp +++ b/src/network/tls_policy.hpp @@ -24,6 +24,12 @@ public: bool verify_certificate{true}; protected: bool req_cert_revocation_info{true}; +private: + // Copied from botan-3's src/lib/utils/read_cfg.cpp + // These were made internal in v3, but they are in use by biboumi so they have + // to be vendored in + static std::map read_cfg(std::istream& is); + static std::string clean_ws(std::string_view s); }; #endif From 7cd4e06f84806ff039e454a0b0fa956069ee692f Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Mon, 19 May 2025 10:57:37 +0200 Subject: [PATCH 25/65] Support all allowed channel prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 2812 specifies in §1.3 that “Channels names are strings (beginning with a '&', '#', '+' or '!' character) of length up to fifty (50) characters.” We previously only allowed the first two prefix characters, or any other one if the server overrides the list with ISUPPORT. --- doc/user.rst | 5 +++-- src/bridge/bridge.cpp | 2 +- src/irc/iid.hpp | 2 +- src/irc/irc_client.cpp | 2 +- src/xmpp/biboumi_component.cpp | 10 +++++----- tests/iid.cpp | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/user.rst b/doc/user.rst index add3480..023036a 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -41,8 +41,9 @@ IRC channels and IRC users have a local part formed like this: ``name`` can be a channel name or an user nickname. The distinction between 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. +``'&'``, ``'#'``, ``'+'`` or ``'!'`` (but this can be overridden by the server, +using the ISUPPORT extension) then it’s a channel name, otherwise this is a +nickname. 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 diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 728a8d7..c826fe1 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -1160,7 +1160,7 @@ std::set Bridge::get_chantypes(const std::string& hostname) const { IrcClient* irc = this->find_irc_client(hostname); if (!irc) - return {'#', '&'}; + return {'&', '#', '+', '!'}; return irc->get_chantypes(); } diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp index 89f4797..ef8cce5 100644 --- a/src/irc/iid.hpp +++ b/src/irc/iid.hpp @@ -20,7 +20,7 @@ class Bridge; * It’s possible to have an empty-string server, but it makes no sense in * biboumi’s context. * - * Assuming the chantypes are '#' and '&': + * Assuming the chantypes are '&', '#', '+' and '!': * * #test%irc.example.org has : * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead) diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index ce1c945..72da2b4 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -155,7 +155,7 @@ IrcClient::IrcClient(std::shared_ptr& poller, std::string hostname, bridge(bridge), welcomed(false), chanmodes({"", "", "", ""}), - chantypes({'#', '&'}), + chantypes({'&', '#', '+', '!'}), tokens_bucket(this->get_throttle_limit(), 1s, [this]() { if (message_queue.empty()) return true; diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 3cfa9d0..725e714 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -482,7 +482,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) // Depending on the 'to' jid in the request, we use one adhoc // command handler or an other - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); AdhocCommandsHandler* adhoc_handler; if (to.local.empty()) adhoc_handler = &this->adhoc_commands_handler; @@ -532,7 +532,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) const XmlNode* query; if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { @@ -736,7 +736,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) const XmlNode* query = stanza.get_child("query", MAM_NS); - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (query && iid.type == Iid::Type::Channel && to.resource.empty()) { const std::string query_id = query->get_tag("queryid"); @@ -865,7 +865,7 @@ void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_lin bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id) { - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; @@ -887,7 +887,7 @@ bool BiboumiComponent::handle_room_configuration_form_request(const std::string& bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id) { - Iid iid(to.local, {'#', '&'}); + Iid iid(to.local, {'&', '#', '+', '!'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; diff --git a/tests/iid.cpp b/tests/iid.cpp index 63b2ba3..d8f17ad 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -47,7 +47,7 @@ namespace Catch TEST_CASE("Iid creation") { - const std::set chantypes {'#', '&'}; + const std::set chantypes {'&', '#', '+', '!'}; Iid iid1("foo%irc.example.org", chantypes); CHECK(std::to_string(iid1) == "foo%irc.example.org"); CHECK(iid1.get_local() == "foo"); @@ -95,7 +95,7 @@ TEST_CASE("Iid creation in fixed_server mode") { Config::set("fixed_irc_server", "fixed.example.com", false); - const std::set chantypes {'#', '&'}; + const std::set chantypes {'&', '#', '+', '!'}; Iid iid1("foo%irc.example.org", chantypes); CHECK(std::to_string(iid1) == "foo%irc.example.org"); CHECK(iid1.get_local() == "foo%irc.example.org"); From 3dc0f8660d77b058d4b62d74a1091f6c23f1b452 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 30 Jul 2025 12:21:35 +0200 Subject: [PATCH 26/65] Update CHANGELOG with new build requirements --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b0df04..77f99f1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,12 @@ For admins - Command line option --test-config (or -t) has been added. When used, biboumi will just exit without any error if the configuration is correct +For packagers +------------- +- The minimum required version of botan has been upgraded to version 3 +- clang 3.4 and gcc 5.0 are no longer supported. The new minimum required + versions are clang >= 14 and gcc >= 11.2 + Version 9.0 - 2020-09-22 ======================== From c7a0ce553f34047a05bda3a6a2f43379f96e5b08 Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Tue, 29 Jul 2025 16:21:02 +0200 Subject: [PATCH 27/65] Remove GNOME and Mozilla IRC networks Both have been decomissioned, both projects moved to Matrix instead. --- conf/irc.gnome.org.policy.txt | 1 - conf/irc.mozilla.org.policy.txt | 1 - 2 files changed, 2 deletions(-) delete mode 120000 conf/irc.gnome.org.policy.txt delete mode 100644 conf/irc.mozilla.org.policy.txt diff --git a/conf/irc.gnome.org.policy.txt b/conf/irc.gnome.org.policy.txt deleted file mode 120000 index f06b958..0000000 --- a/conf/irc.gnome.org.policy.txt +++ /dev/null @@ -1 +0,0 @@ -irc.gimp.org.policy.txt \ No newline at end of file diff --git a/conf/irc.mozilla.org.policy.txt b/conf/irc.mozilla.org.policy.txt deleted file mode 100644 index f099e61..0000000 --- a/conf/irc.mozilla.org.policy.txt +++ /dev/null @@ -1 +0,0 @@ -minimum_dh_group_size = 1024 From 5cb2802fd28acaa8e9ecb4fbef897f1f1b870938 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sun, 27 Feb 2022 11:06:42 +0100 Subject: [PATCH 28/65] Update Catch2 to v3 --- CMakeLists.txt | 22 ++++------------------ docker/test/alpine/Dockerfile | 1 + docker/test/debian/Dockerfile | 3 ++- docker/test/fedora/Dockerfile | 3 ++- tests/colors.cpp | 2 +- tests/config.cpp | 2 +- tests/database.cpp | 2 +- tests/encoding.cpp | 2 +- tests/iid.cpp | 2 +- tests/io_tester.cpp | 2 +- tests/irc.cpp | 2 +- tests/jid.cpp | 2 +- tests/logger.cpp | 2 +- tests/network.cpp | 2 +- tests/test.cpp | 2 -- tests/timed_events.cpp | 16 +--------------- tests/utils.cpp | 2 +- tests/uuid.cpp | 2 +- tests/xmpp.cpp | 2 +- 19 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 tests/test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 387d8df..ef037b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,25 +288,11 @@ foreach(file ${source_all}) endforeach() # -## Add a rule to download the catch unit test framework +## Catch unit test framework # -include(ExternalProject) -ExternalProject_Add(catch - GIT_REPOSITORY "https://codeberg.org/poezio/Catch.git" - GIT_TAG "v1.5.9" - PREFIX "external" - UPDATE_COMMAND "" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - ) -set_target_properties(catch PROPERTIES EXCLUDE_FROM_ALL TRUE) -ExternalProject_Get_Property(catch SOURCE_DIR) -if(NOT EXISTS ${CMAKE_SOURCE_DIR}/tests/catch.hpp) - target_include_directories(test_suite - PUBLIC "${SOURCE_DIR}/single_include/" - ) - add_dependencies(test_suite catch) +find_package(Catch2) +if(Catch2_FOUND) + target_link_libraries(test_suite Catch2::Catch2WithMain) endif() # diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile index 8ce5d4d..fda5388 100644 --- a/docker/test/alpine/Dockerfile +++ b/docker/test/alpine/Dockerfile @@ -20,6 +20,7 @@ botan3-dev \ util-linux-dev \ libgcrypt-dev \ postgresql-dev \ +catch2-3 \ valgrind \ py3-pip \ py3-lxml \ diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile index 3483c2d..b7f393b 100644 --- a/docker/test/debian/Dockerfile +++ b/docker/test/debian/Dockerfile @@ -21,6 +21,7 @@ libsystemd-dev \ uuid-dev \ libgcrypt20-dev \ libpq-dev \ +libcatch2-dev \ valgrind \ libasan6 \ libubsan1 \ @@ -28,4 +29,4 @@ python3-lxml \ python3-slixmpp \ wget -RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin +RUN wget "https://github.com/ergochat/ergo/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index a74ab9d..1ae3c4a 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -24,6 +24,7 @@ postgresql-devel \ lcov \ libasan \ libubsan \ +catch \ valgrind \ python3-pip \ python3-lxml \ @@ -39,4 +40,4 @@ rpmdevtools \ RUN pip3 install --break-system-packages slixmpp # Install oragono, for e2e tests -RUN wget "https://github.com/oragono/oragono/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin +RUN wget "https://github.com/ergochat/ergo/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin diff --git a/tests/colors.cpp b/tests/colors.cpp index 9a6bf9c..cb705ab 100644 --- a/tests/colors.cpp +++ b/tests/colors.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/config.cpp b/tests/config.cpp index ec9844f..30d801f 100644 --- a/tests/config.cpp +++ b/tests/config.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include "io_tester.hpp" #include diff --git a/tests/database.cpp b/tests/database.cpp index 070a460..e616074 100644 --- a/tests/database.cpp +++ b/tests/database.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/encoding.cpp b/tests/encoding.cpp index b5192ff..fc8c655 100644 --- a/tests/encoding.cpp +++ b/tests/encoding.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/iid.cpp b/tests/iid.cpp index d8f17ad..dc264fd 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/io_tester.cpp b/tests/io_tester.cpp index 19c97c9..1d6d59b 100644 --- a/tests/io_tester.cpp +++ b/tests/io_tester.cpp @@ -1,5 +1,5 @@ #include "io_tester.hpp" -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include /** diff --git a/tests/irc.cpp b/tests/irc.cpp index 0f30f15..a113823 100644 --- a/tests/irc.cpp +++ b/tests/irc.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/jid.cpp b/tests/jid.cpp index 592d6f3..cc7d74b 100644 --- a/tests/jid.cpp +++ b/tests/jid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/logger.cpp b/tests/logger.cpp index 1e3392a..871dbcf 100644 --- a/tests/logger.cpp +++ b/tests/logger.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/network.cpp b/tests/network.cpp index a52eb6a..309659a 100644 --- a/tests/network.cpp +++ b/tests/network.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/test.cpp b/tests/test.cpp deleted file mode 100644 index 0c7c351..0000000 --- a/tests/test.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch.hpp" diff --git a/tests/timed_events.cpp b/tests/timed_events.cpp index fece422..b8ccf6c 100644 --- a/tests/timed_events.cpp +++ b/tests/timed_events.cpp @@ -1,21 +1,7 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include -/** - * Let Catch know how to display std::chrono::duration values - */ -namespace Catch -{ - template struct StringMaker> - { - static std::string convert(const std::chrono::duration& value) - { - return std::to_string(std::chrono::duration_cast(value).count()) + "ms"; - } - }; -} - /** * TODO, use a mock clock instead of relying on the real time with a sleep: * it’s unreliable on heavy load. diff --git a/tests/utils.cpp b/tests/utils.cpp index 6151733..5a9f0a0 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include diff --git a/tests/uuid.cpp b/tests/uuid.cpp index 12c6c32..4f9bdfb 100644 --- a/tests/uuid.cpp +++ b/tests/uuid.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include diff --git a/tests/xmpp.cpp b/tests/xmpp.cpp index c49c2fd..d88f8d7 100644 --- a/tests/xmpp.cpp +++ b/tests/xmpp.cpp @@ -1,4 +1,4 @@ -#include "catch.hpp" +#include "catch2/catch_test_macros.hpp" #include #include From 9c92f7ce4a3d0844c9dfc65f44ddf655170169c5 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 30 Jul 2025 13:45:19 +0200 Subject: [PATCH 29/65] Bounce message corrections On IRC it is confusing when an XMPP user "corrects" a message, but on the other side a new message is sent. To prevent spam and confusion, do not allow message corrections at all, and instead return an error if attempted. Fixes: #3436 Fixes: #3480 --- CHANGELOG.rst | 2 ++ src/xmpp/biboumi_component.cpp | 9 +++++++++ src/xmpp/xmpp_component.hpp | 1 + tests/end_to_end/scenarios/message_correction.py | 10 ++++++++++ 4 files changed, 22 insertions(+) create mode 100644 tests/end_to_end/scenarios/message_correction.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 77f99f1..85f4d19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ For users can still use the in-room JID (#chan%irc@biboumi/NickName) to send a private message but the response you will receive will come from nickname%irc@biboumi. +- Message Correction (XEP-0308) no longer works, and will instead return an + error when the user attempts to. For admins ---------- diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 725e714..7be2bee 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -295,6 +295,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza) error_type, error_name, error_text); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); + const XmlNode* replace = stanza.get_child("replace", MESSAGE_CORRECT_NS); + + if (replace) + { + error_type = "cancel"; + error_name = "feature-not-implemented"; + error_text = "This room is mapped from IRC, which does not support message correction."; + return; + } try { // catch IRCNotConnected exceptions if (type == "groupchat" && iid.type == Iid::Type::Channel) diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index ee6b776..a415183 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -39,6 +39,7 @@ #define STABLE_ID_NS "urn:xmpp:sid:0" #define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id" #define SELF_PING_FLAG MUC_NS"#self-ping-optimization" +#define MESSAGE_CORRECT_NS "urn:xmpp:message-correct:0" /** * An XMPP component, communicating with an XMPP server using the protocole diff --git a/tests/end_to_end/scenarios/message_correction.py b/tests/end_to_end/scenarios/message_correction.py new file mode 100644 index 0000000..67f7750 --- /dev/null +++ b/tests/end_to_end/scenarios/message_correction.py @@ -0,0 +1,10 @@ +from scenarios import * + +scenario = ( + scenarios.simple_channel_join.scenario, + send_stanza("hello"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='hello']"), + send_stanza("hello"), + expect_stanza("/message[@from='#foo%{irc_server_one}'][@id='the-message-id-2'][@to='{jid_one}/{resource_one}'][@type='error']/error[@type='cancel']/stanza:feature-not-implemented", + "/message[@from='#foo%{irc_server_one}'][@id='the-message-id-2'][@to='{jid_one}/{resource_one}'][@type='error']/error[@type='cancel']/stanza:text[text()='This room is mapped from IRC, which does not support message correction.']"), +) From 814bb67df84608d7905fafd6555fc0f581766493 Mon Sep 17 00:00:00 2001 From: poVoq Date: Mon, 1 Sep 2025 03:26:34 +0200 Subject: [PATCH 30/65] Fix disco for IRC servers They should not advertise that they are MUCs since they are not. Backport from: https://git.sr.ht/~singpolyma/biboumi/commit/764878fb0f357ec42b59b58d199fe2d5a046f998 --- src/xmpp/biboumi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 7be2bee..4d432bb 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -987,10 +987,10 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; XmlSubNode identity(query, "identity"); - identity["category"] = "conference"; + identity["category"] = "gateway"; 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, STABLE_MUC_ID_NS}) + for (const char *ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; From d3b32428749bf818a5667ab1344b9104f0e5fa50 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 30 Jul 2025 14:12:24 +0200 Subject: [PATCH 31/65] Fix references to localhost -> irc.localhost in e2e tests --- tests/end_to_end/scenarios/channel_custom_topic.py | 4 ++-- tests/end_to_end/scenarios/channel_force_join.py | 2 +- tests/end_to_end/scenarios/channel_history.py | 2 +- tests/end_to_end/scenarios/channel_history_on_fixed_server.py | 2 +- tests/end_to_end/scenarios/channel_join_with_password.py | 4 ++-- tests/end_to_end/scenarios/channel_join_with_two_users.py | 4 ++-- tests/end_to_end/scenarios/channel_messages.py | 4 ++-- tests/end_to_end/scenarios/multiline_message.py | 4 ++-- tests/end_to_end/scenarios/raw_message.py | 2 +- tests/end_to_end/scenarios/raw_message_fixed_irc_server.py | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py index 3b3104e..b15ccc8 100644 --- a/tests/end_to_end/scenarios/channel_custom_topic.py +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -14,13 +14,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py index 9a24c06..c03b9b9 100644 --- a/tests/end_to_end/scenarios/channel_force_join.py +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -11,7 +11,7 @@ scenario = ( send_stanza(""), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost']" ], [ "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py index 0014d65..a9f74a1 100644 --- a/tests/end_to_end/scenarios/channel_history.py +++ b/tests/end_to_end/scenarios/channel_history.py @@ -9,7 +9,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@irc.localhost'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py index 0e957e1..ad3701b 100644 --- a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -11,7 +11,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@irc.localhost'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py index fdebcbe..e9873b1 100644 --- a/tests/end_to_end/scenarios/channel_join_with_password.py +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -19,13 +19,13 @@ scenario = ( send_stanza(" SECRET"), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py index 3b2b102..0e36db2 100644 --- a/tests/end_to_end/scenarios/channel_join_with_two_users.py +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -8,13 +8,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index a3ead30..8215094 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -11,10 +11,10 @@ scenario = ( # Our presence, sent to the other user, and ourself expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"], diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index cd42c6c..73e9331 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -38,10 +38,10 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), # Our presence, sent to the other user expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.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='{lower_nick_two}%{irc_server_one}/~{nick_two}@localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py index c6cd4e7..98df0ea 100644 --- a/tests/end_to_end/scenarios/raw_message.py +++ b/tests/end_to_end/scenarios/raw_message.py @@ -7,5 +7,5 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), + expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} irc.localhost * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py index 7eb5b13..b5006c7 100644 --- a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -9,6 +9,6 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} localhost * {nick_one}']"), + expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} irc.localhost * {nick_one}']"), ) From 979ba96cb0639fe12f5467023d5716049fd3a792 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 30 Jul 2025 15:52:25 +0200 Subject: [PATCH 32/65] Update to ergo 2.15+ --- CMakeLists.txt | 2 +- docker/test/alpine/Dockerfile | 12 +- docker/test/debian/Dockerfile | 6 +- docker/test/fedora/Dockerfile | 7 +- src/bridge/bridge.cpp | 1 + tests/end_to_end/__main__.py | 11 +- tests/end_to_end/ircd.yaml | 131 ++++-------------- .../scenarios/channel_custom_topic.py | 4 +- .../scenarios/channel_force_join.py | 2 +- tests/end_to_end/scenarios/channel_history.py | 2 +- .../channel_history_on_fixed_server.py | 2 +- .../scenarios/channel_join_with_password.py | 4 +- .../scenarios/channel_join_with_two_users.py | 4 +- .../end_to_end/scenarios/channel_messages.py | 4 +- .../end_to_end/scenarios/multiline_message.py | 4 +- tests/end_to_end/scenarios/raw_message.py | 2 +- .../scenarios/raw_message_fixed_irc_server.py | 2 +- tests/end_to_end/scenarios/sasl.py | 4 +- tests/end_to_end/sequences.py | 14 +- 19 files changed, 78 insertions(+), 140 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef037b3..be47ffe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ endif() # ## Find optional instrumentation libraries that will be used in debug only # -find_library(LIBASAN NAMES asan libasan.so.4 libasan.so.3 libasan.so.2 libasan.so.1) +find_library(LIBASAN NAMES asan libasan.so.8 libasan.so.4 libasan.so.3 libasan.so.2 libasan.so.1) find_library(LIBUBSAN NAMES ubsan libubsan.so.0) # diff --git a/docker/test/alpine/Dockerfile b/docker/test/alpine/Dockerfile index fda5388..047d134 100644 --- a/docker/test/alpine/Dockerfile +++ b/docker/test/alpine/Dockerfile @@ -1,13 +1,17 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM alpine:latest +FROM docker.io/alpine:edge ENV LC_ALL C.UTF-8 +# py3-slixmpp is in testing repo +RUN echo "https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories + # Needed to build biboumi RUN apk add --no-cache \ git \ +ergo \ make \ cmake \ g++ \ @@ -24,6 +28,7 @@ catch2-3 \ valgrind \ py3-pip \ py3-lxml \ +py3-slixmpp \ python3-dev \ libffi-dev \ go \ @@ -31,8 +36,3 @@ rust \ cargo \ wget -# Install oragono, for e2e tests -RUN wget "https://github.com/ergochat/ergo/archive/v2.0.0.tar.gz" && tar xvf "v2.0.0.tar.gz" && cd "ergo-2.0.0" && make && cp ~/go/bin/oragono /usr/local/bin/oragono - -# Install slixmpp, for e2e tests -RUN pip3 install slixmpp --break-system-packages diff --git a/docker/test/debian/Dockerfile b/docker/test/debian/Dockerfile index b7f393b..8ba8064 100644 --- a/docker/test/debian/Dockerfile +++ b/docker/test/debian/Dockerfile @@ -1,7 +1,7 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM debian:bookworm +FROM docker.io/debian:trixie ENV LC_ALL=C.UTF-8 @@ -23,10 +23,10 @@ libgcrypt20-dev \ libpq-dev \ libcatch2-dev \ valgrind \ -libasan6 \ +libasan8 \ libubsan1 \ python3-lxml \ python3-slixmpp \ +ergochat \ wget -RUN wget "https://github.com/ergochat/ergo/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index 1ae3c4a..cdab0c4 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -1,8 +1,9 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM fedora:latest +FROM docker.io/fedora:latest +ARG ERGO_VERSION=2.16.0 ENV LC_ALL=C.UTF-8 # TODO: upgrade to botan3 @@ -39,5 +40,5 @@ rpmdevtools \ # Install slixmpp, for e2e tests RUN pip3 install --break-system-packages slixmpp -# Install oragono, for e2e tests -RUN wget "https://github.com/ergochat/ergo/releases/download/v2.0.0/oragono-2.0.0-linux-x64.tar.gz" && tar xvf oragono-2.0.0-linux-x64.tar.gz && cp oragono-2.0.0-linux-x64/oragono /usr/local/bin +# Install ergo, for e2e tests +RUN wget "https://github.com/ergochat/ergo/releases/download/v${ERGO_VERSION}/ergo-${ERGO_VERSION}-linux-x86_64.tar.gz" && tar xvf ergo-${ERGO_VERSION}-linux-x86_64.tar.gz && cp -r ergo-${ERGO_VERSION}-linux-x86_64 /usr/local/lib/ergo && ln -s /usr/local/lib/ergo/ergo /usr/local/bin/ergo diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index c826fe1..ec76ef6 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -13,6 +13,7 @@ #include #include "result_set_management.hpp" #include +#include using namespace std::string_literals; diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index 2ca26c0..e1c73bb 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -14,6 +14,7 @@ import signal import atexit import sys import os +import shutil from slixmpp.xmlstream.matcher.base import MatcherBase @@ -180,8 +181,9 @@ class IrcServerRunner(ProcessRunner): os.remove("ircd.db") except FileNotFoundError: pass - subprocess.run(["oragono", "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) - self.create = asyncio.create_subprocess_exec("oragono", "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", + ergo_path = shutil.which("ergo") or shutil.which("ergochat") + subprocess.run([ergo_path, "mkcerts", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml"]) + self.create = asyncio.create_subprocess_exec(ergo_path, "run", "--conf", os.getcwd() + "/../tests/end_to_end/ircd.yaml", stderr=asyncio.subprocess.PIPE) class BiboumiTest: @@ -205,7 +207,8 @@ class BiboumiTest: pass logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s', - filename=output_filename) + filename=output_filename, + force=True) with open("test.conf", "w") as fd: fd.write(confs[self.scenario.conf]) @@ -241,7 +244,7 @@ class BiboumiTest: xmpp.saved_values.clear() - if xmpp.server: + if xmpp.boundjid.host: xmpp.accepting_server.close() return not failed diff --git a/tests/end_to_end/ircd.yaml b/tests/end_to_end/ircd.yaml index 057674c..ec21801 100644 --- a/tests/end_to_end/ircd.yaml +++ b/tests/end_to_end/ircd.yaml @@ -1,4 +1,4 @@ -# oragono IRCd config +# ergo IRCd config # network configuration network: @@ -33,7 +33,7 @@ server: # 'proxy' should typically be false. It's only for Kubernetes-style load # balancing that does not terminate TLS, but sends an initial PROXY line # in plaintext. - proxy: false + proxy: false # Example of a Unix domain socket for proxying: # "/tmp/oragono_sock": @@ -87,14 +87,15 @@ server: # casemapping controls what kinds of strings are permitted as identifiers (nicknames, # channel names, account names, etc.), and how they are normalized for case. - # with the recommended default of 'precis', utf-8 identifiers that are "sane" - # (according to RFC 8265) are allowed, and the server additionally tries to protect - # against confusable characters ("homoglyph attacks"). - # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive', - # which allows identifiers to contain unusual characters like emoji, but makes users - # vulnerable to homoglyph attacks. unless you're really confident in your decision, - # we recommend leaving this value at its default (changing it once the network is - # already up and running is problematic). + # the recommended default is 'ascii' (traditional ASCII-only identifiers). + # the other options are 'precis', which allows UTF8 identifiers that are "sane" + # (according to UFC 8265), with additional mitigations for homoglyph attacks, + # 'permissive', which allows identifiers containing unusual characters like + # emoji, at the cost of increased vulnerability to homoglyph attacks and potential + # client compatibility problems, and the legacy mappings 'rfc1459' and + # 'rfc1459-strict'. we recommend leaving this value at its default; + # however, note that changing it once the network is already up and running is + # problematic. casemapping: "precis" # whether to look up user hostnames with reverse DNS @@ -109,12 +110,12 @@ server: check-ident: false # password to login to the server - # generated using "oragono genpasswd" + # generated using "ergo genpasswd" #password: "" # motd filename # if you change the motd, you should move it to ircd.motd - motd: + # motd: # motd formatting codes # if this is true, the motd is escaped using formatting codes like $c, $b, and $i @@ -136,7 +137,7 @@ server: # (comment this out to use passwords only) fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" - # password the gateway uses to connect, made with oragono genpasswd + # password the gateway uses to connect, made with ergo genpasswd password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee" # addresses/CIDRs that can use this webirc command @@ -209,42 +210,13 @@ server: # IP cloaking hides users' IP addresses from other users and from channel admins # (but not from server admins), while still allowing channel admins to ban # offending IP addresses or networks. In place of hostnames derived from reverse - # DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are + # DNS, users see fake domain names like pwbs2ui4377257x8.ergo. These names are # generated deterministically from the underlying IP address, but if the underlying # IP is not already known, it is infeasible to recover it from the cloaked name. ip-cloaking: # whether to enable IP cloaking enabled: false - # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono - netname: "oragono" - - # secret key to prevent dictionary attacks against cloaked IPs - # any high-entropy secret is valid for this purpose: - # you MUST generate a new one for your installation. - # suggestion: use the output of `oragono mksecret` - # note that rotating this key will invalidate all existing ban masks. - secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" - - # name of an environment variable to pull the secret from, for use with - # k8s secret distribution: - # secret-environment-variable: "ORAGONO_CLOAKING_SECRET" - - # the cloaked hostname is derived only from the CIDR (most significant bits - # of the IP address), up to a configurable number of bits. this is the - # granularity at which bans will take effect for IPv4. Note that changing - # this value will invalidate any stored bans. - cidr-len-ipv4: 32 - - # analogous granularity for IPv6 - cidr-len-ipv6: 64 - - # number of bits of hash output to include in the cloaked hostname. - # more bits means less likelihood of distinct IPs colliding, - # at the cost of a longer cloaked hostname. if this value is set to 0, - # all users will receive simply `netname` as their cloaked hostname. - num-bits: 64 - # secure-nets identifies IPs and CIDRs which are secure at layer 3, # for example, because they are on a trusted internal LAN or a VPN. # plaintext connections from these IPs and CIDRs will be considered @@ -272,21 +244,6 @@ accounts: # length of time a user has to verify their account before it can be re-registered verify-timeout: "32h" - # callbacks to allow - enabled-callbacks: - - none # no verification needed, will instantly register successfully - - # example configuration for sending verification emails via a local mail relay - # callbacks: - # mailto: - # server: localhost - # port: 25 - # tls: - # enabled: false - # username: "" - # password: "" - # sender: "admin@my.network" - # throttle account login attempts (to prevent either password guessing, or DoS # attacks on the server aimed at forcing repeated expensive bcrypt computations) login-throttling: @@ -343,10 +300,16 @@ accounts: # rename-timeout - this is how long users have 'til they're renamed rename-timeout: 30s - # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) - rename-prefix: Guest- + # format for guest nicknames: + # 1. these nicknames cannot be registered or reserved + # 2. if a client is automatically renamed by the server, + # this is the template that will be used (e.g., Guest-nccj6rgmt97cg) + # 3. if force-guest-format (see below) is enabled, clients without + # a registered account will have this template applied to their + # nicknames (e.g., 'katie' will become 'Guest-katie') + guest-nickname-format: Guest-* - # multiclient controls whether oragono allows multiple connections to + # multiclient controls whether ergo allows multiple connections to # attach to the same client/nickname identity; this is part of the # functionality traditionally provided by a bouncer like ZNC multiclient: @@ -379,27 +342,6 @@ accounts: # (make sure any changes you make here are RFC-compliant) valid-regexp: '^[0-9A-Za-z.\-_/]+$' - # options controlling users requesting vhosts: - user-requests: - # can users request vhosts at all? if this is false, operators with the - # 'vhosts' capability can still assign vhosts manually - enabled: false - - # if uncommented, all new vhost requests will be dumped into the given - # channel, so opers can review them as they are sent in. ensure that you - # have registered and restricted the channel appropriately before you - # uncomment this. - #channel: "#vhosts" - - # after a user's vhost has been approved or rejected, they need to wait - # this long (starting from the time of their original request) - # before they can request a new one. - cooldown: 168h - - # vhosts that users can take without approval, using `/HS TAKE` - offer-list: - #- "oragono.test" - # support for deferring password checking to an external LDAP server # you should probably ignore this section! consult the grafana docs for details: # https://grafana.com/docs/grafana/latest/auth/ldap/ @@ -466,9 +408,8 @@ oper-classes: # capability names capabilities: - - "oper:local_kill" - - "oper:local_ban" - - "oper:local_unban" + - "kill" + - "ban" - "nofakelag" # network operator @@ -479,12 +420,6 @@ oper-classes: # oper class this extends from extends: "local-oper" - # capability names - capabilities: - - "oper:remote_kill" - - "oper:remote_ban" - - "oper:remote_unban" - # server admin "server-admin": # title shown in WHOIS @@ -495,8 +430,7 @@ oper-classes: # capability names capabilities: - - "oper:rehash" - - "oper:die" + - "rehash" - "accreg" - "sajoin" - "samode" @@ -522,7 +456,7 @@ opers: # operators can be authenticated either by password (with the /OPER command), # or by certificate fingerprint, or both. if a password hash is set, then a # password is required to oper up (e.g., /OPER dan mypassword). to generate - # the hash, use `oragono genpasswd`. + # the hash, use `ergo genpasswd`. password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" # if a SHA-256 certificate fingerprint is configured here, then it will be @@ -576,7 +510,7 @@ logging: # debug options debug: - # when enabled, oragono will attempt to recover from certain kinds of + # when enabled, ergo will attempt to recover from certain kinds of # client-triggered runtime errors that would normally crash the server. # this makes the server more resilient to DoS, but could result in incorrect # behavior. deployments that would prefer to "start from scratch", e.g., by @@ -606,7 +540,7 @@ datastore: host: "localhost" # port is unnecessary for connections via unix domain socket: #port: 3306 - user: "oragono" + user: "ergo" password: "hunter2" history-database: "oragono_history" timeout: 3s @@ -720,11 +654,6 @@ history: # (and will eventually be deleted from persistent storage, if that's enabled) #expire-time: 1w - # if this is set, logged-in users cannot retrieve messages older than their - # account registration date, and logged-out users cannot retrieve messages - # older than their sign-on time (modulo grace-period, see below): - enforce-registration-date: false - # but if this is set, you can retrieve messages that are up to `grace-period` # older than the above cutoff time. this is recommended to allow logged-out # users to do session resumption / query history after disconnections. diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py index b15ccc8..b5afdd6 100644 --- a/tests/end_to_end/scenarios/channel_custom_topic.py +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -14,13 +14,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py index c03b9b9..ef62088 100644 --- a/tests/end_to_end/scenarios/channel_force_join.py +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -11,7 +11,7 @@ scenario = ( send_stanza(""), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" ], [ "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py index a9f74a1..69da348 100644 --- a/tests/end_to_end/scenarios/channel_history.py +++ b/tests/end_to_end/scenarios/channel_history.py @@ -9,7 +9,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@irc.localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@{irc_host_one}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py index ad3701b..8a33bb5 100644 --- a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -11,7 +11,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@irc.localhost'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@{irc_host_one}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py index e9873b1..52af7f2 100644 --- a/tests/end_to_end/scenarios/channel_join_with_password.py +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -19,13 +19,13 @@ scenario = ( send_stanza(" SECRET"), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" ], [ "/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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py index 0e36db2..1ea52e2 100644 --- a/tests/end_to_end/scenarios/channel_join_with_two_users.py +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -8,13 +8,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" ], [ "/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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index 8215094..567e6ff 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -11,10 +11,10 @@ scenario = ( # Our presence, sent to the other user, and ourself expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"], diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index 73e9331..594761b 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -38,10 +38,10 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), # Our presence, sent to the other user expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@irc.localhost'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py index 98df0ea..efce8e7 100644 --- a/tests/end_to_end/scenarios/raw_message.py +++ b/tests/end_to_end/scenarios/raw_message.py @@ -7,5 +7,5 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} irc.localhost * {nick_one}']"), + expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_one} * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py index b5006c7..86fe7ed 100644 --- a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -9,6 +9,6 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='irc.localhost: {nick_one} ~{nick_one} irc.localhost * {nick_one}']"), + expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_one} * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/sasl.py b/tests/end_to_end/scenarios/sasl.py index 9f2a27a..97dd2fd 100644 --- a/tests/end_to_end/scenarios/sasl.py +++ b/tests/end_to_end/scenarios/sasl.py @@ -86,7 +86,9 @@ scenario = ( expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"), send_stanza(""), - # Here, the 6 connecting… connected messages from the connection attempt + # Here, the 8 connecting… connected messages from the connection attempt + expect_stanza("/message"), + expect_stanza("/message"), expect_stanza("/message"), expect_stanza("/message"), expect_stanza("/message"), diff --git a/tests/end_to_end/sequences.py b/tests/end_to_end/sequences.py index f151bc7..647c787 100644 --- a/tests/end_to_end/sequences.py +++ b/tests/end_to_end/sequences.py @@ -26,12 +26,14 @@ def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_serve if expected_irc_presence: result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) - if login is not None: - result += (expect_stanza("/message/body[text()='irc.localhost: You are now logged in as %s']" % (login,)),) result += ( expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']") ) + + if login is not None: + result += (expect_stanza("/message/body[text()='irc.localhost: You are now logged in as %s']" % (login,)),) + return result def connection_tls_begin(irc_host, jid, fixed_irc_server): @@ -61,12 +63,12 @@ def connection_end(irc_host, jid, fixed_irc_server=False): xpath_re = "/message[@to='" + jid + "'][@from='" + irc_host + "@biboumi.localhost']/body[re:test(text(), '%s')]" irc_host = 'irc.localhost' return ( - expect_stanza("/message/body[re:test(text(), '%s')]" % (r'^%s: Your host is %s, running version oragono-2\.0\.0(-[a-z0-9]+)? $' % (irc_host, irc_host))), + expect_stanza("/message/body[re:test(text(), '%s')]" % (r'^%s: Your host is %s, running version ergo-2\.\d+\.\d+(-[a-z0-9]+)? $' % (irc_host, irc_host))), expect_stanza(xpath_re % (r'^%s: This server was created .*$' % irc_host)), expect_stanza(xpath_re % (r'^%s: There are \d+ users and \d+ invisible on \d+ server\(s\)$' % irc_host)), - expect_stanza(xpath_re % ("%s: \d+ IRC Operators online" % irc_host,)), - expect_stanza(xpath_re % ("%s: \d+ unregistered connections" % irc_host,)), - expect_stanza(xpath_re % ("%s: \d+ channels formed" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ IRC Operators online" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ unregistered connections" % irc_host,)), + expect_stanza(xpath_re % ("%s: \\d+ channels formed" % irc_host,)), expect_stanza(xpath_re % (r'^%s: I have \d+ clients and \d+ servers$' % irc_host)), expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current local users \d+, max \d+$' % irc_host)), expect_stanza(xpath_re % (r'^%s: \d+ \d+ Current global users \d+, max \d+$' % irc_host)), From 0077d683a8788d58a6282761b902ccdcb9620188 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sun, 3 Aug 2025 14:43:03 +0200 Subject: [PATCH 33/65] Fix prod docker image build --- docker/biboumi/alpine/Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index cc58326..d887327 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -5,10 +5,10 @@ # This is the prefered way to build the release image, used by the # end users, in production. -FROM docker.io/alpine:3.21 as builder +FROM docker.io/alpine:latest as builder RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ - make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan-dev + make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan3-dev RUN git clone https://codeberg.org/poezio/biboumi && \ @@ -17,6 +17,7 @@ RUN git clone https://codeberg.org/poezio/biboumi && \ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_BUILD_TYPE=Release \ -DWITH_BOTAN=1 \ + -DWITHOUT_SYSTEMD=1 \ -DWITH_SQLITE3=1 \ -DWITH_LIBIDN=1 \ -DWITH_POSTGRESQL=1 && \ @@ -25,15 +26,15 @@ RUN git clone https://codeberg.org/poezio/biboumi && \ # --- -FROM docker.io/alpine:3.21 +FROM docker.io/alpine:latest RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ - sqlite-libs udns expat ca-certificates botan + sqlite-libs udns expat ca-certificates botan3 COPY --from=builder /etc/biboumi /etc/biboumi COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi -COPY ./docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg +COPY --from=builder /biboumi/docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg RUN adduser biboumi -D -h /home/biboumi && \ mkdir /var/lib/biboumi && \ From d1d35f2f0462d80fd96157cce0cbe9ed9181eda6 Mon Sep 17 00:00:00 2001 From: Mike Wilson Date: Thu, 16 Oct 2025 15:57:34 -0400 Subject: [PATCH 34/65] Make wording for persistence options more clear in docs --- doc/admin.rst | 12 +++++------- doc/user.rst | 13 +++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/doc/admin.rst b/doc/admin.rst index ec80112..08ef2f2 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -147,18 +147,16 @@ any other IRC server. persistent_by_default ~~~~~~~~~~~~~~~~~~~~~ -If this option is set to `true`, all rooms will be persistent by default: +If this option is set to "true", all rooms will be persistent by default: the value of the “persistent” option in the global configuration of each user will be “true”, but the value of each individual room will still -default to false. This means that a user just needs to change the global -“persistent” configuration option to false in order to override this. +default to "false". This means that a user just needs to change their global +“persistent” configuration option to "false" in order to override this +and be able to set rooms to not persistent. -If it is set to false (the default value), all rooms are not persistent by +If this option is set to "false" (the default value), all rooms are not persistent by default. -Each room can be configured individually by each user, to override this -default value. See :ref:`Ad-hoc commands`. - realname_customization ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/user.rst b/doc/user.rst index 023036a..065d9f0 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -399,12 +399,13 @@ The provided configuration form contains these fields: the database. - **Max history length**: The maximum number of lines in the history that the server is allowed to send when joining a channel. -- **Persistent**: Overrides the value specified in each individual - channel. If this option is set to true, all channels are persistent, - whether or not their specific value is true or false. This option is true - by default for everyone if the `persistent_by_default` configuration - option is true, otherwise it’s false. See below for more details on what a - persistent channel is. +- **Persistent**: If this option is set to true, the persistence setting + specified on each individual channel is ignored and all channels are persistent. + If this option is set to false, the persistence setting specified on each + individual channel is respected. + This option defaults to the value of :ref:`persistent_by_default`, which itself + defaults to false. + See below for more details on what a persistent channel is. On a server JID ~~~~~~~~~~~~~~~ From ccb5b4f712d4faa66bfb57a183ca5cb807d93f2d Mon Sep 17 00:00:00 2001 From: mathieui Date: Tue, 21 Oct 2025 22:51:33 +0200 Subject: [PATCH 35/65] ci: trigger a rebuild of the containers it probably fell through during woodpecker issues. --- .woodpecker/containers-packaging.yaml | 2 +- .woodpecker/containers-test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker/containers-packaging.yaml b/.woodpecker/containers-packaging.yaml index 17c945b..4cbb7e7 100644 --- a/.woodpecker/containers-packaging.yaml +++ b/.woodpecker/containers-packaging.yaml @@ -2,7 +2,7 @@ when: event: [ push ] branch: master path: - include: ["docker/packaging/**/*", ".woodpecker/containers-packaging.yaml"] + include: ["docker/packaging/**/Dockerfile", ".woodpecker/containers-packaging.yaml"] labels: platform: linux/amd64 diff --git a/.woodpecker/containers-test.yaml b/.woodpecker/containers-test.yaml index 76acd13..f7aeb8b 100644 --- a/.woodpecker/containers-test.yaml +++ b/.woodpecker/containers-test.yaml @@ -2,7 +2,7 @@ when: event: [ push ] branch: master path: - include: ["docker/test/**/*", ".woodpecker/containers-test.yaml"] + include: ["docker/test/**/Dockerfile", ".woodpecker/containers-test.yaml"] labels: platform: linux/amd64 From 3b03b791421ec728b4327b0ac72b5ff8ca8219cc Mon Sep 17 00:00:00 2001 From: mathieui Date: Wed, 22 Oct 2025 22:26:24 +0200 Subject: [PATCH 36/65] ci: drop the tests on fedora since fedora does not package botan3 yet, we would be stuck with a non-TLS biboumi which cannot run the e2e tests as well as provide no security whatsoever. --- .woodpecker/test.yaml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index e4077b8..f3b3c36 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -14,32 +14,6 @@ matrix: - "-DWITH_BOTAN=0" steps: - build-fedora: - image: codeberg.org/biboumi/ci-test-fedora:latest - environment: - COMPILER: "g++" - BUILD_TYPE: "Release" - SQLITE3: "-DWITH_SQLITE3=1" - CXX_FLAGS: "-Werror -Wno-psabi" - LIBIDN: "-DWITH_LIBIDN=1" - POSTGRESQL: "-WITH_POSTGRESQL=1" - UDNS: "-DWITH_UDNS=1" - SYSTEMD: "-DWITH_SYSTEMD=1" - commands: - - echo "building with parameters '-DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL'" - - mkdir build/ - - cd build/ - - cmake .. -DCMAKE_CXX_FLAGS="$CXX_FLAGS" -DCMAKE_CXX_COMPILER=$COMPILER -DCMAKE_BUILD_TYPE=$BUILD_TYPE $BOTAN $UDNS $SYSTEMD $LIBIDN $SQLITE3 $POSTGRESQL - - make everything -j$(nproc || echo 1) - - test-fedora: - depends_on: [build-fedora] - image: codeberg.org/biboumi/ci-test-fedora:latest - commands: - - cd build/ - - make check_junit || true; make check - - make e2e - build-alpine: image: codeberg.org/biboumi/ci-test-alpine:latest environment: From 1ea693a0d60e79adbf3a702e86d23fe3075084e7 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sat, 25 Oct 2025 16:44:23 +0200 Subject: [PATCH 37/65] fix e2e tests --- tests/end_to_end/scenarios/channel_custom_topic.py | 4 ++-- tests/end_to_end/scenarios/channel_force_join.py | 2 +- tests/end_to_end/scenarios/channel_history.py | 2 +- tests/end_to_end/scenarios/channel_history_on_fixed_server.py | 2 +- tests/end_to_end/scenarios/channel_join_with_password.py | 4 ++-- tests/end_to_end/scenarios/channel_join_with_two_users.py | 4 ++-- tests/end_to_end/scenarios/channel_messages.py | 4 ++-- tests/end_to_end/scenarios/multiline_message.py | 4 ++-- tests/end_to_end/scenarios/raw_message.py | 2 +- tests/end_to_end/scenarios/raw_message_fixed_irc_server.py | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/end_to_end/scenarios/channel_custom_topic.py b/tests/end_to_end/scenarios/channel_custom_topic.py index b5afdd6..089dbd9 100644 --- a/tests/end_to_end/scenarios/channel_custom_topic.py +++ b/tests/end_to_end/scenarios/channel_custom_topic.py @@ -14,13 +14,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), expect_unordered( [ - "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_force_join.py b/tests/end_to_end/scenarios/channel_force_join.py index ef62088..c708570 100644 --- a/tests/end_to_end/scenarios/channel_force_join.py +++ b/tests/end_to_end/scenarios/channel_force_join.py @@ -11,7 +11,7 @@ scenario = ( send_stanza(""), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']", diff --git a/tests/end_to_end/scenarios/channel_history.py b/tests/end_to_end/scenarios/channel_history.py index 69da348..f18cf27 100644 --- a/tests/end_to_end/scenarios/channel_history.py +++ b/tests/end_to_end/scenarios/channel_history.py @@ -9,7 +9,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@{irc_host_one}'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}%{irc_server_one}/~{nick_one}@{irc_host_two}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py index 8a33bb5..4a18e87 100644 --- a/tests/end_to_end/scenarios/channel_history_on_fixed_server.py +++ b/tests/end_to_end/scenarios/channel_history_on_fixed_server.py @@ -11,7 +11,7 @@ scenario = ( # Second user joins send_stanza(""), - expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@{irc_host_one}'][@role='moderator']", + expect_stanza("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='{lower_nick_one}@{biboumi_host}/~{nick_one}@{irc_host_two}'][@role='moderator']", "/presence/muc_user:x/muc_user:status[@code='110']"), # Receive the history message expect_stanza("/message[@from='#foo@{biboumi_host}/{nick_one}']/body[text()='coucou']", diff --git a/tests/end_to_end/scenarios/channel_join_with_password.py b/tests/end_to_end/scenarios/channel_join_with_password.py index 52af7f2..c2514c8 100644 --- a/tests/end_to_end/scenarios/channel_join_with_password.py +++ b/tests/end_to_end/scenarios/channel_join_with_password.py @@ -19,13 +19,13 @@ scenario = ( send_stanza(" SECRET"), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_join_with_two_users.py b/tests/end_to_end/scenarios/channel_join_with_two_users.py index 1ea52e2..571d567 100644 --- a/tests/end_to_end/scenarios/channel_join_with_two_users.py +++ b/tests/end_to_end/scenarios/channel_join_with_two_users.py @@ -8,13 +8,13 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), 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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}']" + "/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}']" ], [ "/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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], [ diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index 567e6ff..3262832 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -11,10 +11,10 @@ scenario = ( # Our presence, sent to the other user, and ourself expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"], diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index 594761b..c29c1e7 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -38,10 +38,10 @@ scenario = ( sequences.connection("irc.localhost", '{jid_two}/{resource_one}'), # Our presence, sent to the other user expect_unordered( - ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']"], + ["/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@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='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_one}'][@role='participant']", + "/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='{lower_nick_two}%{irc_server_one}/~{nick_two}@{irc_host_two}'][@role='participant']", "/presence/muc_user:x/muc_user:status[@code='110']" ], ["/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"] diff --git a/tests/end_to_end/scenarios/raw_message.py b/tests/end_to_end/scenarios/raw_message.py index efce8e7..b7064f4 100644 --- a/tests/end_to_end/scenarios/raw_message.py +++ b/tests/end_to_end/scenarios/raw_message.py @@ -7,5 +7,5 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_one} * {nick_one}']"), + expect_stanza("/message[@from='{irc_server_one}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_two} * {nick_one}']"), ) diff --git a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py index 86fe7ed..638f466 100644 --- a/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py +++ b/tests/end_to_end/scenarios/raw_message_fixed_irc_server.py @@ -9,6 +9,6 @@ scenario = ( expect_stanza("/message"), send_stanza("WHOIS {nick_one}"), - expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_one} * {nick_one}']"), + expect_stanza("/message[@from='{biboumi_host}'][@type='chat']/body[text()='{irc_host_one}: {nick_one} ~{nick_one} {irc_host_two} * {nick_one}']"), ) From 61242c35bc825d58c9db4301b5696bc17428bf98 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 27 Oct 2025 19:37:59 +0100 Subject: [PATCH 38/65] docs(developer): Point to codeberg and not louiz.org Since biboumi has moved from louiz.org to codeberg.org the docker image is named differently, as well as under a new URL. --- doc/developer.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/developer.rst b/doc/developer.rst index b3ef158..8333716 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -44,9 +44,7 @@ docker is very unsecure) is to follow these instructions: :caption: Start a docker container with everything installed docker run --name biboumi-e2e -v /home/louiz/biboumi/:/home/tester/biboumi \ - --add-host="irc.localhost:127.0.0.1" \ - --add-host="biboumi.localhost:127.0.0.1" \ - --rm -it docker.louiz.org/louiz/biboumi/test-alpine \ + --rm -it codeberg.org/biboumi/ci-test-alpine \ /bin/bash This creates a container where every dependency is already installed. We @@ -54,9 +52,9 @@ mount your working directory inside the container: be sure to modify the first path `/home/louiz/biboumi` with your own. The hosts that we add are needed for the test suite to properly work. -You can use the test-fedora or test-debian images instead of test-alpine -if you want, but it should not change anything (even if your host machine -uses debian or fedora), alpine is just the lighter one. +You can use the ci-test-fedora or ci-test-debian images instead of +ci-test-alpine if you want, but it should not change anything (even if your +host machine uses debian or fedora), alpine is just the lighter one. .. note:: From e1ad60af4fd267ac1df57748afc910b2ea26a8b4 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 27 Oct 2025 19:08:32 +0100 Subject: [PATCH 39/65] tests(iid): Explicitly set fixed_irc_server These tests require that fixed_irc_server is set, and will fail if another test sets them. This may not have been a problem before, but since the catch2 update CI has encountered issues running these tests randomly, as the order of which tests are ran in is now random. --- tests/iid.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/iid.cpp b/tests/iid.cpp index dc264fd..17744dd 100644 --- a/tests/iid.cpp +++ b/tests/iid.cpp @@ -7,6 +7,7 @@ TEST_CASE("Irc user parsing") { + Config::set("fixed_irc_server", "", false); const std::map prefixes{{'!', 'a'}, {'@', 'o'}}; IrcUser user1("!nick!~some@host.bla", prefixes); CHECK(user1.nick == "nick"); @@ -23,6 +24,7 @@ TEST_CASE("Irc user parsing") TEST_CASE("multi-prefix") { + Config::set("fixed_irc_server", "", false); const std::map prefixes{{'!', 'a'}, {'@', 'o'}, {'~', 'f'}}; IrcUser user("!@~nick", prefixes); CHECK(user.nick == "nick"); @@ -47,6 +49,7 @@ namespace Catch TEST_CASE("Iid creation") { + Config::set("fixed_irc_server", "", false); const std::set chantypes {'&', '#', '+', '!'}; Iid iid1("foo%irc.example.org", chantypes); CHECK(std::to_string(iid1) == "foo%irc.example.org"); From cf6dec8ce549524f49faa84aa73143bc821afd3d Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 27 Oct 2025 20:11:08 +0100 Subject: [PATCH 40/65] tests(invite_other): Fix flakey test The order of these events doesn't really matter, and may be sent in out of order sometimes. As such use expect_unordered as to not fail CI randomly. --- tests/end_to_end/scenarios/invite_other.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/end_to_end/scenarios/invite_other.py b/tests/end_to_end/scenarios/invite_other.py index 0e40dcb..030c65d 100644 --- a/tests/end_to_end/scenarios/invite_other.py +++ b/tests/end_to_end/scenarios/invite_other.py @@ -11,8 +11,10 @@ scenario = ( expect_stanza("/presence"), expect_stanza("/message"), send_stanza(""), - expect_stanza("/message/body[text()='{nick_two} has been invited to #foo']"), - expect_stanza("/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"), + expect_unordered( + ["/message/body[text()='{nick_two} has been invited to #foo']"], + ["/message[@to='{jid_two}/{resource_two}'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='#foo%{irc_server_one}/{nick_one}']"], + ), send_stanza(""), expect_stanza("/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"), From d13f66ab905d3857a2fecb4155647e41f4be6450 Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Mon, 19 Feb 2024 15:10:16 +0000 Subject: [PATCH 41/65] Add support for IRCv3 message-tags Implement draft/multiline --- src/bridge/bridge.cpp | 59 +++++++++++++++ src/irc/irc_client.cpp | 155 +++++++++++++++++++++++++++++++++++++--- src/irc/irc_client.hpp | 11 +++ src/irc/irc_message.cpp | 42 ++++++++++- src/irc/irc_message.hpp | 4 ++ 5 files changed, 262 insertions(+), 9 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index ec76ef6..4e9d42d 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -14,6 +14,7 @@ #include "result_set_management.hpp" #include #include +#include using namespace std::string_literals; @@ -221,6 +222,64 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: std::vector lines = utils::split(body, '\n', true); if (lines.empty()) return ; + + if (irc->has_capability("draft/multiline")) + { + std::string uuid; + const auto xmpp_body = this->make_xmpp_body(body); +#ifdef USE_DATABASE + if (this->record_history) + 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()); +#else + uuid = utils::gen_uuid(); +#endif + + IrcChannel* channel = irc->get_channel(iid.get_local()); + if (!channel->joined) + { + log_warning("Cannot send message to channel ", iid.get_local(), ", it is not joined"); + return; + } + + std::vector messages; + for (const std::string& line: lines) + { + constexpr auto max_username_size = 10; + constexpr auto max_hostname_size = 63; + const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - ::strlen("draft/multiline-concat") - 1 - + irc->get_own_nick().size() - max_username_size - max_hostname_size - + ::strlen(":!@ PRIVMSG ") - iid.get_local().length() - ::strlen(" :\r\n"); + const auto privmsgs = cut(line, line_size); + bool first = true; + for (const auto& privmsg: privmsgs) + { + if (first) + { + messages.push_back(IrcMessage({}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + first = false; + } + else + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + } + } + + MessageCallback mirror_to_all_resources = [this, iid, uuid, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), + this->user_jid + "/" + + resource, uuid, uuid); + for (const auto& node: nodes_to_reflect) + stanza.add_child(node); + this->xmpp.send_stanza(stanza); + } + }; + + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); + return; + } + bool first = true; for (const std::string& line: lines) { diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 72da2b4..a1b972c 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -83,6 +83,7 @@ static const std::unordered_mapsend_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); + this->capabilities["batch"] = {[]{}, []{}}; + this->capabilities["message-tags"] = {[]{}, []{}}; this->capabilities["multi-prefix"] = {[]{}, []{}}; + this->capabilities["draft/multiline"] = {[]{}, []{}}; #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), @@ -416,12 +420,34 @@ void IrcClient::parse_in_buffer(const size_t) "”: ", args_size); else { - const auto& cb = it->second.first; - try { - (this->*(cb))(message); - } catch (const std::exception& e) { - log_error("Unhandled exception: ", e.what()); - } + bool added_to_batch = false; + if (this->has_capability("batch")) + { + auto it_tags = message.tags.find("batch"); + if (it_tags != message.tags.end()) + if (it_tags->second.has_value()) + { + auto it_batches = this->batches.find(it_tags->second.value()); + if (it_batches != this->batches.end()) + { + auto message_tags = message.tags; + auto message_prefix = message.prefix; + auto message_command = message.command; + auto message_arguments = message.arguments; + it_batches->second.second.emplace_back(std::move(message_tags), std::move(message_prefix), std::move(message_command), std::move(message_arguments)); + added_to_batch = true; + } + } + } + if (!added_to_batch) + { + const auto& cb = it->second.first; + try { + (this->*(cb))(message); + } catch (const std::exception& e) { + log_error("Unhandled exception: ", e.what()); + } + } } } else @@ -441,6 +467,16 @@ void IrcClient::actual_send(std::pair&& message_pai const MessageCallback& callback = message_pair.second; log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); std::string res; + if (!message.tags.empty()) + { + res += "@"; + for (auto& tag: message.tags) + if (tag.second.has_value()) + res += tag.first + "=" + tag.second.value() + ";"; + else + res += tag.first + ";"; + res.back() = ' '; + } if (!message.prefix.empty()) res += ":" + message.prefix + " "; res += message.command; @@ -606,6 +642,22 @@ void IrcClient::send_ping_command() this->send_message(IrcMessage("PING", {"biboumi"})); } +void IrcClient::send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback) +{ + std::vector args; + args.push_back("+" + reference_tag); + args.push_back(type); + for (const std::string& parameter: parameters) + args.push_back(parameter); + this->send_message(IrcMessage("BATCH", std::move(args))); + for (IrcMessage& message: messages) + { + message.tags["batch"] = reference_tag; + this->send_message(std::move(message)); + } + this->send_message(IrcMessage("BATCH", {"-" + reference_tag}), callback); +} + void IrcClient::forward_server_message(const IrcMessage& message) { const std::string from = message.prefix; @@ -1351,11 +1403,16 @@ void IrcClient::on_cap(const IrcMessage &message) } Capability& capability = it->second; if (sub_command == "ACK") - capability.on_ack(); + { + capability.on_ack(); + this->enabled_capabilities.insert(it->first); + } else if (sub_command == "NACK") - capability.on_nack(); + capability.on_nack(); this->capabilities.erase(it); } + for (const auto& batch: this->batches) + log_debug("Batch: ", batch.first, " exists"); if (this->capabilities.empty()) this->cap_end(); } @@ -1419,3 +1476,85 @@ void IrcClient::cap_end() this->send_message({"CAP", {"END"}}); this->bridge.on_irc_client_connected(this->get_hostname()); } + +bool IrcClient::has_capability(const std::string& capability) +{ + auto it = this->enabled_capabilities.find(capability); + return it != this->enabled_capabilities.end(); +} + +void IrcClient::on_batch(const IrcMessage& message) +{ + if (message.arguments[0][0] == '+') + { + if (message.arguments.size() < 2) + return; + if (message.arguments[1] == "draft/multiline" && message.arguments.size() != 3) + return; + this->batches.emplace(message.arguments[0].substr(1), std::make_pair(std::make_pair(message.prefix, message.arguments), std::vector())); + } + else if (message.arguments[0][0] == '-') + { + const std::string reference_tag = message.arguments[0].substr(1); + this->process_batch(reference_tag); + this->batches.erase(reference_tag); + } + return; +} + +void IrcClient::process_batch(const std::string& reference_tag) +{ + auto it = this->batches.find(reference_tag); + if (it == this->batches.end()) + return; + if (it->second.first.second[1] == "draft/multiline") + { + const IrcUser user(it->second.first.first); + const std::string nick = user.nick; + Iid iid; + iid.set_local(it->second.first.second[2]); + iid.set_server(this->hostname); + bool muc = true; + if (!this->get_channel(iid.get_local())->joined) + { + iid.type = Iid::Type::User; + iid.set_local(nick); + muc = false; + } + else + iid.type = Iid::Type::Channel; + + std::string body; + for (const IrcMessage& message: it->second.second) + { + auto it_tags = message.tags.find("draft/multiline-concat"); + if (it_tags == message.tags.end()) + body += "\n" + message.arguments[1]; + else + body += message.arguments[1]; + } + if (body.find_first_not_of("\n") == std::string::npos) + // Empty message + return; + this->bridge.send_message(iid, nick, body, muc); + } + else + { + for (const IrcMessage& message: it->second.second) + { + auto it_callback = irc_callbacks.find(message.command); + if (it_callback == irc_callbacks.end()) + continue; + const auto& cb = it_callback->second.first; + try + { + (this->*(cb))(message); + } + catch (const std::exception& e) + { + log_error("Unhandled exception: ", e.what()); + } + } + } + return; +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 3423228..4090ab1 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -26,6 +26,7 @@ #include #include #include +#include class IrcClient; @@ -155,6 +156,10 @@ public: * Send the QUIT irc command */ void send_quit_command(const std::string& reason); + /** + * Send a batch of messages + */ + void send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback); /** * Send a message to the gateway user, not generated by the IRC server, * but that might be useful because we want to be verbose (for example we @@ -244,6 +249,7 @@ public: void on_cap(const IrcMessage& message); private: void cap_end(); + void process_batch(const std::string& reference_tag); public: #ifdef WITH_SASL void on_authenticate(const IrcMessage& message); @@ -306,6 +312,7 @@ public: */ void on_channel_mode(const IrcMessage& message); void on_quit(const IrcMessage& message); + void on_batch(const IrcMessage& message); void on_unknown_message(const IrcMessage& message); /** * Return the number of joined channels @@ -326,6 +333,8 @@ public: * Store the history limit that the client asked when joining this room. */ HistoryLimit history_limit; + + bool has_capability(const std::string& capability); private: /** * The hostname of the server we are connected to. @@ -387,6 +396,7 @@ private: SaslState sasl_state{SaslState::unneeded}; #endif std::map capabilities; + std::unordered_set enabled_capabilities; /** * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3 * We store the possible chanmodes in this object. @@ -431,6 +441,7 @@ private: Resolver dns_resolver; TokensBucket tokens_bucket; long int get_throttle_limit() const; + std::unordered_map>, std::vector>> batches; }; diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp index 62fe9a7..c056a8c 100644 --- a/src/irc/irc_message.cpp +++ b/src/irc/irc_message.cpp @@ -1,8 +1,27 @@ #include #include +#include +#include +#include IrcMessage::IrcMessage(std::stringstream ss) { + if (ss.peek() == '@') + { + std::string raw_tags; + ss.ignore(); + ss >> raw_tags; + std::vector tags = utils::split(raw_tags, ';', false); + for (auto &tag: tags) + { + auto pos = tag.find('='); + if (pos == std::string::npos) + this->tags[tag] = {}; + else + this->tags[tag.substr(0, pos)] = tag.substr(pos+1); + } + ss.ignore(); + } if (ss.peek() == ':') { ss.ignore(); @@ -30,6 +49,7 @@ IrcMessage::IrcMessage(std::stringstream ss) IrcMessage::IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args): + tags(), prefix(std::move(prefix)), command(std::move(command)), arguments(std::move(args)) @@ -38,16 +58,36 @@ IrcMessage::IrcMessage(std::string&& prefix, IrcMessage::IrcMessage(std::string&& command, std::vector&& args): + tags(), prefix(), command(std::move(command)), arguments(std::move(args)) { } +IrcMessage::IrcMessage(std::unordered_map>&& tags, + std::string&& prefix, + std::string&& command, + std::vector&& args): + tags(std::move(tags)), + prefix(std::move(prefix)), + command(std::move(command)), + arguments(std::move(args)) +{ +} + std::ostream& operator<<(std::ostream& os, const IrcMessage& message) { os << "IrcMessage"; - os << "[" << message.command << "]"; + os << "{"; + for (auto& tag: message.tags) + { + os << tag.first; + if (tag.second.has_value()) + os << "=" << tag.second.value(); + os << ";"; + } + os << "}[" << message.command << "]"; for (const std::string& arg: message.arguments) { os << "{" << arg << "}"; diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index 5475fd2..17d1c39 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include class IrcMessage { @@ -13,6 +15,7 @@ public: IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {} IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args); IrcMessage(std::string&& command, std::vector&& args); + IrcMessage(std::unordered_map>&& tags, std::string&& prefix, std::string&& command, std::vector&& args); ~IrcMessage() = default; IrcMessage(const IrcMessage&) = delete; @@ -20,6 +23,7 @@ public: IrcMessage& operator=(const IrcMessage&) = delete; IrcMessage& operator=(IrcMessage&&) = default; + std::unordered_map> tags; std::string prefix; std::string command; std::vector arguments; From 7bc27b21e606c3f92091fb422966fc0595658b3f Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Thu, 31 Jul 2025 14:11:42 +0200 Subject: [PATCH 42/65] fix tests --- src/bridge/bridge.cpp | 6 ++++-- tests/end_to_end/scenarios/channel_messages.py | 2 +- tests/end_to_end/scenarios/multiline_message.py | 12 +++--------- .../end_to_end/scenarios/slash_me_channel_message.py | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 4e9d42d..990055b 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -234,6 +234,8 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: #else uuid = utils::gen_uuid(); #endif + if (id.empty()) + id = utils::gen_uuid(); IrcChannel* channel = irc->get_channel(iid.get_local()); if (!channel->joined) @@ -264,12 +266,12 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: } } - MessageCallback mirror_to_all_resources = [this, iid, uuid, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { + MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) { auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), this->user_jid + "/" - + resource, uuid, uuid); + + resource, uuid, id); for (const auto& node: nodes_to_reflect) stanza.add_child(node); this->xmpp.send_stanza(stanza); diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index 3262832..b55914e 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -29,7 +29,7 @@ scenario = ( "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" ], [ - "/message[@id][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']", + "/message[@id][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='\ncoucou']", "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" ] ), diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index c29c1e7..c35c44b 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -8,9 +8,7 @@ scenario = ( # Send a multi-line channel message send_stanza("un\ndeux\ntrois"), # Receive multiple messages, in order - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"), - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"), - expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"), + expect_stanza("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un\ndeux\ntrois']"), # Send a simple message, with no id send_stanza("hello"), @@ -51,12 +49,8 @@ scenario = ( send_stanza("a\nb\nc"), # Receive multiple messages, for each user expect_unordered( - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='b']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='c']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='b']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='c']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='\na\nb\nc']"], ) ) diff --git a/tests/end_to_end/scenarios/slash_me_channel_message.py b/tests/end_to_end/scenarios/slash_me_channel_message.py index d30fba3..28d1289 100644 --- a/tests/end_to_end/scenarios/slash_me_channel_message.py +++ b/tests/end_to_end/scenarios/slash_me_channel_message.py @@ -11,7 +11,7 @@ scenario = ( "/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()='/me rit en IRC']", + "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='\n/me rit en IRC']", "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" ], ), From 6993577cbe7260690c68a7fe2119dd76b5ea13a2 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sun, 3 Aug 2025 13:53:47 +0200 Subject: [PATCH 43/65] fix mode tests --- src/bridge/bridge.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 990055b..df32551 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -258,11 +258,11 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: { if (first) { - messages.push_back(IrcMessage({}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + messages.push_back(IrcMessage({}, std::string(), "PRIVMSG", {iid.get_local(), privmsg})); first = false; } else - messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::string(), "PRIVMSG", {iid.get_local(), privmsg})); } } @@ -278,6 +278,14 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: } }; + // MODE can't be multiline. Only check the first line, and discard the rest silently + if (lines[0].substr(0, 5) == "/mode") + { + std::vector args = utils::split(body.substr(5), ' ', false); + irc->send_mode_command(iid.get_local(), args); + return; // We do not want to send that back to the + // XMPP user, that’s not a textual message. + } irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); return; } From f2a55c4f278379d8cd0a964434b7108d3aae7bf5 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 4 Aug 2025 19:35:14 +0200 Subject: [PATCH 44/65] fix multiline tests --- src/bridge/bridge.cpp | 14 ++++---------- src/irc/irc_client.cpp | 2 +- tests/end_to_end/scenarios/channel_messages.py | 2 +- tests/end_to_end/scenarios/multiline_message.py | 2 +- .../scenarios/slash_me_channel_message.py | 2 +- tests/end_to_end/sequences.py | 6 +++--- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index df32551..0b90c8b 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -223,7 +223,10 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: if (lines.empty()) return ; - if (irc->has_capability("draft/multiline")) + if (irc->has_capability("draft/multiline") + // /me and /mode cannot be multi lined + && lines[0].substr(0, 5) != "/mode" + && lines[0].substr(0, 4) != "/me ") { std::string uuid; const auto xmpp_body = this->make_xmpp_body(body); @@ -277,15 +280,6 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: this->xmpp.send_stanza(stanza); } }; - - // MODE can't be multiline. Only check the first line, and discard the rest silently - if (lines[0].substr(0, 5) == "/mode") - { - std::vector args = utils::split(body.substr(5), ' ', false); - irc->send_mode_command(iid.get_local(), args); - return; // We do not want to send that back to the - // XMPP user, that’s not a textual message. - } irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); return; } diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index a1b972c..5e0503e 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -1528,7 +1528,7 @@ void IrcClient::process_batch(const std::string& reference_tag) for (const IrcMessage& message: it->second.second) { auto it_tags = message.tags.find("draft/multiline-concat"); - if (it_tags == message.tags.end()) + if (!body.empty() && it_tags == message.tags.end()) body += "\n" + message.arguments[1]; else body += message.arguments[1]; diff --git a/tests/end_to_end/scenarios/channel_messages.py b/tests/end_to_end/scenarios/channel_messages.py index b55914e..3262832 100644 --- a/tests/end_to_end/scenarios/channel_messages.py +++ b/tests/end_to_end/scenarios/channel_messages.py @@ -29,7 +29,7 @@ scenario = ( "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" ], [ - "/message[@id][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='\ncoucou']", + "/message[@id][@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]" ] ), diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index c35c44b..e26bc90 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -51,6 +51,6 @@ scenario = ( expect_unordered( ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], - ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='\na\nb\nc']"], + ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], ) ) diff --git a/tests/end_to_end/scenarios/slash_me_channel_message.py b/tests/end_to_end/scenarios/slash_me_channel_message.py index 28d1289..d30fba3 100644 --- a/tests/end_to_end/scenarios/slash_me_channel_message.py +++ b/tests/end_to_end/scenarios/slash_me_channel_message.py @@ -11,7 +11,7 @@ scenario = ( "/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()='\n/me rit en IRC']", + "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='/me rit en IRC']", "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]" ], ), diff --git a/tests/end_to_end/sequences.py b/tests/end_to_end/sequences.py index 647c787..e4117f6 100644 --- a/tests/end_to_end/sequences.py +++ b/tests/end_to_end/sequences.py @@ -23,14 +23,14 @@ def connection_begin(irc_host, jid, expected_irc_presence=False, fixed_irc_serve expect_stanza(xpath % ('Connecting to %s:6667 (not encrypted)' % irc_host)), expect_stanza(xpath % 'Connected to IRC server.')) - if expected_irc_presence: - result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) - result += ( expect_stanza("/message/body[text()='irc.localhost: *** Looking up your hostname...']"), expect_stanza("/message/body[text()='irc.localhost: *** Found your hostname']") ) + if expected_irc_presence: + result += (expect_stanza("/presence[@from='" + irc_host + "@biboumi.localhost']"),) + if login is not None: result += (expect_stanza("/message/body[text()='irc.localhost: You are now logged in as %s']" % (login,)),) From fdae7b03e090cb2de88c6f9088c5d7862dde1a3a Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 4 Aug 2025 19:58:23 +0200 Subject: [PATCH 45/65] Update Changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 85f4d19..302a96a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,9 @@ For users nickname%irc@biboumi. - Message Correction (XEP-0308) no longer works, and will instead return an error when the user attempts to. +- XMPP users can send multiline messages on IRC servers which support + draft/multiline, as well as IRC users sending multiline messages will appear as + one message on XMPP. For admins ---------- From fbb93dbd08291837548d516a19cde8a0a802c3d8 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Tue, 14 Oct 2025 20:45:44 +0200 Subject: [PATCH 46/65] add persist_user_denylist and persist_user_allowlist Closes: #3511 --- CHANGELOG.rst | 2 + doc/admin.rst | 13 +++ src/bridge/bridge.cpp | 5 +- src/utils/is_requester_allowed_to_persist.hpp | 55 ++++++++++++ src/xmpp/biboumi_adhoc_commands.cpp | 9 +- tests/is_requester_allowed_to_persist.cpp | 89 +++++++++++++++++++ 6 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/utils/is_requester_allowed_to_persist.hpp create mode 100644 tests/is_requester_allowed_to_persist.cpp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 302a96a..981e000 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,8 @@ For admins ---------- - Command line option --test-config (or -t) has been added. When used, biboumi will just exit without any error if the configuration is correct +- Options persist_user_denylist and persist_user_allowlist have been added to + granualy control who can set the persist option For packagers ------------- diff --git a/doc/admin.rst b/doc/admin.rst index 08ef2f2..a04a59a 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -246,6 +246,19 @@ configuration file is located: for example if biboumi reads its configuration from /etc/biboumi/biboumi.cfg, the policy_directory value will be /etc/biboumi. +persist_user_denylist +~~~~~~~~~~~~~~~~~~~~~ + +A list of XMPP users or domains who are not allowed to change the `persist` +ad-hoc option and who will not be impacted by the `persistent_by_default` +config option. Specific JIDs in this list will take priority over allowed JIDs +or domains. + +persist_user_allowlist +~~~~~~~~~~~~~~~~~~~~~~ + +A list of XMPP XMPP users or domains who are allowed to change the `persist` +ad-hoc option. TLS configuration ----------------- diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 0b90c8b..b123f63 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -499,7 +500,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con bool persistent = false; #ifdef USE_DATABASE const auto goptions = Database::get_global_options(this->user_jid); - if (goptions.col()) + if (!utils::is_requester_allowed_to_persist(Jid(this->user_jid))) + persistent = false; + else if (goptions.col()) persistent = true; else { diff --git a/src/utils/is_requester_allowed_to_persist.hpp b/src/utils/is_requester_allowed_to_persist.hpp new file mode 100644 index 0000000..5dc5956 --- /dev/null +++ b/src/utils/is_requester_allowed_to_persist.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "xmpp/jid.hpp" +#include + +namespace utils +{ +/** + * Check config if a given requester is allowed to persist target + * + * The following values are checked + * + * - A user's jid (foo@example.com) + * - A user's domain (example.com) + * + * The following config options are checked in order + * + * 1. `persist_user_denylist` contains the requester's jid + * 2. `persist_user_allowlist` contains the requester's jid + * 3. `persist_user_denylist` contains the requester's domain + * 4. `persist_user_allowlist` contains the contains the requester's domain + * 5. allow + */ +inline bool is_requester_allowed_to_persist(const Jid &requester) { + // requester.bare checks + // deny + // 1. + bool jid_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.bare()); + if (jid_in_user_denylist) + return false; + + // allow + // 2. + bool jid_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.bare()); + if (jid_in_user_allowlist) + return true; + + // requester.domain checks + // deny + // 3. + bool domain_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.domain); + if (domain_in_user_denylist) + return false; + + // allow + // 4. + bool domain_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.domain); + if (domain_in_user_allowlist) + return true; + + // default allow + // 5. + return true; +} +} diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index aea316d..4a267e9 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -159,6 +160,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman } } + if (utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid()))) { XmlSubNode persistent(x, "field"); persistent["var"] = "persistent"; @@ -208,7 +210,8 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, bridge->set_record_history(options.col()); } else if (field->get_tag("var") == "persistent" && - value) + value + && utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid()))) options.col() = to_bool(value->get_inner()); } @@ -640,6 +643,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, } } + if (utils::is_requester_allowed_to_persist(requester)) { XmlSubNode persistent(x, "field"); persistent["var"] = "persistent"; @@ -699,7 +703,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const else if (field->get_tag("var") == "encoding_in" && value) options.col() = value->get_inner(); - else if (field->get_tag("var") == "persistent" && value) + else if (field->get_tag("var") == "persistent" && value && utils::is_requester_allowed_to_persist(requester)) options.col() = to_bool(value->get_inner()); else if (field->get_tag("var") == "record_history" && value && !value->get_inner().empty()) @@ -953,3 +957,4 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, message = ss.str(); } + diff --git a/tests/is_requester_allowed_to_persist.cpp b/tests/is_requester_allowed_to_persist.cpp new file mode 100644 index 0000000..69cf5f4 --- /dev/null +++ b/tests/is_requester_allowed_to_persist.cpp @@ -0,0 +1,89 @@ +#include "catch2/catch_test_macros.hpp" +#include "utils/is_requester_allowed_to_persist.hpp" +#include "xmpp/jid.hpp" + + +TEST_CASE("default") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist - jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("denylist - domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("allowlist - jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_allowlist", "foo@example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("allowlist - domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_allowlist", "example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny domain, allow jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "example.com"); + Config::set("persist_user_allowlist", "foo@example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny jid, allow domain") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", "example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("denylist + allowlist - deny jid, allow jid") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", "foo@example.com"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + From 51930c1e783b8276a02f5b3f9046f4f869767fff Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Fri, 28 Nov 2025 20:50:55 +0100 Subject: [PATCH 47/65] add default_persistence_allowed In addition set all config options in every test, as catch2 can run tests out of order and as demonstrated in e1ad60af4fd267ac1df57748afc910b2ea26a8b4 this can be an issue --- CHANGELOG.rst | 5 +-- doc/admin.rst | 6 ++++ src/utils/is_requester_allowed_to_persist.hpp | 4 +-- tests/is_requester_allowed_to_persist.cpp | 36 +++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 981e000..c6101fc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,8 +19,9 @@ For admins ---------- - Command line option --test-config (or -t) has been added. When used, biboumi will just exit without any error if the configuration is correct -- Options persist_user_denylist and persist_user_allowlist have been added to - granualy control who can set the persist option +- Options default_persistence_allowed, persist_user_denylist, and + persist_user_allowlist have been added to granualy control who can set the + persist option For packagers ------------- diff --git a/doc/admin.rst b/doc/admin.rst index a04a59a..effe6ac 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -260,6 +260,12 @@ persist_user_allowlist A list of XMPP XMPP users or domains who are allowed to change the `persist` ad-hoc option. +default_persistence_allowed +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a given XMPP user or domain is not in either denylist or allowlist this +option decides if the user is allowed to persist or not. + TLS configuration ----------------- diff --git a/src/utils/is_requester_allowed_to_persist.hpp b/src/utils/is_requester_allowed_to_persist.hpp index 5dc5956..b81f3d3 100644 --- a/src/utils/is_requester_allowed_to_persist.hpp +++ b/src/utils/is_requester_allowed_to_persist.hpp @@ -19,7 +19,7 @@ namespace utils * 2. `persist_user_allowlist` contains the requester's jid * 3. `persist_user_denylist` contains the requester's domain * 4. `persist_user_allowlist` contains the contains the requester's domain - * 5. allow + * 5. `default_persistence_allowed` is true (default true) */ inline bool is_requester_allowed_to_persist(const Jid &requester) { // requester.bare checks @@ -50,6 +50,6 @@ inline bool is_requester_allowed_to_persist(const Jid &requester) { // default allow // 5. - return true; + return Config::get_bool("default_persistence_allowed", true); } } diff --git a/tests/is_requester_allowed_to_persist.cpp b/tests/is_requester_allowed_to_persist.cpp index 69cf5f4..372e4b3 100644 --- a/tests/is_requester_allowed_to_persist.cpp +++ b/tests/is_requester_allowed_to_persist.cpp @@ -9,6 +9,31 @@ TEST_CASE("default") Config::clear(); Config::set("persist_user_denylist", ""); Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == true); + Config::clear(); +} + +TEST_CASE("default - default_persistence_allowed - false") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", "false"); + bool result = utils::is_requester_allowed_to_persist(jid); + CHECK(result == false); + Config::clear(); +} + +TEST_CASE("default - default_persistence_allowed - true") +{ + Jid jid("foo@example.com"); + Config::clear(); + Config::set("persist_user_denylist", ""); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", "true"); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == true); Config::clear(); @@ -19,6 +44,8 @@ TEST_CASE("denylist - jid") Jid jid("foo@example.com"); Config::clear(); Config::set("persist_user_denylist", "foo@example.com"); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == false); Config::clear(); @@ -29,6 +56,8 @@ TEST_CASE("denylist - domain") Jid jid("foo@example.com"); Config::clear(); Config::set("persist_user_denylist", "example.com"); + Config::set("persist_user_allowlist", ""); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == false); Config::clear(); @@ -39,6 +68,8 @@ TEST_CASE("allowlist - jid") Jid jid("foo@example.com"); Config::clear(); Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("persist_user_denylist", ""); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == true); Config::clear(); @@ -49,6 +80,8 @@ TEST_CASE("allowlist - domain") Jid jid("foo@example.com"); Config::clear(); Config::set("persist_user_allowlist", "example.com"); + Config::set("persist_user_denylist", ""); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == true); Config::clear(); @@ -60,6 +93,7 @@ TEST_CASE("denylist + allowlist - deny domain, allow jid") Config::clear(); Config::set("persist_user_denylist", "example.com"); Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == true); Config::clear(); @@ -71,6 +105,7 @@ TEST_CASE("denylist + allowlist - deny jid, allow domain") Config::clear(); Config::set("persist_user_denylist", "foo@example.com"); Config::set("persist_user_allowlist", "example.com"); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == false); Config::clear(); @@ -82,6 +117,7 @@ TEST_CASE("denylist + allowlist - deny jid, allow jid") Config::clear(); Config::set("persist_user_denylist", "foo@example.com"); Config::set("persist_user_allowlist", "foo@example.com"); + Config::set("default_persistence_allowed", ""); bool result = utils::is_requester_allowed_to_persist(jid); CHECK(result == false); Config::clear(); From b96eaaddba997d7541dd90fa813e5d068ec03a6b Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 8 Dec 2025 20:38:29 +0100 Subject: [PATCH 48/65] update some version strings --- CMakeLists.txt | 8 ++++---- doc/conf.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be47ffe..7454fe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,12 +7,12 @@ 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.") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.2) + message(FATAL_ERROR "GCC version must be at least 11.2.") 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.") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14) + message(FATAL_ERROR "Clang version must be at least 14.") endif() endif() diff --git a/doc/conf.py b/doc/conf.py index 504c15b..339d304 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -24,9 +24,9 @@ copyright = '2018, Florent Le Coz' author = 'Florent Le Coz' # The short X.Y version -version = '8.4' +version = '10.0' # The full version, including alpha/beta/rc tags -release = '8.4' +release = '10.0' # -- General configuration --------------------------------------------------- From d235c784bd461eb71397cbcb481edcb541727b10 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Fri, 2 Jan 2026 20:24:09 +0100 Subject: [PATCH 49/65] build docker image from local files, not git clone --- .dockerignore | 40 ++++++++++++++++++++++++++++++++ docker/biboumi/alpine/Dockerfile | 17 ++++++++------ 2 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..83cd31d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Gcov files +*.gcno +*.gcov +*.gcda + +# Python files +*.pyc +*.pyo + +# Build directories +build/ + +# Clion directory +.idea/ \ No newline at end of file diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile index d887327..28f843a 100644 --- a/docker/biboumi/alpine/Dockerfile +++ b/docker/biboumi/alpine/Dockerfile @@ -10,19 +10,22 @@ FROM docker.io/alpine:latest as builder RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \ make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan3-dev +COPY . /source -RUN git clone https://codeberg.org/poezio/biboumi && \ - mkdir ./biboumi/build && \ - cd ./biboumi/build && \ +WORKDIR /source + +RUN mkdir build && cd build && \ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_BUILD_TYPE=Release \ -DWITH_BOTAN=1 \ -DWITHOUT_SYSTEMD=1 \ -DWITH_SQLITE3=1 \ -DWITH_LIBIDN=1 \ - -DWITH_POSTGRESQL=1 && \ - make -j8 && \ - make install + -DWITH_POSTGRESQL=1 \ + -DWITH_UDNS=1 \ + -DWITH_LIBIDN=1 && \ + cmake --build . && \ + cmake --install . # --- @@ -34,7 +37,7 @@ RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \ COPY --from=builder /etc/biboumi /etc/biboumi COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi -COPY --from=builder /biboumi/docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg +COPY --from=builder /source/docker/biboumi/alpine/biboumi.cfg /etc/biboumi/biboumi.cfg RUN adduser biboumi -D -h /home/biboumi && \ mkdir /var/lib/biboumi && \ From 0d714c2c0dd6679050df23e56eb5c660dfe07bbb Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Thu, 25 Dec 2025 00:38:52 +0100 Subject: [PATCH 50/65] Rename Ports to Unencrypted ports This makes it clear that these ports are specified to connect without TLS, and hopefully lead to people using TLS ports instead by default. --- doc/user.rst | 3 ++- src/xmpp/biboumi_adhoc_commands.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/user.rst b/doc/user.rst index 065d9f0..ae89de0 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -444,7 +444,8 @@ server. The provided configuration form contains these fields: 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. +- **Unencrypted 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 communication will be insecure. The default list contains 6697 and 6670. diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 4a267e9..f1e612a 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -266,7 +266,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com XmlSubNode ports(x, "field"); ports["var"] = "ports"; ports["type"] = "text-multi"; - ports["label"] = "Ports"; + ports["label"] = "Unencrypted ports"; set_desc(ports, "List of ports to try, without TLS. Defaults: 6667."); for (const auto& val: utils::split(options.col(), ';', false)) { From ee7ecebbdc6ef58d68dd8624381efa9cad5985a7 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sun, 21 Dec 2025 21:42:53 +0100 Subject: [PATCH 51/65] add ctest support and remove absolute path from etc directory Some packagers use ctest by default to run tests. This is a small easy-win to add to biboumi. In addition some distros don't use /etc as their configuration path, and have to patch this out (see guix and nix), so this is also an easy fix. --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7454fe4..0d05db4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,6 +293,9 @@ endforeach() find_package(Catch2) if(Catch2_FOUND) target_link_libraries(test_suite Catch2::Catch2WithMain) + include(CTest) + include(Catch) + catch_discover_tests(test_suite) endif() # @@ -339,7 +342,7 @@ install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION share/man/man1 OPTIONAL COMPONENT documentation) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) -install(FILES ${policy_files} DESTINATION /etc/biboumi COMPONENT configuration) +install(FILES ${policy_files} DESTINATION etc/biboumi COMPONENT configuration) # ## Dist target From fdcc9f9b848ac662304b30ddbc533ab8c3b1c887 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Fri, 19 Dec 2025 17:52:05 +0100 Subject: [PATCH 52/65] fix fedora package --- CMakeLists.txt | 19 +++++++++++++------ docker/test/fedora/Dockerfile | 7 +++---- packaging/biboumi.spec.cmake | 11 ++++++----- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d05db4..c2f4188 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -350,9 +350,7 @@ install(FILES ${policy_files} DESTINATION etc/bibo # add_custom_command(OUTPUT ${ARCHIVE_NAME}.tar.xz COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar HEAD^{tree} - > ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar - # Append this specific file that is not part of the git repo - COMMAND tar -rf ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar -P ${SOURCE_DIR}/single_include/catch.hpp --xform 's|/.*/|${ARCHIVE_NAME}/tests/|g' + --output=${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar # Remove a potential existing archive COMMAND rm -f ${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_NAME}.tar.xz # Compress the archive @@ -360,12 +358,21 @@ add_custom_command(OUTPUT ${ARCHIVE_NAME}.tar.xz COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${ARCHIVE_NAME}.tar.xz created." WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ) + add_custom_target(dist - DEPENDS ${ARCHIVE_NAME}.tar.xz - DEPENDS catch) + DEPENDS ${ARCHIVE_NAME}.tar.xz) + +# The forge only provides .tar.gz, so make this target to be used to simulate +# downloading a tarball +add_custom_command(OUTPUT ${RPM_VERSION}.tar.gz + COMMAND git archive --prefix=${ARCHIVE_NAME}/ --format=tar.gz HEAD^{tree} + --output=${CMAKE_CURRENT_BINARY_DIR}/${RPM_VERSION}.tar.gz + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --cyan "${RPM_VERSION}.tar.gz created." + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) add_custom_target(rpm - DEPENDS dist + DEPENDS ${RPM_VERSION}.tar.gz COMMAND mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} COMMAND rpmbuild --define "_topdir `pwd`/rpmbuild/" --define "_sourcedir `pwd`" -ba biboumi.spec ) diff --git a/docker/test/fedora/Dockerfile b/docker/test/fedora/Dockerfile index cdab0c4..a5b3dec 100644 --- a/docker/test/fedora/Dockerfile +++ b/docker/test/fedora/Dockerfile @@ -1,12 +1,11 @@ # This Dockerfile creates a docker image suitable to run biboumi’s build and # tests. For example, it can be used on with gitlab-ci. -FROM docker.io/fedora:latest +FROM docker.io/fedora:44 ARG ERGO_VERSION=2.16.0 ENV LC_ALL=C.UTF-8 -# TODO: upgrade to botan3 RUN dnf --refresh install -y \ git \ make \ @@ -17,7 +16,7 @@ udns-devel \ expat-devel \ libidn-devel \ sqlite-devel \ -botan2-devel \ +botan3-devel \ systemd-devel \ libuuid-devel \ libgcrypt-devel \ @@ -25,7 +24,7 @@ postgresql-devel \ lcov \ libasan \ libubsan \ -catch \ +catch-devel \ valgrind \ python3-pip \ python3-lxml \ diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index 1352505..7dc30d0 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -4,8 +4,8 @@ Release: 1%{?dist} Summary: Lightweight XMPP to IRC gateway License: zlib -URL: http://biboumi.louiz.org -Source0: http://git.louiz.org/biboumi/snapshot/biboumi-%{version}.tar.xz +URL: https://biboumi.codeberg.page/ +Source0: https://codeberg.org/poezio/biboumi/archive/%{version}.tar.gz BuildRequires: libidn-devel BuildRequires: expat-devel @@ -13,7 +13,8 @@ BuildRequires: libuuid-devel BuildRequires: systemd-devel BuildRequires: sqlite-devel BuildRequires: postgresql-devel -BuildRequires: botan2-devel +BuildRequires: botan3-devel +BuildRequires: catch-devel BuildRequires: cmake BuildRequires: systemd BuildRequires: python3-sphinx @@ -64,8 +65,8 @@ make check %{?_smp_mflags} %changelog --* ${RPM_DATE} Le Coz Florent - ${RPM_VERSION}-1 --- Build latest git revision +* ${RPM_DATE} Le Coz Florent - ${RPM_VERSION}-1 + Build latest git revision * Tue Sep 22 2020 Le Coz Florent - 9.0-1 Update to version 9.0 From 4de88b5160dd1a6d85c097d6020e1ecb12e39483 Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Thu, 4 Dec 2025 18:32:31 +0100 Subject: [PATCH 53/65] Fix documentation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t fix them all, but at least makes the build a bit more readable. --- doc/conf.py | 2 +- doc/synopsis.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 339d304..4cf9d53 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -59,7 +59,7 @@ master_doc = 'index' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/doc/synopsis.rst b/doc/synopsis.rst index 2b28c74..15455b1 100644 --- a/doc/synopsis.rst +++ b/doc/synopsis.rst @@ -4,15 +4,15 @@ Synopsis biboumi [-ht] [*config_filename*] Command-Line Options -======== +==================== -h, \\-\\-help -~~~~~~~~ +~~~~~~~~~~~~~~ Display a help message and exit. -t, \\-\\-test-config -~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ Do not run, just test the configuration file syntax. Exit with a 0 status if the configuration is valid, exits with a non-zero status From 6ed185a8a53c97c8c778588ee7e18aad13c860f2 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Wed, 3 Dec 2025 18:29:40 +0100 Subject: [PATCH 54/65] add archive_expires_after option --- CHANGELOG.rst | 2 + CMakeLists.txt | 6 +++ doc/admin.rst | 22 ++++++++++ src/database/database.cpp | 9 ++++ src/database/database.hpp | 1 + src/main.cpp | 3 ++ src/scheduler/scheduler.cpp | 68 +++++++++++++++++++++++++++++ src/scheduler/scheduler.hpp | 10 +++++ src/utils/parse_duration.cpp | 36 ++++++++++++++++ src/utils/parse_duration.hpp | 8 ++++ tests/parse_duration.cpp | 84 ++++++++++++++++++++++++++++++++++++ 11 files changed, 249 insertions(+) create mode 100644 src/scheduler/scheduler.cpp create mode 100644 src/scheduler/scheduler.hpp create mode 100644 src/utils/parse_duration.cpp create mode 100644 src/utils/parse_duration.hpp create mode 100644 tests/parse_duration.cpp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c6101fc..5b31bac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,8 @@ For admins - Options default_persistence_allowed, persist_user_denylist, and persist_user_allowlist have been added to granualy control who can set the persist option +- Option archive_expires_after has been added to delete older messages from the + database For packagers ------------- diff --git a/CMakeLists.txt b/CMakeLists.txt index c2f4188..96854c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,10 @@ file(GLOB source_bridge src/bridge/*.[hc]pp) add_library(bridge OBJECT ${source_bridge}) +file(GLOB source_scheduler + src/scheduler/*.[hc]pp) +add_library(scheduler OBJECT ${source_scheduler}) + file(GLOB source_config src/config/*.[hc]pp) add_library(config OBJECT ${source_config}) @@ -217,6 +221,7 @@ add_executable(${PROJECT_NAME} src/main.cpp $ $ $ + $ $ $) @@ -230,6 +235,7 @@ add_executable(test_suite ${source_tests} $ $ $ + $ $ $) set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE) diff --git a/doc/admin.rst b/doc/admin.rst index effe6ac..2a59f56 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -266,6 +266,28 @@ default_persistence_allowed If a given XMPP user or domain is not in either denylist or allowlist this option decides if the user is allowed to persist or not. +archive_expires_after +~~~~~~~~~~~~~~~~~~~~~ + +Messages in the archive can expire after some time, by default never. This +can be changed by this setting + +.. code-block:: ini + + # one day + archive_expires_after=1d + # one week + archive_expires_after=1w + # two months + archive_expires_after=2m + # one year + archive_expires_after=1y + # 1h + archive_expires_after=1h + # keep messages forever, the default + archive_expires_after=0 + + TLS configuration ----------------- diff --git a/src/database/database.cpp b/src/database/database.cpp index 861abcb..9f2657a 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -2,6 +2,7 @@ #ifdef USE_DATABASE #include +#include #include #include #include @@ -317,6 +318,14 @@ std::vector Database::get_full_roster() return query.execute(*Database::db); } +void Database::delete_old_muclines(const long& seconds) +{ + auto query = DeleteQuery(Database::muc_log_lines.get_name()); + query << " WHERE " << Database::Date{} << "<" << seconds; + + query.execute(*Database::db); +} + void Database::close() { Database::db = nullptr; diff --git a/src/database/database.hpp b/src/database/database.hpp index de1df49..12b3ef5 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -153,6 +153,7 @@ class Database static void delete_roster_item(const std::string& local, const std::string& remote); static std::vector get_contact_list(const std::string& local); static std::vector get_full_roster(); + static void delete_old_muclines(const long& seconds); static void close(); static void open(const std::string& filename); diff --git a/src/main.cpp b/src/main.cpp index 20bf9fd..9365499 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include +#include // A flag set by the SIGINT signal handler. static std::atomic stop(false); @@ -107,6 +108,7 @@ static int main_loop(std::string hostname, std::string password) if (Config::get_int("identd_port", 113) != 0) identd = std::make_unique(*xmpp_component, p, static_cast(Config::get_int("identd_port", 113))); + Scheduler::start_scheduler(); auto timeout = TimedEventsManager::instance().get_timeout(); while (p->poll(timeout) != -1) { @@ -130,6 +132,7 @@ static int main_loop(std::string hostname, std::string password) #endif if (identd) identd->shutdown(); + Scheduler::stop_scheduler(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } diff --git a/src/scheduler/scheduler.cpp b/src/scheduler/scheduler.cpp new file mode 100644 index 0000000..3050396 --- /dev/null +++ b/src/scheduler/scheduler.cpp @@ -0,0 +1,68 @@ +#include "biboumi.h" +#include +#include +#include +#include + +#ifdef USE_DATABASE +#include +#endif + +static const std::unordered_map< + std::string, std::pair>> + events = { +#ifdef USE_DATABASE + {"cleanup_old_messages", + {60s * 10 /* 10min */, &Scheduler::cleanup_old_messages}}, +#endif +}; + +void Scheduler::start_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + const std::chrono::seconds interval = job.second.first; + const std::function job_function = job.second.second; + // Run it once at startup + job_function(); + // Then every interval + TimedEventsManager::instance().add_event( + TimedEvent(interval, job_function, name)); + } +} + +void Scheduler::stop_scheduler() { + for (const auto &job : events) { + const std::string name = job.first; + TimedEventsManager::instance().cancel(name); + } +} + +#ifdef USE_DATABASE +void Scheduler::cleanup_old_messages() { + // Get the config value + const std::string archive_expires_after = + Config::get("archive_expires_after", "0"); + const std::optional seconds = + utils::parse_duration(archive_expires_after); + // If it's actually set and relevant, start cleanup + if (seconds.has_value() && seconds.value() != 0) { + // Calculate age of messages to be deleted + log_debug("[cleanup_old_messages] Deleting messages older than ", + archive_expires_after, " (", seconds.value(), " seconds)"); + + // Convert the config value to a duration + const auto duration = std::chrono::duration(seconds.value()); + // Get timestamp now relative to unix epoch + const auto now = std::chrono::system_clock::now(); + // Older than this get deleted + const auto ago = now - duration; + // Convert to seconds since epoch + const auto since_epoch = std::chrono::duration_cast( + ago.time_since_epoch()); + Database::delete_old_muclines(since_epoch.count()); + } else + log_debug("[cleanup_old_messages] Not deleting anything. " + "archive_expires_after is ", + archive_expires_after); +} +#endif diff --git a/src/scheduler/scheduler.hpp b/src/scheduler/scheduler.hpp new file mode 100644 index 0000000..b35aeaa --- /dev/null +++ b/src/scheduler/scheduler.hpp @@ -0,0 +1,10 @@ +#pragma once + +class Scheduler { +public: + static void start_scheduler(); + static void stop_scheduler(); + + // Jobs + static void cleanup_old_messages(); +}; diff --git a/src/utils/parse_duration.cpp b/src/utils/parse_duration.cpp new file mode 100644 index 0000000..371c4c4 --- /dev/null +++ b/src/utils/parse_duration.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include + +namespace utils { + +// Strongly inspired from prosody +// https://hg.prosody.im/trunk/file/0ff11f2e87cd/util/human/io.lua#l214 +static const long day = 86400; + +static const std::unordered_map multipliers = { + {"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425}, + {"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600}, +}; + +std::optional parse_duration(const std::string &duration) { + std::size_t ptr; + long number; + try { + number = std::stol(duration, &ptr, 10); + } catch (std::invalid_argument &) { + return {}; + } + const std::string m = duration.substr(ptr); + + if (!multipliers.contains(m)) + return {}; + + const long multiplier = multipliers.at(m); + + return multiplier * number; +} +} // namespace utils diff --git a/src/utils/parse_duration.hpp b/src/utils/parse_duration.hpp new file mode 100644 index 0000000..b0f9b21 --- /dev/null +++ b/src/utils/parse_duration.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace utils { +std::optional parse_duration(const std::string &duration); +} // namespace utils diff --git a/tests/parse_duration.cpp b/tests/parse_duration.cpp new file mode 100644 index 0000000..45e9348 --- /dev/null +++ b/tests/parse_duration.cpp @@ -0,0 +1,84 @@ +#include "catch2/catch_test_macros.hpp" + +#include +#include + +TEST_CASE("parse_duration - 2d") +{ + const long expected = (60 * 60 * 24) * 2; + const std::optional output = utils::parse_duration("2d"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5w") +{ + const long expected = ((60 * 60 * 24) * 7) * 5; + const std::optional output = utils::parse_duration("5w"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - asdf") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("asdf"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 1s") +{ + const long expected = 1; + const std::optional output = utils::parse_duration("1s"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 2mon") +{ + const long expected = ((60 * 60 * 24) * 31) * 2; + const std::optional output = utils::parse_duration("2mon"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y") +{ + const long expected = (long)((60 * 60 * 24) * 365.2425) * 5; + const std::optional output = utils::parse_duration("5y"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - ") +{ + const std::optional expected; + const std::optional output = utils::parse_duration(""); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5blorbs") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5blorbs"); + + CHECK(expected == output); +} + +TEST_CASE("parse_duration - 5y2d") +{ + const std::optional expected; + const std::optional output = utils::parse_duration("5y2d"); + + CHECK(expected == output); +} From c15a05788586458df98081414d5d372ede2fa6e4 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sat, 29 Nov 2025 14:04:35 +0100 Subject: [PATCH 55/65] fix multiline private messages --- src/bridge/bridge.cpp | 43 ++++++++++++++++--- .../end_to_end/scenarios/multiline_message.py | 12 +++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index b123f63..003288d 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -225,6 +225,7 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: return ; if (irc->has_capability("draft/multiline") + && lines.size() > 1 // /me and /mode cannot be multi lined && lines[0].substr(0, 5) != "/mode" && lines[0].substr(0, 4) != "/me ") @@ -241,13 +242,6 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: if (id.empty()) id = utils::gen_uuid(); - IrcChannel* channel = irc->get_channel(iid.get_local()); - if (!channel->joined) - { - log_warning("Cannot send message to channel ", iid.get_local(), ", it is not joined"); - return; - } - std::vector messages; for (const std::string& line: lines) { @@ -467,6 +461,41 @@ void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::vector lines = utils::split(body, '\n', true); if (lines.empty()) return ; + + if (irc->has_capability("draft/multiline") + && lines.size() > 1 + // /me and /mode cannot be multi lined + && lines[0].substr(0, 5) != "/mode" + && lines[0].substr(0, 4) != "/me ") + { + std::basic_string uuid = utils::gen_uuid(); + std::vector messages; + for (const std::string &line : lines) { + constexpr auto max_username_size = 10; + constexpr auto max_hostname_size = 63; + const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - + ::strlen("draft/multiline-concat") - 1 - + irc->get_own_nick().size() - max_username_size - + max_hostname_size - ::strlen(":!@ PRIVMSG ") - + iid.get_local().length() - ::strlen(" :\r\n"); + const auto privmsgs = cut(line, line_size); + bool first = true; + for (const auto &privmsg : privmsgs) { + if (first) { + messages.push_back(IrcMessage({}, std::string(), "PRIVMSG", + {iid.get_local(), privmsg})); + first = false; + } else + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, + std::string(), "PRIVMSG", + {iid.get_local(), privmsg})); + } + } + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, + [](const IrcClient *, const IrcMessage &) {}); + return; + } + for (const std::string& line: lines) { if (line.substr(0, 4) == "/me ") diff --git a/tests/end_to_end/scenarios/multiline_message.py b/tests/end_to_end/scenarios/multiline_message.py index e26bc90..1e8ab1a 100644 --- a/tests/end_to_end/scenarios/multiline_message.py +++ b/tests/end_to_end/scenarios/multiline_message.py @@ -52,5 +52,15 @@ scenario = ( ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], ["/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a\nb\nc']"], - ) + ), + + # Send a multi-line private message, to a in-room JID + send_stanza("a\nb\nc"), + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@type='chat']/body[text()='a\nb\nc']"), + + # Send a multi-line private message, to a server-wide JID + send_stanza("a\nb\nc"), + # Message is received with a server-wide JID + expect_stanza("/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}'][@type='chat']/body[text()='a\nb\nc']"), ) From b2540951782ab4a38db22d8833c6a9b3f2f00b7b Mon Sep 17 00:00:00 2001 From: poVoq Date: Sat, 10 Jan 2026 19:53:49 +0100 Subject: [PATCH 56/65] Fix link to documentation --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 895e277..c9a3b84 100644 --- a/README.rst +++ b/README.rst @@ -52,5 +52,5 @@ Biboumi is released under the zlib license. Please read the COPYING file for details. .. _INSTALL: doc/install.rst -.. _the documentation: https://doc.biboumi.louiz.org +.. _the documentation: https://biboumi.codeberg.page/ .. _contributing: CONTRIBUTING.rst From 3fc18e1e1af8b7f456b9d6e7c379c0ef117f8ed7 Mon Sep 17 00:00:00 2001 From: poVoq Date: Sat, 10 Jan 2026 19:59:00 +0100 Subject: [PATCH 57/65] change to direct /doc/ --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c9a3b84..8eae0fd 100644 --- a/README.rst +++ b/README.rst @@ -52,5 +52,5 @@ Biboumi is released under the zlib license. Please read the COPYING file for details. .. _INSTALL: doc/install.rst -.. _the documentation: https://biboumi.codeberg.page/ +.. _the documentation: https://biboumi.codeberg.page/doc/ .. _contributing: CONTRIBUTING.rst From b250237e015f7ffb78d576f8192224afac052135 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sat, 10 Jan 2026 23:04:36 +0100 Subject: [PATCH 58/65] install files using GNUInstallDirs --- CMakeLists.txt | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 96854c0..687a442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -344,11 +344,23 @@ add_custom_target(everything DEPENDS test_suite biboumi) # ## Install target # -install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION share/man/man1 OPTIONAL COMPONENT documentation) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION lib/systemd/system COMPONENT init) +include(GNUInstallDirs) + +# From cmake version 4.1 CMAKE_INSTALL_SYSCONFDIR has special behaviour when +# CMAKE_INSTALL_PREFIX is /, /usr, or /opt/..., so to not update cmake version +# just yet reimplement this here +# https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html#special-cases +if (CMAKE_INSTALL_SYSCONFDIR STREQUAL "etc" AND (CMAKE_INSTALL_PREFIX STREQUAL "/" OR CMAKE_INSTALL_PREFIX STREQUAL "/usr")) + set(CMAKE_INSTALL_SYSCONFDIR "/etc") +elseif (CMAKE_INSTALL_SYSCONFDIR STREQUAL "etc" AND (CMAKE_INSTALL_PREFIX MATCHES "^/opt/")) + set(CMAKE_INSTALL_SYSCONFDIR "/etc${CMAKE_INSTALL_PREFIX}") +endif() + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 OPTIONAL COMPONENT documentation) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION ${CMAKE_INSTALL_LIBDIR}/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) -install(FILES ${policy_files} DESTINATION etc/biboumi COMPONENT configuration) +install(FILES ${policy_files} DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/biboumi COMPONENT configuration) # ## Dist target From bf958d0e34e7d8a39dccfcf3c64f72ef0db41f73 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sun, 7 Dec 2025 15:01:03 +0100 Subject: [PATCH 59/65] add CAP LS 302 support and max-lines support for draft/multiline messages --- src/bridge/bridge.cpp | 53 +++++++++++++++++++-------- src/irc/irc_client.cpp | 51 +++++++++++++++++++++++--- src/irc/irc_client.hpp | 5 ++- src/irc/parse_caps.cpp | 56 +++++++++++++++++++++++++++++ src/irc/parse_caps.hpp | 29 +++++++++++++++ src/utils/chunk.hpp | 24 +++++++++++++ tests/chunk.cpp | 49 +++++++++++++++++++++++++ tests/end_to_end/__main__.py | 2 ++ tests/parse_caps.cpp | 70 ++++++++++++++++++++++++++++++++++++ 9 files changed, 318 insertions(+), 21 deletions(-) create mode 100644 src/irc/parse_caps.cpp create mode 100644 src/irc/parse_caps.hpp create mode 100644 src/utils/chunk.hpp create mode 100644 tests/chunk.cpp create mode 100644 tests/parse_caps.cpp diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 003288d..e0d8793 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include "result_set_management.hpp" #include #include @@ -242,9 +244,27 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: if (id.empty()) id = utils::gen_uuid(); - std::vector messages; - for (const std::string& line: lines) - { + // A message with a lot of lines may need to be split, even if the + // server supports draft/multiline, as we may not be allowed to send + // that many lines all at once. As such check max-lines + // https://ircv3.net/specs/extensions/multiline#capabilities + std::vector> lines_chunked = {lines}; + auto cap_value = irc->get_capability_value("draft/multiline"); + if (cap_value) { + auto cap_value_parsed = parse_cap_keyval(cap_value.value()); + if (cap_value_parsed.contains("max-lines") && + cap_value_parsed.at("max-lines").has_value()) { + std::stringstream max_lines_string( + cap_value_parsed.at("max-lines").value()); + size_t size; + max_lines_string >> size; + if (size > 0) + lines_chunked = utils::chunk(lines, size); + } + } + for (const auto &lines : lines_chunked) { + std::vector messages; + for (const std::string &line : lines) { constexpr auto max_username_size = 10; constexpr auto max_hostname_size = 63; const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - ::strlen("draft/multiline-concat") - 1 - @@ -264,18 +284,21 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: } } - MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { - for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) - { - auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), - this->user_jid + "/" - + resource, uuid, id); - for (const auto& node: nodes_to_reflect) - stanza.add_child(node); - this->xmpp.send_stanza(stanza); - } - }; - irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); + MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), + this->user_jid + "/" + + resource, uuid, id); + for (const auto& node: nodes_to_reflect) + stanza.add_child(node); + this->xmpp.send_stanza(stanza); + } + }; + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); + id = utils::gen_uuid(); + uuid = utils::gen_uuid(); + } return; } diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 5e0503e..a3d1973 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -322,10 +323,16 @@ void IrcClient::on_connected() } #endif - { - for (const auto &pair : this->capabilities) - this->send_message({ "CAP", {"REQ", pair.first}}); - } + this->send_message({ "CAP", {"LS", "302"}}); + + // If we never get a response from CAP LS start requesting things blindly + TimedEventsManager::instance().add_event(TimedEvent( + std::chrono::steady_clock::now() + 10s, + [this]() { + for (const auto &pair : this->capabilities) + this->send_message({"CAP", {"REQ", pair.first}}); + }, + "cap_fallback_" + this->get_unique_identifier())); this->send_nick_command(this->current_nick); #ifdef USE_DATABASE @@ -1392,6 +1399,30 @@ long int IrcClient::get_throttle_limit() const void IrcClient::on_cap(const IrcMessage &message) { const auto& sub_command = message.arguments[1]; + if (sub_command == "LS") { + /** + * CAP LS 302 can return either a multiline response or a one line response + * - CAP * LS * :multiline + * - CAP * LS :last line + */ + const bool last_cap_ls = message.arguments[2] != "*"; + const auto caps = + last_cap_ls ? message.arguments[2] : message.arguments[3]; + auto caps_parsed = parse_caps(caps); + this->cap_ls_response.merge(caps_parsed); + if (last_cap_ls) { + // The CAP LS response has finished. Cancel the fallback + TimedEventsManager::instance().cancel("cap_fallback_" + this->get_unique_identifier()); + std::erase_if(this->capabilities, [this](auto &pair) { + if (this->cap_ls_response.contains(pair.first)) { + this->send_message({"CAP", {"REQ", pair.first}}); + return false; + } else + return true; + }); + } + return; + } const auto& caps = utils::split(message.arguments[2], ' ', false); for (const auto& cap: caps) { @@ -1477,12 +1508,18 @@ void IrcClient::cap_end() this->bridge.on_irc_client_connected(this->get_hostname()); } -bool IrcClient::has_capability(const std::string& capability) +bool IrcClient::has_capability(const std::string& capability) const { auto it = this->enabled_capabilities.find(capability); return it != this->enabled_capabilities.end(); } + +const std::optional IrcClient::get_capability_value(const std::string& capability) const +{ + return this->cap_ls_response.at(capability); +} + void IrcClient::on_batch(const IrcMessage& message) { if (message.arguments[0][0] == '+') @@ -1558,3 +1595,7 @@ void IrcClient::process_batch(const std::string& reference_tag) } return; } + +const std::string IrcClient::get_unique_identifier() const { + return this->bridge.get_jid() + this->hostname; +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 4090ab1..76d6b05 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -250,6 +250,7 @@ public: private: void cap_end(); void process_batch(const std::string& reference_tag); + const std::string get_unique_identifier() const; public: #ifdef WITH_SASL void on_authenticate(const IrcMessage& message); @@ -334,7 +335,8 @@ public: */ HistoryLimit history_limit; - bool has_capability(const std::string& capability); + bool has_capability(const std::string& capability) const; + const std::optional get_capability_value(const std::string& capability) const; private: /** * The hostname of the server we are connected to. @@ -395,6 +397,7 @@ private: */ SaslState sasl_state{SaslState::unneeded}; #endif + std::unordered_map> cap_ls_response; std::map capabilities; std::unordered_set enabled_capabilities; /** diff --git a/src/irc/parse_caps.cpp b/src/irc/parse_caps.cpp new file mode 100644 index 0000000..7e46e13 --- /dev/null +++ b/src/irc/parse_caps.cpp @@ -0,0 +1,56 @@ +#include "irc/parse_caps.hpp" + +const std::unordered_map> +parse_caps(const std::string &caps) { + std::unordered_map> output; + // caps => multi-prefix sasl=PLAIN,EXTERNAL + // draft/multiline=max-bytes=40000,max-lines=10 + const std::vector &caps_list = utils::split(caps, ' ', false); + for (const std::string &cap : caps_list) { + // cap => multi-prefix + // cap => sasl=PLAIN,EXTERNAL + // cap => draft/multiline=max-bytes=40000,max-lines=10 + const size_t delimiter = cap.find("="); + // delimiter(multi-prefix) => npos + // delimiter(sasl=PLAIN,EXTERNAL) => 5 + // delimiter(draft/multiline=max-bytes=40000,max-lines=10) => 16 + const std::string cap_name = cap.substr(0, delimiter); + std::optional cap_value; + if (delimiter != std::string::npos) + cap_value = cap.substr(delimiter + 1); + output.insert_or_assign(cap_name, cap_value); + } + + return output; +} + +const std::vector parse_cap_val_list(const std::string &value) { + // value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL + return utils::split(value, ',', false); +} + +const std::unordered_map> +parse_cap_keyval(const std::string &value) { + std::unordered_map> output; + // value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL + // value(max-bytes=40000,max-lines=10) => + // max-bytes=40000,max-lines=10 + const std::vector &values_list = utils::split(value, ',', false); + // values_list(PLAIN,EXTERNAL) => { "PLAIN", "EXTERNAL" } + // values_list(draft/multiline=max-bytes=40000,max-lines=10) => { + // "max-bytes=40000", "max-lines=10" } + for (const std::string &value : values_list) { + const size_t value_delimiter = value.find("="); + // key(PLAIN) => PLAIN + // key(max-bytes=40000) => max-bytes + const std::string key = value.substr(0, value_delimiter); + std::optional val; + if (value_delimiter != std::string::npos) { + // val(max-bytes=40000) => 40000 + val = value.substr(value_delimiter + 1); + } + output.insert_or_assign(key, val); + } + + return output; +} diff --git a/src/irc/parse_caps.hpp b/src/irc/parse_caps.hpp new file mode 100644 index 0000000..2899e53 --- /dev/null +++ b/src/irc/parse_caps.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "utils/split.hpp" +#include +#include +#include +#include +#include + +/** + * Parse a list of IRC caps into a map of capabilities + */ +const std::unordered_map> +parse_caps(const std::string &caps); + +/** + * Parse the value of a cap into a list of values + * + * Used for the sasl cap + */ +const std::vector parse_cap_val_list(const std::string &value); + +/** + * Parse the value of a cap into a key-value map + * + * Used for the multiline cap + */ +const std::unordered_map> +parse_cap_keyval(const std::string &value); diff --git a/src/utils/chunk.hpp b/src/utils/chunk.hpp new file mode 100644 index 0000000..f6eb3ef --- /dev/null +++ b/src/utils/chunk.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace utils { +template +inline std::vector> chunk(const std::vector &vector, + const std::size_t size) { + std::vector> output; + std::vector current_chunk; + for (const auto &el : vector) { + current_chunk.push_back(el); + if (current_chunk.size() >= size) { + output.push_back(current_chunk); + current_chunk = {}; + } + } + if (current_chunk.size() != 0) + output.push_back(current_chunk); + + return output; +} + +} // namespace utils diff --git a/tests/chunk.cpp b/tests/chunk.cpp new file mode 100644 index 0000000..f14c352 --- /dev/null +++ b/tests/chunk.cpp @@ -0,0 +1,49 @@ +#include "utils/chunk.hpp" +#include "catch2/catch_test_macros.hpp" +#include + +TEST_CASE("chunk 5") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 5); + + CHECK( output.size() == 2 ); + CHECK( output.at(0).size() == 5 ); + CHECK( output.at(1).size() == 5 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(0).at(1) == 2 ); + + CHECK( output.at(1).at(0) == 6 ); + CHECK( output.at(1).at(1) == 7 ); +} + +TEST_CASE("chunk 7") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 7); + + CHECK( output.size() == 2 ); + CHECK( output.at(0).size() == 7 ); + CHECK( output.at(1).size() == 3 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(0).at(1) == 2 ); + + CHECK( output.at(1).at(0) == 8 ); + CHECK( output.at(1).at(1) == 9 ); +} + +TEST_CASE("chunk 1") { + const std::vector input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const auto output = utils::chunk(input, 1); + + CHECK( output.size() == 10 ); + CHECK( output.at(0).size() == 1 ); + CHECK( output.at(1).size() == 1 ); + + CHECK( output.at(0).at(0) == 1 ); + CHECK( output.at(1).at(0) == 2 ); + CHECK( output.at(2).at(0) == 3 ); + CHECK( output.at(3).at(0) == 4 ); + CHECK( output.at(8).at(0) == 9 ); + CHECK( output.at(9).at(0) == 10 ); +} diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index e1c73bb..8063be5 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -341,6 +341,8 @@ async def main(): print("Waiting for irc server to exit…") irc.stop() await irc.wait() + res = await irc.process.stderr.readline() + irc_output.write(res.decode()) if failures: print("%d test%s failed, please fix %s." % (failures, 's' if failures > 1 else '', diff --git a/tests/parse_caps.cpp b/tests/parse_caps.cpp new file mode 100644 index 0000000..6beb0c3 --- /dev/null +++ b/tests/parse_caps.cpp @@ -0,0 +1,70 @@ +#include "catch2/catch_test_macros.hpp" +#include "irc/parse_caps.hpp" +#include + +TEST_CASE("simple caps") +{ + const auto output = parse_caps("multi-prefix extended-join account-notify"); + CHECK( output.contains("multi-prefix") ); + CHECK( output.contains("extended-join") ); + CHECK( output.contains("account-notify") ); +} + +TEST_CASE("key-value") +{ + const auto output = parse_caps("sasl=PLAIN example.com/a=b"); + CHECK( output.contains("sasl") ); + CHECK( output.contains("example.com/a") ); + + CHECK( output.at("sasl") == "PLAIN" ); + CHECK( output.at("example.com/a") == "b" ); +} + +TEST_CASE("nested key-value") +{ + const auto output = parse_caps("draft/multiline=max-bytes=40000,max-lines=10 example.com/a=foo=bar,b=c"); + CHECK( output.contains("draft/multiline") ); + CHECK( output.contains("example.com/a") ); + + const auto multiline = output.at("draft/multiline"); + const auto a = output.at("example.com/a"); + CHECK( multiline == "max-bytes=40000,max-lines=10" ); + CHECK ( a == "foo=bar,b=c" ); + + const auto multiline_val = parse_cap_keyval(multiline.value()); + const auto a_val = parse_cap_keyval(a.value()); + CHECK( multiline_val.at("max-bytes").has_value() ); + CHECK( multiline_val.at("max-lines").has_value() ); + CHECK( a_val.at("foo").has_value() ); + CHECK( a_val.at("b").has_value() ); + + CHECK( multiline_val.at("max-bytes").value() == "40000" ); + CHECK( multiline_val.at("max-lines").value() == "10" ); + CHECK( a_val.at("foo").value() == "bar" ); + CHECK( a_val.at("b").value() == "c" ); +} + +TEST_CASE("all") +{ + const auto output = parse_caps("message-tags sasl=PLAIN,EXTERNAL draft/multiline=max-bytes=40000,max-lines=10"); + CHECK( output.contains("message-tags") ); + CHECK( output.contains("sasl") ); + CHECK( output.contains("draft/multiline") ); + + const auto multiline = output.at("draft/multiline"); + const auto sasl = output.at("sasl"); + const auto message_tags = output.at("message-tags"); + CHECK( !message_tags.has_value() ); + CHECK( sasl == "PLAIN,EXTERNAL" ); + CHECK( multiline == "max-bytes=40000,max-lines=10" ); + + const auto multiline_val = parse_cap_keyval(multiline.value()); + const auto sasl_val = parse_cap_val_list(sasl.value()); + CHECK( std::find(sasl_val.begin(), sasl_val.end(), "PLAIN") != sasl_val.end() ); + CHECK( std::find(sasl_val.begin(), sasl_val.end(), "EXTERNAL") != sasl_val.end() ); + CHECK( multiline_val.at("max-bytes").has_value() ); + CHECK( multiline_val.at("max-lines").has_value() ); + + CHECK( multiline_val.at("max-bytes").value() == "40000" ); + CHECK( multiline_val.at("max-lines").value() == "10" ); +} From 3d321c7d92bd1c42007b50929ccc9b492b921dec Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 10 Feb 2026 08:53:34 +0100 Subject: [PATCH 60/65] cmake: Explicitly search for Catch2 version 3 The current CMakeLists block find_package(Catch2) if(Catch2_FOUND) target_link_libraries(test_suite Catch2::Catch2WithMain) include(CTest) include(Catch) catch_discover_tests(test_suite) endif() would search for any Catch2 version, but later tries to link against Catch2 version 3 (Catch2WithMain). This means the block would fail on systems that have Catch2 version 2 installed, but not Catch2 version 3. Fix that by requesting Catch2 version 3 when invoking find_package(). Link: https://bugs.gentoo.org/969522 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 687a442..d5bdade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,7 +296,7 @@ endforeach() # ## Catch unit test framework # -find_package(Catch2) +find_package(Catch2 3) if(Catch2_FOUND) target_link_libraries(test_suite Catch2::Catch2WithMain) include(CTest) From 889c0aaf29fe29ea8bfd5fa3d012cb499a97b03b Mon Sep 17 00:00:00 2001 From: bkmgit Date: Thu, 12 Feb 2026 09:54:50 +0100 Subject: [PATCH 61/65] Systemd service dir is /usr/lib/systemd/system https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d5bdade..2971f2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -358,7 +358,7 @@ endif() install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 OPTIONAL COMPONENT documentation) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION ${CMAKE_INSTALL_LIBDIR}/systemd/system COMPONENT init) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION /usr/lib/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) install(FILES ${policy_files} DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/biboumi COMPONENT configuration) From 3a92e8742ff30230a82071cc15f510f706251863 Mon Sep 17 00:00:00 2001 From: Vladislav Grigoryev Date: Tue, 3 Feb 2026 12:12:01 +0300 Subject: [PATCH 62/65] Fix RPM builds on Fedora Linux * Add missing build dependency * Fix bogus date in %changelog * Fix path to the systemd unit * Use SPDX license identifier Signed-off-by: Vladislav Grigoryev --- CMakeLists.txt | 2 +- packaging/biboumi.spec.cmake | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2971f2a..c996f73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -358,7 +358,7 @@ endif() install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/biboumi.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 OPTIONAL COMPONENT documentation) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION /usr/lib/systemd/system COMPONENT init) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/biboumi.service DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system COMPONENT init) file(GLOB policy_files conf/*policy.txt) install(FILES ${policy_files} DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/biboumi COMPONENT configuration) diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake index 7dc30d0..748904a 100644 --- a/packaging/biboumi.spec.cmake +++ b/packaging/biboumi.spec.cmake @@ -3,7 +3,7 @@ Version: ${RPM_VERSION} Release: 1%{?dist} Summary: Lightweight XMPP to IRC gateway -License: zlib +License: Zlib URL: https://biboumi.codeberg.page/ Source0: https://codeberg.org/poezio/biboumi/archive/%{version}.tar.gz @@ -16,6 +16,7 @@ BuildRequires: postgresql-devel BuildRequires: botan3-devel BuildRequires: catch-devel BuildRequires: cmake +BuildRequires: g++ BuildRequires: systemd BuildRequires: python3-sphinx @@ -77,7 +78,7 @@ make check %{?_smp_mflags} * Tue Feb 25 2020 Le Coz Florent - 8.4-1 Update to version 8.4 -* Wed Jun 1 2018 Le Coz Florent - 8.3-1 +* Fri Jun 1 2018 Le Coz Florent - 8.3-1 Update to version 8.3 * Fri May 25 2018 Le Coz Florent - 8.2-1 From b51b93d6d43d40620bc4f16d913fe1d1df0290fe Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Fri, 17 Jul 2026 19:41:16 +0200 Subject: [PATCH 63/65] doc(admin): format verify_certificate documentation into a list --- doc/admin.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/admin.rst b/doc/admin.rst index 2a59f56..fff918d 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -310,8 +310,9 @@ To see the list of possible options to configure, refer to `Botan’s TLS documentation `_. In addition to these Botan options, biboumi implements a few custom options listed hereafter: + - verify_certificate: if this value is set to false, biboumi will not check -the certificate validity at all. The default value is true. + the certificate validity at all. The default value is true. By default, biboumi provides a few policy files, to work around some issues found with a few well-known IRC servers. From ad14748a30a6bbd5667597cd2a295fa3af5c5842 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Mon, 20 Jul 2026 15:12:53 +0200 Subject: [PATCH 64/65] database: various optimizations for SQLite and PostgreSQL --- CHANGELOG.rst | 3 ++ doc/admin.rst | 10 +++++++ src/config/config.cpp | 1 + src/config/config.hpp | 2 -- src/database/database_type.hpp | 9 ++++++ src/database/engine.hpp | 3 +- src/database/postgresql_engine.cpp | 3 ++ src/database/postgresql_engine.hpp | 4 ++- src/database/row.hpp | 1 + src/database/sqlite3_engine.cpp | 48 ++++++++++++++++++++++++++++-- src/database/sqlite3_engine.hpp | 21 +++++++++++++ src/database/table.hpp | 1 - src/scheduler/scheduler.cpp | 45 ++++++++++++++++++++++++++-- src/scheduler/scheduler.hpp | 1 + src/utils/timed_events.hpp | 2 +- 15 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 src/database/database_type.hpp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b31bac..cb1e22e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,9 @@ For admins persist option - Option archive_expires_after has been added to delete older messages from the database +- SQLite enables WAL by default now. This can be changed with the + sqlite_journal_mode option. +- Biboumi will regularely perform maintanance on the database. For packagers ------------- diff --git a/doc/admin.rst b/doc/admin.rst index fff918d..2ae86ac 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -287,6 +287,16 @@ can be changed by this setting # keep messages forever, the default archive_expires_after=0 +sqlite_journal_mode +~~~~~~~~~~~~~~~~~~~ + +Set the `journal_mode https://sqlite.org/pragma.html#pragma_journal_mode`_ to +be used when connecting to an SQLite database. This option can be one of +`DELETE`, `TRUNCATE`, `PERSIST`, `MEMORY`, `WAL`, `OFF`, or `none` to not set +the journal_mode at all. + +By default this option is set to `WAL` to use `Write-Ahead Logging +https://sqlite.org/wal.html`_. TLS configuration ----------------- diff --git a/src/config/config.cpp b/src/config/config.cpp index 2f64b9e..6a20357 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/config/config.hpp b/src/config/config.hpp index 9c28e8c..625a749 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -16,8 +16,6 @@ #pragma once #include -#include -#include #include #include #include diff --git a/src/database/database_type.hpp b/src/database/database_type.hpp new file mode 100644 index 0000000..0fb48db --- /dev/null +++ b/src/database/database_type.hpp @@ -0,0 +1,9 @@ +#pragma once + +enum class DatabaseType +{ + none, + sqlite3, + postgresql, +}; + diff --git a/src/database/engine.hpp b/src/database/engine.hpp index 41dccf5..369b4cc 100644 --- a/src/database/engine.hpp +++ b/src/database/engine.hpp @@ -7,11 +7,11 @@ * Everything else (all portable stuf) should go outside of this class. */ +#include #include #include #include -#include #include #include @@ -36,6 +36,7 @@ class DatabaseEngine return {}; } virtual std::string id_column_type() = 0; + virtual DatabaseType get_type() { return DatabaseType::none; }; int64_t last_inserted_rowid{-1}; }; diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp index 59bc885..d3773e4 100644 --- a/src/database/postgresql_engine.cpp +++ b/src/database/postgresql_engine.cpp @@ -101,4 +101,7 @@ std::string PostgresqlEngine::id_column_type() return "SERIAL"; } +DatabaseType PostgresqlEngine::get_type() { + return DatabaseType::postgresql; +}; #endif diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp index 1a9c249..4153922 100644 --- a/src/database/postgresql_engine.hpp +++ b/src/database/postgresql_engine.hpp @@ -2,9 +2,9 @@ #include #include -#include #include +#include #include #include @@ -30,6 +30,8 @@ class PostgresqlEngine: public DatabaseEngine void extract_last_insert_rowid(Statement& statement) override; std::string get_returning_id_sql_string(const std::string& col_name) override; std::string id_column_type() override; + DatabaseType get_type() override final; + private: PGconn* const conn; }; diff --git a/src/database/row.hpp b/src/database/row.hpp index 4004b5d..f5c13b7 100644 --- a/src/database/row.hpp +++ b/src/database/row.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include template diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp index 5e3bba1..7b4f541 100644 --- a/src/database/sqlite3_engine.cpp +++ b/src/database/sqlite3_engine.cpp @@ -7,9 +7,14 @@ #include +#include #include #include -#include +#include + +#include +#include + Sqlite3Engine::Sqlite3Engine(sqlite3* db): db(db) @@ -46,6 +51,8 @@ std::set Sqlite3Engine::get_all_columns_from_table(const std::strin std::unique_ptr Sqlite3Engine::open(const std::string& filename) { sqlite3* new_db; + const std::string dirname = utils::dirname(filename); + std::filesystem::create_directories(dirname); auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); if (res != SQLITE_OK) { @@ -53,7 +60,31 @@ std::unique_ptr Sqlite3Engine::open(const std::string& filename) sqlite3_close(new_db); throw std::runtime_error(""); } - return std::make_unique(new_db); + std::unique_ptr db = std::make_unique(new_db); + + // Enable Write-Ahead Logging + // https://sqlite.org/wal.html + auto sqlite_journal_mode = Config::get("sqlite_journal_mode", "WAL"); + if (sqlite_journal_mode != "none") { + if (Sqlite3Engine::is_valid_sqlite_journal_mode(sqlite_journal_mode)) { + auto [success, err_msg] = + db->raw_exec("PRAGMA journal_mode = " + sqlite_journal_mode); + if (!success) + log_error("Failed enabling SQLite WAL with error: ", err_msg); + } else { + log_error("Invalid config option sqlite_journal_mode: ", + sqlite_journal_mode); + } + } + for (const auto &s : Sqlite3Engine::startup_commands) { + auto [success, err_msg] = db->raw_exec(s); + if (!success) { + log_error("Failed running startup command: ", s, + "; with error: ", err_msg); + } + } + + return db; } std::tuple Sqlite3Engine::raw_exec(const std::string& query) @@ -97,4 +128,17 @@ std::string Sqlite3Engine::id_column_type() return "INTEGER PRIMARY KEY AUTOINCREMENT"; } +DatabaseType Sqlite3Engine::get_type() { + return DatabaseType::sqlite3; +}; + + +bool Sqlite3Engine::is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode) { + // https://sqlite.org/pragma.html#pragma_journal_mode + for (const std::string &s : Sqlite3Engine::valid_sqlite_journal_modes) { + if (sqlite_journal_mode == s) + return true; + } + return false; +} #endif diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp index a7bfcdb..9e29bed 100644 --- a/src/database/sqlite3_engine.hpp +++ b/src/database/sqlite3_engine.hpp @@ -29,8 +29,29 @@ class Sqlite3Engine: public DatabaseEngine std::unique_ptr prepare(const std::string& query) override; void extract_last_insert_rowid(Statement& statement) override; std::string id_column_type() override; + DatabaseType get_type() override final; + private: sqlite3* const db; + constexpr static const char *const startup_commands[3] = { + // Wait for a maximum of 5 seconds if a write is busy + "PRAGMA busy_timeout = 5000", + // Reduce disk syncs + // >When synchronous is NORMAL (1), the SQLite database engine will still + // >sync at the most critical moments + // https://sqlite.org/pragma.html#pragma_synchronous + "PRAGMA synchronous = NORMAL", + // Run PRAGMA command early to analyze all tables, as recommended by + // sqlite + // >Applications that use long-lived database connections should run + // >"PRAGMA optimize=0x10002;" when the connection is first opened + // https://sqlite.org/lang_analyze.html + "PRAGMA optimize=0x10002", + }; + constexpr static std::string valid_sqlite_journal_modes[6] = { + "DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF", + }; + static bool is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode); }; #else diff --git a/src/database/table.hpp b/src/database/table.hpp index 0b8bfc0..2f63784 100644 --- a/src/database/table.hpp +++ b/src/database/table.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include diff --git a/src/scheduler/scheduler.cpp b/src/scheduler/scheduler.cpp index 3050396..aee2503 100644 --- a/src/scheduler/scheduler.cpp +++ b/src/scheduler/scheduler.cpp @@ -1,10 +1,12 @@ #include "biboumi.h" #include +#include #include #include #include #ifdef USE_DATABASE +#include #include #endif @@ -13,8 +15,10 @@ static const std::unordered_map< events = { #ifdef USE_DATABASE {"cleanup_old_messages", - {60s * 10 /* 10min */, &Scheduler::cleanup_old_messages}}, -#endif + {std::chrono::seconds(10min), &Scheduler::cleanup_old_messages}}, + {"optimize_db", + {std::chrono::seconds(24h), &Scheduler::optimize_db}}, +#endif // USE_DATABASE }; void Scheduler::start_scheduler() { @@ -65,4 +69,39 @@ void Scheduler::cleanup_old_messages() { "archive_expires_after is ", archive_expires_after); } -#endif + +void Scheduler::optimize_db() { + log_debug("[optimize_db] Starting. Type: ", (int)Database::db->get_type()); + switch (Database::db->get_type()) { +# ifdef SQLITE3_FOUND + case DatabaseType::sqlite3: { + log_debug("[optimize_db] Optimizing Sqlite database"); + // Run PRAGMA optimize periodically, as recommended by sqlite + // https://sqlite.org/lang_analyze.html#automatically_running_analyze + auto [success, err_msg] = Database::db->raw_exec("PRAGMA optimize"); + if (!success) { + log_error("[optimize_db] Failed optimizing Sqlite database: ", err_msg); + } + break; + } +# endif // SQLITE3_FOUND +# ifdef PQ_FOUND + case DatabaseType::postgresql: { + log_debug("[optimize_db] Optimizing PostgreSQL database"); + // Run VACUUM and ANALYZE periodically + // https://www.postgresql.org/docs/current/sql-vacuum.html + // https://www.postgresql.org/docs/current/sql-analyze.html + auto [success, err_msg] = Database::db->raw_exec("VACUUM ANALYZE"); + if (!success) { + log_error("[optimize_db] Failed optimizing PostgreSQL database: ", + err_msg); + } + break; + } +# endif // PQ_FOUND + case DatabaseType::none: + default: + break; + } +} +#endif // USE_DATABASE diff --git a/src/scheduler/scheduler.hpp b/src/scheduler/scheduler.hpp index b35aeaa..63e56cc 100644 --- a/src/scheduler/scheduler.hpp +++ b/src/scheduler/scheduler.hpp @@ -7,4 +7,5 @@ public: // Jobs static void cleanup_old_messages(); + static void optimize_db(); }; diff --git a/src/utils/timed_events.hpp b/src/utils/timed_events.hpp index fa0fc50..0ab3246 100644 --- a/src/utils/timed_events.hpp +++ b/src/utils/timed_events.hpp @@ -22,7 +22,7 @@ class TimedEvent friend class TimedEventsManager; public: /** - * An event the occurs only once, at the given time_point + * An event that occurs only once, at the given time_point */ explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point, std::function callback, std::string name=""); From a261848ef4ae8d59433efb151814faa9db6d2ab5 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 2 Aug 2026 19:37:44 +0200 Subject: [PATCH 65/65] build: fix wrong constextpr preventing some compilers from building biboumi The latest commit introduced an issue because a std::string cannot really be constexpr. Fortunately, a cosnt char* const works just as well here. --- src/database/sqlite3_engine.cpp | 2 +- src/database/sqlite3_engine.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp index 7b4f541..5128065 100644 --- a/src/database/sqlite3_engine.cpp +++ b/src/database/sqlite3_engine.cpp @@ -135,7 +135,7 @@ DatabaseType Sqlite3Engine::get_type() { bool Sqlite3Engine::is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode) { // https://sqlite.org/pragma.html#pragma_journal_mode - for (const std::string &s : Sqlite3Engine::valid_sqlite_journal_modes) { + for (const char* s : Sqlite3Engine::valid_sqlite_journal_modes) { if (sqlite_journal_mode == s) return true; } diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp index 9e29bed..13ed4d0 100644 --- a/src/database/sqlite3_engine.hpp +++ b/src/database/sqlite3_engine.hpp @@ -48,7 +48,7 @@ private: // https://sqlite.org/lang_analyze.html "PRAGMA optimize=0x10002", }; - constexpr static std::string valid_sqlite_journal_modes[6] = { + constexpr static const char *const valid_sqlite_journal_modes[6] = { "DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF", }; static bool is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode);