~singpolyma/biboumi

76a8189b46177eb78eee12d1cb3266f282acd380 — louiz’ 8 years ago ffad430
Implement result-set-management for LIST queries

ref #2948
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