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": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build" "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": { "presentation": {
"clear": true "clear": true
}, },
"hide": true, "hide": false,
"group": { "group": {
"kind": "build" "kind": "build"
} }

View file

@ -1,3 +1,4 @@
#include <spdlog/common.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <boost/asio/co_spawn.hpp> #include <boost/asio/co_spawn.hpp>
@ -7,6 +8,8 @@
#include <larra/printer_stream.hpp> #include <larra/printer_stream.hpp>
#include <print> #include <print>
namespace iq = larra::xmpp::iq;
auto Coroutine() -> boost::asio::awaitable<void> { auto Coroutine() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Connecting client..."); 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>>( 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"}, larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
{.useTls = larra::xmpp::client::Options::kNever}); {.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( co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> { [](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{}); co_await client.Send(larra::xmpp::presence::c2s::Available{});
}, },
client); client);
} catch(const std::exception& err) { } catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what()); SPDLOG_ERROR("{}", err.what());
co_return; co_return;

View file

@ -11,16 +11,24 @@
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
#include <boost/asio/use_awaitable.hpp> #include <boost/asio/use_awaitable.hpp>
#include <charconv> #include <charconv>
#include <larra/bind.hpp>
#include <larra/client/challenge_response.hpp> #include <larra/client/challenge_response.hpp>
#include <larra/client/options.hpp> #include <larra/client/options.hpp>
#include <larra/client/starttls_response.hpp> #include <larra/client/starttls_response.hpp>
#include <larra/client/xmpp_client_stream_features.hpp> #include <larra/client/xmpp_client_stream_features.hpp>
#include <larra/encryption.hpp> #include <larra/encryption.hpp>
#include <larra/features.hpp> #include <larra/features.hpp>
#include <larra/roster.hpp>
#include <larra/stream.hpp> #include <larra/stream.hpp>
#include <larra/user_account.hpp> #include <larra/user_account.hpp>
#include <larra/xml_stream.hpp> #include <larra/xml_stream.hpp>
#include <ranges> #include <ranges>
#include <utility>
namespace rng = std::ranges;
namespace views = std::views;
namespace iq = larra::xmpp::iq;
namespace larra::xmpp { namespace larra::xmpp {
constexpr auto kDefaultXmppPort = 5222; constexpr auto kDefaultXmppPort = 5222;
@ -29,12 +37,10 @@ constexpr auto kDefaultXmppPort = 5222;
namespace larra::xmpp::client { namespace larra::xmpp::client {
namespace rng = std::ranges;
namespace views = std::views;
template <typename Connection> template <typename Connection>
struct Client { 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<>> template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
constexpr auto Close(Token token = {}) { constexpr auto Close(Token token = {}) {
this->active = false; this->active = false;
@ -68,14 +74,35 @@ struct Client {
auto Send(const T& object) -> boost::asio::awaitable<void> { auto Send(const T& object) -> boost::asio::awaitable<void> {
co_await this->connection.Send(object); co_await this->connection.Send(object);
} }
[[nodiscard]] constexpr auto Jid() const -> const BareJid& { [[nodiscard]] constexpr auto Jid() const -> const FullJid& {
return this->jid; 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: private:
bool active = true; bool active = true;
XmlStream<Connection> connection{}; XmlStream<Connection> connection{};
BareJid jid; FullJid jid;
::iq::Roster roster;
}; };
struct StartTlsNegotiationError : std::runtime_error { struct StartTlsNegotiationError : std::runtime_error {
@ -116,15 +143,14 @@ struct Challenge {
throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name())); throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name()));
} }
std::string decoded = DecodeBase64(node->get_first_child_text()->get_content()); std::string decoded = DecodeBase64(node->get_first_child_text()->get_content());
auto params = std::views::split(decoded, ',') // auto params = std::views::split(decoded, ',') | std::views::transform([](auto param) {
| std::views::transform([](auto param) { // return std::string_view{param};
return std::string_view{param}; // }) |
}) // std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> {
| std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { // auto v = param.find("=");
auto v = param.find("="); // return {param.substr(0, v), param.substr(v + 1)};
return {param.substr(0, v), param.substr(v + 1)}; // }) |
}) // std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
| std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
return {.body = std::move(decoded), return {.body = std::move(decoded),
.serverNonce = params.at("r"), .serverNonce = params.at("r"),
.salt = DecodeBase64(params.at("s")), .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) {
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{}};
}
} // namespace larra::xmpp::iq

View file

@ -295,4 +295,18 @@ struct RangeToWrapper : T {
: T{std::forward<Args>(args)...} {}; : 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
} || 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 } // 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