* https://codeberg.org/poezio/biboumi: (65 commits)
  build: fix wrong constextpr preventing some compilers from building biboumi
  database: various optimizations for SQLite and PostgreSQL
  doc(admin): format verify_certificate documentation into a list
  Fix RPM builds on Fedora Linux
  Systemd service dir is  /usr/lib/systemd/system
  cmake: Explicitly search for Catch2 version 3
  add CAP LS 302 support and max-lines support for draft/multiline messages
  install files using GNUInstallDirs
  change to direct /doc/
  Fix link to documentation
  fix multiline private messages
  add archive_expires_after option
  Fix documentation warnings
  fix fedora package
  add ctest support and remove absolute path from etc directory
  Rename Ports to Unencrypted ports
  build docker image from local files, not git clone
  update some version strings
  add default_persistence_allowed
  add persist_user_denylist and persist_user_allowlist
  ...
This commit is contained in:
Stephen Paul Weber
2026-09-23 09:59:13 -05:00
109 changed files with 1935 additions and 471 deletions
+125 -7
View File
@@ -2,18 +2,22 @@
#include <utility>
#include <xmpp/biboumi_component.hpp>
#include <network/poller.hpp>
#include <utils/chunk.hpp>
#include <utils/empty_if_fixed_server.hpp>
#include <utils/encoding.hpp>
#include <utils/tolower.hpp>
#include <utils/uuid.hpp>
#include <logger/logger.hpp>
#include <utils/is_requester_allowed_to_persist.hpp>
#include <utils/revstr.hpp>
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
#include <database/database.hpp>
#include <irc/parse_caps.hpp>
#include "result_set_management.hpp"
#include <algorithm>
#include <cstring>
#include <utils/string.hpp>
using namespace std::string_literals;
@@ -64,7 +68,7 @@ void Bridge::shutdown(const std::string& exit_message)
{
for (auto& pair: this->irc_clients)
{
std::unique_ptr<IrcClient>& irc = pair.second;
std::shared_ptr<IrcClient>& irc = pair.second;
irc->send_quit_command(exit_message);
}
}
@@ -135,11 +139,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<IrcClient>(this->poller, hostname,
std::make_shared<IrcClient>(this->poller, hostname,
nickname, username,
realname, jid.domain,
*this));
std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname);
std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
return irc.get();
}
}
@@ -221,6 +225,83 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::
std::vector<std::string> 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::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
if (id.empty())
id = utils::gen_uuid();
// 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<std::vector<std::string>> 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<IrcMessage> 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}));
}
}
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;
}
bool first = true;
for (const std::string& line: lines)
{
@@ -403,6 +484,41 @@ void Bridge::send_private_message(const Iid& iid, const std::string& body, const
std::vector<std::string> 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<char> uuid = utils::gen_uuid();
std::vector<IrcMessage> 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 ")
@@ -436,7 +552,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<Database::GlobalPersistent>())
if (!utils::is_requester_allowed_to_persist(Jid(this->user_jid)))
persistent = false;
else if (goptions.col<Database::GlobalPersistent>())
persistent = true;
else
{
@@ -1151,12 +1269,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe
}
}
std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients()
std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
{
return this->irc_clients;
}
const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const
const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const
{
return this->irc_clients;
}
@@ -1165,7 +1283,7 @@ std::set<char> Bridge::get_chantypes(const std::string& hostname) const
{
IrcClient* irc = this->find_irc_client(hostname);
if (!irc)
return {'#', '&'};
return {'&', '#', '+', '!'};
return irc->get_chantypes();
}