Features: iq::Bind and iq::Roster #4

Open
Ivan-lis wants to merge 4 commits from feature_roster_on_login into main
7 changed files with 207 additions and 16 deletions
Showing only changes of commit 7685385559 - Show all commits

12
.vscode/launch.json vendored
View file

@ -12,6 +12,18 @@
"args": [],
"cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug: tests",
"program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
"--gtest_filter=Roster*"
],
"cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build"
}
]
}

2
.vscode/tasks.json vendored
View file

@ -104,7 +104,7 @@
"presentation": {
"clear": true
},
"hide": true,
"hide": false,
"group": {
"kind": "build"
}

View file

@ -1,3 +1,4 @@
#include <spdlog/common.h>
#include <spdlog/spdlog.h>
#include <boost/asio/co_spawn.hpp>
@ -7,6 +8,8 @@
#include <larra/printer_stream.hpp>
#include <print>
namespace iq = larra::xmpp::iq;
auto Coroutine() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Connecting client...");
@ -14,11 +17,32 @@ auto Coroutine() -> boost::asio::awaitable<void> {
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
{.useTls = larra::xmpp::client::Options::kNever});
// rfc6120 7.1
// After a client authenticates with a server,
// it MUST bind a specific resource to the stream so that the server can properly address the client.
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
co_await client.CreateResourceBind();
},
client);
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
co_await client.UpdateListOfContacts();
},
client);
// rfc6120 2.2
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPPCORE]),
// a client SHOULD request the roster before sending initial presence
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send presence: Available");
co_await client.Send(larra::xmpp::presence::c2s::Available{});
},
client);
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
co_return;

View file

