diff options
-rw-r--r-- | louloulibs/xmpp/xmpp_component.hpp | 4 | ||||
-rw-r--r-- | src/bridge/bridge.cpp | 181 | ||||
-rw-r--r-- | src/bridge/bridge.hpp | 26 | ||||
-rw-r--r-- | src/bridge/list_element.hpp | 7 | ||||
-rw-r--r-- | src/bridge/result_set_management.hpp | 10 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.cpp | 62 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.hpp | 6 | ||||
-rw-r--r-- | tests/end_to_end/__main__.py | 193 |
8 files changed, 441 insertions, 48 deletions
diff --git a/louloulibs/xmpp/xmpp_component.hpp b/louloulibs/xmpp/xmpp_component.hpp index b556ce2..4b6f37d 100644 --- a/louloulibs/xmpp/xmpp_component.hpp +++ b/louloulibs/xmpp/xmpp_component.hpp @@ -31,6 +31,7 @@ #define FORWARD_NS "urn:xmpp:forward:0" #define CLIENT_NS "jabber:client" #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 @@ -219,6 +220,9 @@ public: 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 */ diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index d16875f..8849ef9 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -1,5 +1,4 @@ #include <bridge/bridge.hpp> -#include <bridge/list_element.hpp> #include <xmpp/biboumi_component.hpp> #include <network/poller.hpp> #include <utils/empty_if_fixed_server.hpp> @@ -10,6 +9,7 @@ #include <utils/split.hpp> #include <xmpp/jid.hpp> #include <database/database.hpp> +#include "result_set_management.hpp" using namespace std::string_literals; @@ -386,45 +386,164 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick) irc->send_nick_command(new_nick); } -void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, - const std::string& to_jid) +void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, 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, - const IrcMessage& message) mutable -> bool + this->add_waiting_irc(std::move(cb)); + } + + // 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; - 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 (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; + if (std::distance(begin, end) >= rs_info.max) + end = begin + rs_info.max; } - else if (message.command == "322" || message.command == "RPL_LIST") - { // Add element to list - if (message.arguments.size() == 4) - list.emplace_back(message.arguments[1], message.arguments[2], - message.arguments[3]); - return false; + } + else + { + if (rs_info.after.empty() && rs_info.before.empty() && rs_info.max < 0) + 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") - { // 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); - return true; + if (!rs_info.before.empty()) + { + 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 (end == channel_list.channels.end()) + return false; } - return false; - }; - this->add_waiting_irc(std::move(cb)); + if (rs_info.max >= 0) + { + 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, @@ -1002,4 +1121,4 @@ void Bridge::set_record_history(const bool val) { this->record_history = val; } -#endif
\ No newline at end of file +#endif diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index b278ea7..1a1d201 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -1,5 +1,7 @@ #pragma once +#include <bridge/result_set_management.hpp> +#include <bridge/list_element.hpp> #include <irc/irc_message.hpp> #include <irc/irc_client.hpp> @@ -17,6 +19,7 @@ class BiboumiComponent; class Poller; +class ResultSetInfo; /** * A callback called for each IrcMessage we receive. If the message triggers @@ -87,8 +90,19 @@ public: 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& from_jid); - void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, - const std::string& to_jid); + void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, 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, const std::string& affiliation, const std::string& role); /** @@ -271,7 +285,6 @@ private: * response iq. */ std::vector<irc_responder_callback_t> waiting_irc; - /** * Resources to IRC channel/server mapping: */ @@ -300,6 +313,13 @@ private: * TODO: send message history */ 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 bool record_history { true }; #endif diff --git a/src/bridge/list_element.hpp b/src/bridge/list_element.hpp index 1eff2ee..554c83d 100644 --- a/src/bridge/list_element.hpp +++ b/src/bridge/list_element.hpp @@ -1,6 +1,6 @@ #pragma once - +#include <vector> #include <string> struct ListElement @@ -17,3 +17,8 @@ struct ListElement }; +struct ChannelList +{ + bool complete{true}; + std::vector<ListElement> channels{}; +}; diff --git a/src/bridge/result_set_management.hpp b/src/bridge/result_set_management.hpp new file mode 100644 index 0000000..6ff82ba --- /dev/null +++ b/src/bridge/result_set_management.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <string> + +struct ResultSetInfo +{ + int max{-1}; + std::string before{}; + std::string after{}; +}; diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 3a016b9..f43b5e0 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -15,7 +15,7 @@ #include <stdexcept> #include <iostream> -#include <stdio.h> +#include <cstdlib> #include <louloulibs.h> #include <biboumi.h> @@ -27,6 +27,7 @@ #endif #include <database/database.hpp> +#include <bridge/result_set_management.hpp> using namespace std::string_literals; @@ -463,7 +464,22 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) } else if (node.empty() && iid.type == Iid::Type::Server) { // 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(); } } @@ -749,10 +765,11 @@ void BiboumiComponent::send_ping_request(const std::string& from, this->waiting_iq[id] = result_cb; } -void BiboumiComponent::send_iq_room_list_result(const std::string& id, - const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list) +void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid, + const std::string& from, const ChannelList& channel_list, + std::vector<ListElement>::const_iterator begin, + std::vector<ListElement>::const_iterator end, + const ResultSetInfo& rs_info) { Stanza iq("iq"); iq["from"] = from + "@" + this->served_hostname; @@ -761,12 +778,41 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, iq["type"] = "result"; XmlNode query("query"); query["xmlns"] = DISCO_ITEMS_NS; - for (const auto& room: rooms_list) + + for (auto it = begin; it != end; ++it) { XmlNode item("item"); - item["jid"] = room.channel + "%" + from + "@" + this->served_hostname; + item["jid"] = it->channel + "@" + this->served_hostname; 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)); this->send_stanza(iq); } diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index 8b2ac78..77104ed 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -79,9 +79,9 @@ public: /** * Send the channels list in one big stanza */ - void send_iq_room_list_result(const std::string& id, const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list); + void send_iq_room_list_result(const std::string& id, const std::string& to_jid, const std::string& from, + const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin, + 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); /** * Handle the various stanza types diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py index d498ba7..f4decff 100644 --- a/tests/end_to_end/__main__.py +++ b/tests/end_to_end/__main__.py @@ -117,7 +117,8 @@ def match(stanza, xpath): 'mam': 'urn:xmpp:mam:1', 'delay': 'urn:xmpp:delay', 'forward': 'urn:xmpp:forward:0', - 'client': 'jabber:client'}) + 'client': 'jabber:client', + 'rsm': 'http://jabber.org/protocol/rsm'}) return matched @@ -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='#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}'), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#bbb%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#ccc%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#ddd%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#eee%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#fff%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#ggg%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#hhh%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#iii%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(send_stanza, + "<presence from='{jid_one}/{resource_one}' to='#jjj%{irc_server_one}/{nick_one}' />"), + partial(expect_stanza, "/message"), + partial(expect_stanza, "/presence"), + partial(expect_stanza, "/message"), + + partial(log_message, "Request the first page, with a limit of 3"), + partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><max>3</max></set></query></iq>"), + partial(expect_stanza, ( + "/iq[@type='result']/disco_items:query", + "/iq/disco_items:query/disco_items:item[@jid='#aaa%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#bbb%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#ccc%{irc_server_one}']", + "/iq/disco_items:query/rsm:set/rsm:first[text()='#aaa%{irc_server_one}'][@index='0']", + "/iq/disco_items:query/rsm:set/rsm:last[text()='#ccc%{irc_server_one}']" + )), + + partial(log_message, "Request subsequent pages"), + partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#ccc%{irc_server_one}</after><max>3</max></set></query></iq>"), + partial(expect_stanza, ( + "/iq[@type='result']/disco_items:query", + "/iq/disco_items:query/disco_items:item[@jid='#ddd%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#eee%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#fff%{irc_server_one}']", + "/iq/disco_items:query/rsm:set/rsm:first[text()='#ddd%{irc_server_one}'][@index='3']", + "/iq/disco_items:query/rsm:set/rsm:last[text()='#fff%{irc_server_one}']" + )), + + partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#fff%{irc_server_one}</after><max>3</max></set></query></iq>"), + partial(expect_stanza, ( + "/iq[@type='result']/disco_items:query", + "/iq/disco_items:query/disco_items:item[@jid='#ggg%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#hhh%{irc_server_one}']", + "/iq/disco_items:query/disco_items:item[@jid='#iii%{irc_server_one}']", + "/iq/disco_items:query/rsm:set/rsm:first[text()='#ggg%{irc_server_one}'][@index='6']", + "/iq/disco_items:query/rsm:set/rsm:last[text()='#iii%{irc_server_one}']" + )), + + partial(send_stanza, "<iq from='{jid_one}/{resource_one}' id='id' to='{irc_server_one}' type='get'><query xmlns='http://jabber.org/protocol/disco#items'><set xmlns='http://jabber.org/protocol/rsm'><after>#iii%{irc_server_one}</after><max>3</max></set></query></iq>"), + partial(expect_stanza, ( + "/iq[@type='result']/disco_items:query", + "/iq/disco_items:query/disco_items:item[@jid='#jjj%{irc_server_one}']", + "/iq/disco_items:query/rsm:set/rsm:first[text()='#jjj%{irc_server_one}'][@index='9']", + "/iq/disco_items:query/rsm:set/rsm:last[text()='#jjj%{irc_server_one}']", + "/iq/disco_items:query/rsm:set/rsm:count[text()='10']" + )), + ]) ) failures = 0 |