Commit 76a8189b authored by louiz’'s avatar louiz’

Implement result-set-management for LIST queries

ref #2948
parent ffad4306
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
#define FORWARD_NS "urn:xmpp:forward:0" #define FORWARD_NS "urn:xmpp:forward:0"
#define CLIENT_NS "jabber:client" #define CLIENT_NS "jabber:client"
#define DATAFORM_NS "jabber:x:data" #define DATAFORM_NS "jabber:x:data"
#define RSM_NS "http://jabber.org/protocol/rsm"
/** /**
* An XMPP component, communicating with an XMPP server using the protocole * An XMPP component, communicating with an XMPP server using the protocole
...@@ -219,6 +220,9 @@ public: ...@@ -219,6 +220,9 @@ public:
virtual void after_handshake() {} virtual void after_handshake() {}
const std::string& get_served_hostname() const
{ return this->served_hostname; }
/** /**
* Whether or not we ever succeeded our authentication to the XMPP server * Whether or not we ever succeeded our authentication to the XMPP server
*/ */
......
#include <bridge/bridge.hpp> #include <bridge/bridge.hpp>
#include <bridge/list_element.hpp>
#include <xmpp/biboumi_component.hpp> #include <xmpp/biboumi_component.hpp>
#include <network/poller.hpp> #include <network/poller.hpp>
#include <utils/empty_if_fixed_server.hpp> #include <utils/empty_if_fixed_server.hpp>
...@@ -10,6 +9,7 @@ ...@@ -10,6 +9,7 @@
#include <utils/split.hpp> #include <utils/split.hpp>
#include <xmpp/jid.hpp> #include <xmpp/jid.hpp>
#include <database/database.hpp> #include <database/database.hpp>
#include "result_set_management.hpp"
using namespace std::string_literals; using namespace std::string_literals;
...@@ -386,45 +386,164 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick) ...@@ -386,45 +386,164 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick)
irc->send_nick_command(new_nick); irc->send_nick_command(new_nick);
} }
void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid,
const std::string& to_jid) ResultSetInfo rs_info)
{ {
IrcClient* irc = this->get_irc_client(iid.get_server()); auto& list = channel_list_cache[iid.get_server()];
irc->send_list_command(); // We fetch the list from the IRC server only if we have a complete
// cached list that needs to be invalidated (that is, when the request
// doesn’t have a after or before, or when the list is empty).
// If the list is not complete, this means that a request is already
// ongoing, so we just need to add the callback.
// By default the list is complete and empty.
if (list.complete &&
(list.channels.empty() || (rs_info.after.empty() && rs_info.before.empty())))
{
IrcClient* irc = this->get_irc_client(iid.get_server());
irc->send_list_command();
// Add a callback that will populate our list
list.channels.clear();
list.complete = false;
irc_responder_callback_t cb = [this, iid](const std::string& irc_hostname,
const IrcMessage& message) -> bool
{
if (irc_hostname != iid.get_server())
return false;
std::vector<ListElement> list; auto& list = channel_list_cache[iid.get_server()];
if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES"
|| message.command == "ERR_NOSUCHSERVER")
{
list.complete = true;
return true;
}
else if (message.command == "322" || message.command == "RPL_LIST")
{ // Add element to list
if (message.arguments.size() == 4)
{
list.channels.emplace_back(message.arguments[1] + utils::empty_if_fixed_server("%" + iid.get_server()),
message.arguments[2], message.arguments[3]);
}
return false;
}
else if (message.command == "323" || message.command == "RPL_LISTEND")
{ // Send the iq response with the content of the list
list.complete = true;
return true;
}
return false;
};
irc_responder_callback_t cb = [this, iid, iq_id, to_jid, list=std::move(list)](const std::string& irc_hostname, this->add_waiting_irc(std::move(cb));
const IrcMessage& message) mutable -> bool }
// If the list is complete, we immediately send the answer.
// Otherwise, we install a callback, that will populate our list and send
// the answer when we can.
if (list.complete)
{ {
if (irc_hostname != iid.get_server()) this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
}
else
{
// Add a callback to answer the request as soon as we can
irc_responder_callback_t cb = [this, iid, iq_id, to_jid,
rs_info=std::move(rs_info)](const std::string& irc_hostname,
const IrcMessage& message) -> bool
{
if (irc_hostname != iid.get_server())
return false;
if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES"
|| message.command == "ERR_NOSUCHSERVER")
{
std::string text;
if (message.arguments.size() >= 2)
text = message.arguments[1];
this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "wait", "service-unavailable", text, false);
return true;
}
else if (message.command == "322" || message.command == "RPL_LIST")
{
auto& list = channel_list_cache[iid.get_server()];
const auto res = this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
log_debug("We added a new channel in our list, can we send the result? ", std::boolalpha, res);
return res;
}
else if (message.command == "323" || message.command == "RPL_LISTEND")
{ // Send the iq response with the content of the list
auto& list = channel_list_cache[iid.get_server()];
this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
return true;
}
return false; return false;
if (message.command == "263" || message.command == "RPL_TRYAGAIN" || };
message.command == "ERR_TOOMANYMATCHES" || message.command == "ERR_NOSUCHSERVER")
this->add_waiting_irc(std::move(cb));
}
}
bool Bridge::send_matching_channel_list(const ChannelList& channel_list, const ResultSetInfo& rs_info,
const std::string& id, const std::string& to_jid, const std::string& from)
{
auto begin = channel_list.channels.begin();
auto end = channel_list.channels.begin();
if (channel_list.complete)
{
begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
{
return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname();
});
if (begin == channel_list.channels.end())
begin = channel_list.channels.begin();
else
begin = std::next(begin);
end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
{
return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname();
});
if (rs_info.max >= 0)
{ {
std::string text; if (std::distance(begin, end) >= rs_info.max)
if (message.arguments.size() >= 2) end = begin + rs_info.max;
text = message.arguments[1];
this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id,
"wait", "service-unavailable", text, false);
return true;
} }
else if (message.command == "322" || message.command == "RPL_LIST") }
{ // Add element to list else
if (message.arguments.size() == 4) {
list.emplace_back(message.arguments[1], message.arguments[2], if (rs_info.after.empty() && rs_info.before.empty() && rs_info.max < 0)
message.arguments[3]); return false;
return false; if (!rs_info.after.empty())
{
begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
{
return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname();
});
if (begin == channel_list.channels.end())
return false;
begin = std::next(begin);
} }
else if (message.command == "323" || message.command == "RPL_LISTEND") if (!rs_info.before.empty())
{ // Send the iq response with the content of the list {
this->xmpp.send_iq_room_list_result(iq_id, to_jid, std::to_string(iid), list); end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
return true; {
return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname();
});
if (end == channel_list.channels.end())
return false;
} }
return false; if (rs_info.max >= 0)
}; {
this->add_waiting_irc(std::move(cb)); if (std::distance(begin, end) < rs_info.max)
return false;
else
end = begin + rs_info.max;
}
}
this->xmpp.send_iq_room_list_result(id, to_jid, from, channel_list, begin, end, rs_info);
return true;
} }
void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason, void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason,
...@@ -1002,4 +1121,4 @@ void Bridge::set_record_history(const bool val) ...@@ -1002,4 +1121,4 @@ void Bridge::set_record_history(const bool val)
{ {
this->record_history = val; this->record_history = val;
} }
#endif #endif
\ No newline at end of file
#pragma once #pragma once
#include <bridge/result_set_management.hpp>
#include <bridge/list_element.hpp>
#include <irc/irc_message.hpp> #include <irc/irc_message.hpp>
#include <irc/irc_client.hpp> #include <irc/irc_client.hpp>
...@@ -17,6 +19,7 @@ ...@@ -17,6 +19,7 @@
class BiboumiComponent; class BiboumiComponent;
class Poller; class Poller;
class ResultSetInfo;
/** /**
* A callback called for each IrcMessage we receive. If the message triggers * A callback called for each IrcMessage we receive. If the message triggers
...@@ -87,8 +90,19 @@ public: ...@@ -87,8 +90,19 @@ public:
void send_irc_version_request(const std::string& irc_hostname, const std::string& target, void send_irc_version_request(const std::string& irc_hostname, const std::string& target,
const std::string& iq_id, const std::string& to_jid, const std::string& iq_id, const std::string& to_jid,
const std::string& from_jid); const std::string& from_jid);
void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid,
const std::string& to_jid); ResultSetInfo rs_info);
/**
* Check if the channel list contains what is needed to answer the RSM request,
* if it does, send the iq result. If the list is complete but does not contain
* everything, send the result anyway (because there are no more available
* channels that could complete the list).
*
* Returns true if we sent the answer.
*/
bool send_matching_channel_list(const ChannelList& channel_list,
const ResultSetInfo& rs_info, const std::string& id, const std::string& to_jid,
const std::string& from);
void forward_affiliation_role_change(const Iid& iid, const std::string& nick, void forward_affiliation_role_change(const Iid& iid, const std::string& nick,
const std::string& affiliation, const std::string& role); const std::string& affiliation, const std::string& role);
/** /**
...@@ -271,7 +285,6 @@ private: ...@@ -271,7 +285,6 @@ private:
* response iq. * response iq.
*/ */
std::vector<irc_responder_callback_t> waiting_irc; std::vector<irc_responder_callback_t> waiting_irc;
/** /**
* Resources to IRC channel/server mapping: * Resources to IRC channel/server mapping:
*/ */
...@@ -300,6 +313,13 @@ private: ...@@ -300,6 +313,13 @@ private:
* TODO: send message history * TODO: send message history
*/ */
void generate_channel_join_for_resource(const Iid& iid, const std::string& resource); void generate_channel_join_for_resource(const Iid& iid, const std::string& resource);
/**
* A cache of the channels list (as returned by the server on a LIST
* request), to be re-used on a subsequent XMPP list request that
* uses result-set-management.
*/
std::map<IrcHostname, ChannelList> channel_list_cache;
#ifdef USE_DATABASE #ifdef USE_DATABASE
bool record_history { true }; bool record_history { true };
#endif #endif
......
#pragma once #pragma once
#include <vector>
#include <string> #include <string>
struct ListElement struct ListElement
...@@ -17,3 +17,8 @@ struct ListElement ...@@ -17,3 +17,8 @@ struct ListElement
}; };
struct ChannelList
{
bool complete{true};
std::vector<ListElement> channels{};
};
#pragma once
#include <string>
struct ResultSetInfo
{
int max{-1};
std::string before{};
std::string after{};
};
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
#include <stdexcept> #include <stdexcept>
#include <iostream> #include <iostream>
#include <stdio.h> #include <cstdlib>
#include <louloulibs.h> #include <louloulibs.h>
#include <biboumi.h> #include <biboumi.h>
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#endif #endif
#include <database/database.hpp> #include <database/database.hpp>
#include <bridge/result_set_management.hpp>
using namespace std::string_literals; using namespace std::string_literals;
...@@ -463,7 +464,22 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) ...@@ -463,7 +464,22 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
} }
else if (node.empty() && iid.type == Iid::Type::Server) else if (node.empty() && iid.type == Iid::Type::Server)
{ // Disco on an IRC server: get the list of channels { // Disco on an IRC server: get the list of channels
bridge->send_irc_channel_list_request(iid, id, from); ResultSetInfo rs_info;
const XmlNode* set_node = query->get_child("set", RSM_NS);
if (set_node)
{
const XmlNode* after = set_node->get_child("after", RSM_NS);
if (after)
rs_info.after = after->get_inner();
const XmlNode* before = set_node->get_child("before", RSM_NS);
if (before)
rs_info.before = before->get_inner();
const XmlNode* max = set_node->get_child("max", RSM_NS);
if (max)
rs_info.max = std::atoi(max->get_inner().data());
}
bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
stanza_error.disable(); stanza_error.disable();
} }
} }
...@@ -749,10 +765,11 @@ void BiboumiComponent::send_ping_request(const std::string& from, ...@@ -749,10 +765,11 @@ void BiboumiComponent::send_ping_request(const std::string& from,
this->waiting_iq[id] = result_cb; this->waiting_iq[id] = result_cb;
} }
void BiboumiComponent::send_iq_room_list_result(const std::string& id, void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid,
const std::string& to_jid, const std::string& from, const ChannelList& channel_list,
const std::string& from, std::vector<ListElement>::const_iterator begin,
const std::vector<ListElement>& rooms_list) std::vector<ListElement>::const_iterator end,
const ResultSetInfo& rs_info)
{ {
Stanza iq("iq"); Stanza iq("iq");
iq["from"] = from + "@" + this->served_hostname; iq["from"] = from + "@" + this->served_hostname;
...@@ -761,12 +778,41 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, ...@@ -761,12 +778,41 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id,
iq["type"] = "result"; iq["type"] = "result";
XmlNode query("query"); XmlNode query("query");
query["xmlns"] = DISCO_ITEMS_NS; query["xmlns"] = DISCO_ITEMS_NS;
for (const auto& room: rooms_list)
for (auto it = begin; it != end; ++it)
{ {
XmlNode item("item"); XmlNode item("item");
item["jid"] = room.channel + "%" + from + "@" + this->served_hostname; item["jid"] = it->channel + "@" + this->served_hostname;
query.add_child(std::move(item)); query.add_child(std::move(item));
} }
if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
{
XmlNode set_node("set");
set_node["xmlns"] = RSM_NS;
if (begin != channel_list.channels.cend())
{
XmlNode first_node("first");
first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin));
first_node.set_inner(begin->channel + "@" + this->served_hostname);
set_node.add_child(std::move(first_node));
}
if (end != channel_list.channels.cbegin())
{
XmlNode last_node("last");
last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname);
set_node.add_child(std::move(last_node));
}
if (channel_list.complete)
{
XmlNode count_node("count");
count_node.set_inner(std::to_string(channel_list.channels.size()));
set_node.add_child(std::move(count_node));
}
query.add_child(std::move(set_node));
}
iq.add_child(std::move(query)); iq.add_child(std::move(query));
this->send_stanza(iq); this->send_stanza(iq);
} }
......
...@@ -79,9 +79,9 @@ public: ...@@ -79,9 +79,9 @@ public:
/** /**
* Send the channels list in one big stanza * Send the channels list in one big stanza
*/ */
void send_iq_room_list_result(const std::string& id, const std::string& to_jid, void send_iq_room_list_result(const std::string& id, const std::string& to_jid, const std::string& from,
const std::string& from, const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin,
const std::vector<ListElement>& rooms_list); std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info);
void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick); void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick);
/** /**
* Handle the various stanza types * Handle the various stanza types
......
...@@ -117,7 +117,8 @@ def match(stanza, xpath): ...@@ -117,7 +117,8 @@ def match(stanza, xpath):
'mam': 'urn:xmpp:mam:1', 'mam': 'urn:xmpp:mam:1',
'delay': 'urn:xmpp:delay', 'delay': 'urn:xmpp:delay',
'forward': 'urn:xmpp:forward:0', 'forward': 'urn:xmpp:forward:0',
'client': 'jabber:client'}) 'client': 'jabber:client',
'rsm': 'http://jabber.org/protocol/rsm'})
return matched return matched
...@@ -1244,7 +1245,195 @@ if __name__ == '__main__': ...@@ -1244,7 +1245,195 @@ if __name__ == '__main__':
"/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']", "/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']" "/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']"
)) ))
]) ]),
Scenario("channel_list_with_rsm",
[
handshake_sequence(),
partial(log_message, "Join first channel #foo"),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']")
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
partial(log_message, "Join second channel #bar"),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#bar%{irc_server_one}/{nick_one}' />"),
partial(expect_stanza,
"/message/body[text()='Mode #bar [+nt] by {irc_host_one}']"),
partial(expect_stanza, "/presence"),
partial(expect_stanza, "/message[@from='#bar%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
partial(log_message, "Join third channel #coucou"),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#coucou%{irc_server_one}/{nick_one}' />"),
partial(expect_stanza,
"/message/body[text()='Mode #coucou [+nt] by {irc_host_one}']"),
partial(expect_stanza, "/presence"),
partial(expect_stanza, "/message[@from='#coucou%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
partial(log_message, "Request with max=0"),
partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>0</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
)),
partial(log_message, "Request with max=2"),
partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>2</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
"/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
"/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
)),
partial(log_message, "Request with max=12"),
partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>12</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
"/iq/disco_items:query/disco_items:item[@jid='#bar%{irc_server_one}']",
"/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
"/iq/disco_items:query/disco_items:item[@jid='#foo%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:first[text()='#bar%{irc_server_one}'][@index='0']",
"/iq/disco_items:query/rsm:set/rsm:last[text()='#foo%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
)),
partial(log_message, "Request with max=1 after=#bar"),
partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
"/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:first[text()='#coucou%{irc_server_one}'][@index='1']",
"/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
)),
partial(log_message, "Request with max=1 after=#bar"),
partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id1' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#bar%{irc_server_one}</after><max>1</max></set></query></iq>"),
partial(expect_stanza, (
"/iq[@type='result']/disco_items:query",
"/iq/disco_items:query/disco_items:item[@jid='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:first[text()='#coucou%{irc_server_one}'][@index='1']",
"/iq/disco_items:query/rsm:set/rsm:last[text()='#coucou%{irc_server_one}']",
"/iq/disco_items:query/rsm:set/rsm:count[text()='3']"
))
]),
Scenario("complete_channel_list_with_pages_of_3",
[
handshake_sequence(),
partial(log_message, "Join 10 channels"),
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#aaa%{irc_server_one}/{nick_one}' />"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),