Add support for IRCv3 message-tags Implement draft/multiline
This commit is contained in:
committed by
mathieui
parent
cf6dec8ce5
commit
d13f66ab90
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 << "}";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user