@ -11,16 +11,24 @@
#include <boost/asio/ssl.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <charconv>
#include <larra/bind.hpp>
#include <larra/client/challenge_response.hpp>
#include <larra/client/options.hpp>
#include <larra/client/starttls_response.hpp>
#include <larra/client/xmpp_client_stream_features.hpp>
#include <larra/encryption.hpp>
#include <larra/features.hpp>
#include <larra/roster.hpp>
#include <larra/stream.hpp>
#include <larra/user_account.hpp>
#include <larra/xml_stream.hpp>
#include <ranges>
#include <utility>
namespace rng = std::ranges;
namespace views = std::views;
namespace iq = larra::xmpp::iq;
namespace larra::xmpp {
constexpr auto kDefaultXmppPort = 5222;
@ -29,12 +37,10 @@ constexpr auto kDefaultXmppPort = 5222;
namespace larra::xmpp::client {
namespace rng = std::ranges;
namespace views = std::views;
template <typename Connection>
struct Client {
constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {};
constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {
}
template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
constexpr auto Close(Token token = {}) {
this->active = false;
@ -68,14 +74,35 @@ struct Client {
auto Send(const T& object) -> boost::asio::awaitable<void> {
co_await this->connection.Send(object);
}
[[nodiscard]] constexpr auto Jid() const -> const BareJid& {
[[nodiscard]] constexpr auto Jid() const -> const FullJid& {
return this->jid;
}
auto CreateResourceBind() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send IQ: Set::Bind");
co_await this->Send(::iq::MakeSetBind());
auto bind_result = co_await connection.template Read<::iq::ResultBind>();
jid.resource = std::move(bind_result.payload.jid->resource);
co_return;
}
auto UpdateListOfContacts() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send IQ: Get::Roster");
co_await this->Send(::iq::MakeGetRoster(jid));
const auto roster_result = co_await connection.template Read<::iq::ResultRoster>();
roster = std::move(roster_result.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
co_return;
Review

Maybe we can change namings later

Maybe we can change namings later
}
private:
bool active = true;
XmlStream<Connection> connection{};
BareJid jid;
FullJid jid;
::iq::Roster roster;
};
struct StartTlsNegotiationError : std::runtime_error {
@ -116,15 +143,14 @@ struct Challenge {
throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name()));
}
std::string decoded = DecodeBase64(node->get_first_child_text()->get_content());
auto params = std::views::split(decoded, ',') //
| std::views::transform([](auto param) { //
return std::string_view{param}; //
}) //
| std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { //
auto v = param.find("="); //
return {param.substr(0, v), param.substr(v + 1)}; //
}) //
| std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
auto params = std::views::split(decoded, ',') | std::views::transform([](auto param) {
return std::string_view{param};
Ivan-lis marked this conversation as resolved Outdated

Why? It was more readable before.

Why? It was more readable before.
}) |
std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> {
auto v = param.find("=");
return {param.substr(0, v), param.substr(v + 1)};
}) |
std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
return {.body = std::move(decoded),
.serverNonce = params.at("r"),
.salt = DecodeBase64(params.at("s")),

View file

@ -0,0 +1,76 @@
#pragma once
#include <libxml++/libxml++.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp>
#include <larra/jid.hpp>
#include <larra/utils.hpp>
#include <ranges>
#include <string>
#include <vector>
namespace larra::xmpp::iq {
struct Roster {
static constexpr auto kDefaultName = "query";
static constexpr auto kDefaultNamespace = "jabber:iq:roster";
std::vector<BareJid> items;
friend auto ToString(const Roster& roster) -> std::string {
static constexpr std::string_view prefix = "Roster: [\n\t";
static constexpr std::string_view suffix = "]";
// \n\r\t
std::size_t total_length = std::ranges::fold_left(roster.items | std::views::transform([](const auto& el) {
return larra::xmpp::utils::AccumulateFieldLength(el) + 3;
}),
prefix.length() + suffix.length(),
std::plus<>{});
std::string s;
s.resize(total_length);
s = prefix;
for(const auto& el : roster.items) {
s += ToString(el);
s += "\n\t";
}
return s += suffix;
}
friend constexpr auto operator<<(xmlpp::Element* element, const Roster& roster) {
element->set_attribute("xmlns", Roster::kDefaultNamespace);
std::ranges::for_each(roster.items, [element](const auto& item) {
sha512sum marked this conversation as resolved Outdated

Why not range based for?

Why not range based for?
element->add_child_element("item")->set_attribute("jid", ToString(item));
});
}
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> Roster {
const auto& item_nodes = element->get_children("item");
if(item_nodes.empty()) {
SPDLOG_DEBUG("No items at Iq::Roster");
}
return {.items = item_nodes | std::views::transform([](const xmlpp::Node* node) {
auto item_element = dynamic_cast<const xmlpp::Element*>(node);
if(!item_element) {
throw std::runtime_error("Can't convert xmlpp::Node to xmlpp::Element");
Ivan-lis marked this conversation as resolved
Review

You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization

You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization
Review

Done

Done
}
auto jid_ptr = item_element->get_attribute("jid");
if(!jid_ptr) {
throw std::runtime_error("Not found attribute 'jid' for parse Roster item");
}
return BareJid::Parse(jid_ptr->get_value());
}) |
std::ranges::to<std::vector<BareJid>>()};
}
};
using GetRoster = Get<Roster>;
using ResultRoster = Result<Roster>;
using IqRoster = Iq<Roster>;
inline auto MakeGetRoster(const FullJid& jid) {
return GetRoster{.id = "1", .from = ToString(jid), .payload = Roster{}};
Ivan-lis marked this conversation as resolved Outdated

For what?

For what?
}
} // namespace larra::xmpp::iq

View file

@ -295,4 +295,18 @@ struct RangeToWrapper : T {
: T{std::forward<Args>(args)...} {};
};
template <typename T>
concept LengthCalculatable = requires(const T& obj) {
{ obj.length() } -> std::convertible_to<std::size_t>; // Checks if obj has a length() method returning a type convertible to std::size_t
sha512sum marked this conversation as resolved Outdated

A comment that doesn't explain anything and just repeats the code.

A comment that doesn't explain anything and just repeats the code.
} || std::convertible_to<T, std::string>;
template <typename T>
auto AccumulateFieldLength(const T& obj) -> std::size_t {
std::size_t total_length = 0;
boost::pfr::for_each_field(obj, [&](const LengthCalculatable auto& field) {
total_length += field.length(); // Accumulate length of each field
});
return total_length;
}
} // namespace larra::xmpp::utils

39
tests/roster.cpp Normal file
View file

@ -0,0 +1,39 @@
#include <gtest/gtest.h>
#include <larra/roster.hpp>
#include "larra/jid.hpp"
namespace larra::xmpp {
TEST(Roster, SerializeAndParse) {
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::MakeGetRoster(jid);
roster.payload.items.emplace_back("u1", "s1");
roster.payload.items.emplace_back("u2", "s2");
Ivan-lis marked this conversation as resolved
Review

Why not just throw all the necessary elements through the vector constructor in Roster constructor?

Why not just throw all the necessary elements through the vector constructor in Roster constructor?
roster.payload.items.emplace_back("u3", "s3");
xmlpp::Document doc;
auto node = doc.create_root_node("iq");
node << roster;
auto parse_res = decltype(roster)::Parse(node);
ASSERT_EQ(roster.payload.items.size(), parse_res.payload.items.size());
for(const auto& [idx, expect_el, parsed_el] : std::views::zip(std::views::iota(0), roster.payload.items, parse_res.payload.items)) {
EXPECT_EQ(expect_el, parsed_el) << "Mismatched on idx: " << idx;
// std::cerr << " " << "idx: " << idx << "; expect_el: " << expect_el << "; parsed_el: " << parsed_el << '\n';
Ivan-lis marked this conversation as resolved
Review

Code left in the comment

Code left in the comment
}
}
TEST(Roster, Print) {
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::MakeGetRoster(jid);
roster.payload.items.emplace_back("u1", "s1");
roster.payload.items.emplace_back("u2", "s2");
roster.payload.items.emplace_back("u3", "s3");
EXPECT_NO_THROW({ std::cerr << "[ ] Roster payload: " << ToString(roster.payload) << '\n'; });
Ivan-lis marked this conversation as resolved
Review

EXPECT_EQ and test content

EXPECT_EQ and test content
}
} // namespace larra::xmpp