add CAP LS 302 support and max-lines support for draft/multiline messages
This commit is contained in:
committed by
mathieui
parent
b250237e01
commit
bf958d0e34
+38
-15
@@ -2,6 +2,7 @@
|
||||
#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>
|
||||
@@ -12,6 +13,7 @@
|
||||
#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>
|
||||
@@ -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<IrcMessage> 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<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 -
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+46
-5
@@ -11,6 +11,7 @@
|
||||
#include <config/config.hpp>
|
||||
#include <utils/tolower.hpp>
|
||||
#include <utils/split.hpp>
|
||||
#include <irc/parse_caps.hpp>
|
||||
#include <utils/string.hpp>
|
||||
|
||||
#include <sstream>
|
||||
@@ -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<const std::string> 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;
|
||||
}
|
||||
|
||||
@@ -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<const std::string> 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<std::string, std::optional<std::string>> cap_ls_response;
|
||||
std::map<std::string, Capability> capabilities;
|
||||
std::unordered_set<std::string> enabled_capabilities;
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "irc/parse_caps.hpp"
|
||||
|
||||
const std::unordered_map<std::string, std::optional<std::string>>
|
||||
parse_caps(const std::string &caps) {
|
||||
std::unordered_map<std::string, std::optional<std::string>> output;
|
||||
// caps => multi-prefix sasl=PLAIN,EXTERNAL
|
||||
// draft/multiline=max-bytes=40000,max-lines=10
|
||||
const std::vector<std::string> &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<std::string> 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<std::string> parse_cap_val_list(const std::string &value) {
|
||||
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
|
||||
return utils::split(value, ',', false);
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::optional<std::string>>
|
||||
parse_cap_keyval(const std::string &value) {
|
||||
std::unordered_map<std::string, std::optional<std::string>> output;
|
||||
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
|
||||
// value(max-bytes=40000,max-lines=10) =>
|
||||
// max-bytes=40000,max-lines=10
|
||||
const std::vector<std::string> &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<std::string> 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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/split.hpp"
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Parse a list of IRC caps into a map of capabilities
|
||||
*/
|
||||
const std::unordered_map<std::string, std::optional<std::string>>
|
||||
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<std::string> 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<std::string, std::optional<std::string>>
|
||||
parse_cap_keyval(const std::string &value);
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <config/config.hpp>
|
||||
|
||||
namespace utils {
|
||||
template <typename T>
|
||||
inline std::vector<std::vector<T>> chunk(const std::vector<T> &vector,
|
||||
const std::size_t size) {
|
||||
std::vector<std::vector<T>> output;
|
||||
std::vector<T> 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
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "utils/chunk.hpp"
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("chunk 5") {
|
||||
const std::vector<int> 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<int> 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<int> 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 );
|
||||
}
|
||||
@@ -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 '',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "irc/parse_caps.hpp"
|
||||
#include <iterator>
|
||||
|
||||
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" );
|
||||
}
|
||||
Reference in New Issue
Block a user