M louloulibs/xmpp/xmpp_component.hpp => louloulibs/xmpp/xmpp_component.hpp +4 -0
@@ 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
*/
M src/bridge/bridge.cpp => src/bridge/bridge.cpp +150 -31
@@ 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
M src/bridge/bridge.hpp => src/bridge/bridge.hpp +23 -3
@@ 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
M src/bridge/list_element.hpp => src/bridge/list_element.hpp +6 -1
@@ 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{};
+};
A src/bridge/result_set_management.hpp => src/bridge/result_set_management.hpp +10 -0
@@ 0,0 1,10 @@
+#pragma once
+
+#include <string>
+
+struct ResultSetInfo
+{
+ int max{-1};
+ std::string before{};
+ std::string after{};
+};
M src/xmpp/biboumi_component.cpp => src/xmpp/biboumi_component.cpp +54 -8
@@ 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);
}
M src/xmpp/biboumi_component.hpp => src/xmpp/biboumi_component.hpp +3 -3
@@ 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
M tests/end_to_end/__main__.py => tests/end_to_end/__main__.py +191 -2
@@ 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