Add support for IRCv3 message-tags Implement draft/multiline

This commit is contained in:
Benjamin Graillot
2025-11-28 20:30:12 +01:00
committed by mathieui
parent cf6dec8ce5
commit d13f66ab90
5 changed files with 262 additions and 9 deletions
+59
View File
@@ -14,6 +14,7 @@
#include "result_set_management.hpp"
#include <algorithm>
#include <cstring>
#include <utils/string.hpp>
using namespace std::string_literals;
@@ -221,6 +222,64 @@ 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"))
{
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<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::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)
{
+147 -8
View File
@@ -83,6 +83,7 @@ static const std::unordered_map<std::string,
{"KICK", {&IrcClient::on_kick, {3, 0}}},
{"INVITE", {&IrcClient::on_invite, {2, 0}}},
{"CAP", {&IrcClient::on_cap, {3, 0}}},
{"BATCH", {&IrcClient::on_batch, {1, 0}}},
#ifdef WITH_SASL
{"AUTHENTICATE", {&IrcClient::on_authenticate, {1, 0}}},
{"900", {&IrcClient::on_sasl_login, {3, 0}}},
@@ -287,7 +288,10 @@ void IrcClient::on_connected()
this->send_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<IrcMessage, MessageCallback>&& 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<std::string>& parameters, std::vector<IrcMessage>& messages, MessageCallback callback)
{
std::vector<std::string> 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<IrcMessage>()));
}
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;
}
+11
View File
@@ -26,6 +26,7 @@
#include <map>
#include <set>
#include <utils/tokens_bucket.hpp>
#include <unordered_set>
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<std::string>& parameters, std::vector<IrcMessage>& 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<std::string, Capability> capabilities;
std::unordered_set<std::string> 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::string, std::pair<std::pair<std::string, std::vector<std::string>>, std::vector<IrcMessage>>> batches;
};
+41 -1
View File
@@ -1,8 +1,27 @@
#include <irc/irc_message.hpp>
#include <iostream>
#include <string>
#include <utils/split.hpp>
#include <optional>
IrcMessage::IrcMessage(std::stringstream ss)
{
if (ss.peek() == '@')
{
std::string raw_tags;
ss.ignore();
ss >> raw_tags;
std::vector<std::string> 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<std::string>&& 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<std::string>&& args):
tags(),
prefix(),
command(std::move(command)),
arguments(std::move(args))
{
}
IrcMessage::IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags,
std::string&& prefix,
std::string&& command,
std::vector<std::string>&& 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 << "}";
+4
View File
@@ -5,6 +5,8 @@
#include <string>
#include <ostream>
#include <sstream>
#include <unordered_map>
#include <optional>
class IrcMessage
{
@@ -13,6 +15,7 @@ public:
IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {}
IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags, std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
~IrcMessage() = default;
IrcMessage(const IrcMessage&) = delete;
@@ -20,6 +23,7 @@ public:
IrcMessage& operator=(const IrcMessage&) = delete;
IrcMessage& operator=(IrcMessage&&) = default;
std::unordered_map<std::string, std::optional<std::string>> tags;
std::string prefix;
std::string command;
std::vector<std::string> arguments;