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

Merged
sha512sum merged 3 commits from feature_roster_on_login into main 2024-11-26 15:25:56 +00:00
7 changed files with 201 additions and 1 deletions
Showing only changes of commit c52d237dc4 - Show all commits

View file

@ -27,6 +27,12 @@ auto Coroutine() -> boost::asio::awaitable<void> {
}, },
client); client);
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
co_await client.UpdateListOfContacts();
},
client);
// rfc6120 2.2 // rfc6120 2.2
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPPCORE]), // 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 // a client SHOULD request the roster before sending initial presence

View file

@ -18,6 +18,7 @@
#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>
@ -94,10 +95,28 @@ struct Client {
co_return; co_return;
} }
auto UpdateListOfContacts() -> boost::asio::awaitable<void> {

Maybe we can change namings later

Maybe we can change namings later
SPDLOG_INFO("Send IQ: Get::Roster");
co_await this->Send(::iq::GetRoster{.id = "1", .from = jid, .payload = {}});
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) {
roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
}),
get_roster_response);
co_return;
}
private: private:
bool active = true; bool active = true;
XmlStream<Connection> connection{}; XmlStream<Connection> connection{};
FullJid jid; FullJid jid;
::iq::Roster roster;
}; };
struct StartTlsNegotiationError : std::runtime_error { struct StartTlsNegotiationError : std::runtime_error {

View file

@ -1,7 +1,9 @@
#pragma once #pragma once
#include <larra/jid.hpp>
#include <larra/serialization.hpp> #include <larra/serialization.hpp>
#include <larra/stream_error.hpp> #include <larra/stream_error.hpp>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <optional>
#include <string> #include <string>
namespace larra::xmpp { namespace larra::xmpp {
@ -11,6 +13,8 @@ namespace iq {
template <auto& Name, typename PayloadType> template <auto& Name, typename PayloadType>
struct BaseImplWithPayload { struct BaseImplWithPayload {
std::string id; std::string id;
Ivan-lis marked this conversation as resolved Outdated

Why std::string and not a more limited type like Jid ?

Why std::string and not a more limited type like Jid ?

Fixed

Fixed
std::optional<Jid> from{};
std::optional<Jid> to{};
PayloadType payload; PayloadType payload;
static const inline std::string kName = Name; static const inline std::string kName = Name;
static constexpr auto kDefaultName = "iq"; static constexpr auto kDefaultName = "iq";
@ -19,16 +23,45 @@ struct BaseImplWithPayload {
[[nodiscard]] constexpr auto Id(this Self&& self, std::string id) -> std::decay_t<Self> { [[nodiscard]] constexpr auto Id(this Self&& self, std::string id) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"id", BaseImplWithPayload>(std::forward<Self>(self), std::move(id)); return utils::FieldSetHelper::With<"id", BaseImplWithPayload>(std::forward<Self>(self), std::move(id));
} }
template <typename Self>
[[nodiscard]] constexpr auto To(this Self&& self, Jid to) -> std::decay_t<Self> {
Ivan-lis marked this conversation as resolved Outdated

Uses std::string for setters even though another type is stored.

Uses std::string for setters even though another type is stored.
return utils::FieldSetHelper::With<"to", BaseImplWithPayload>(std::forward<Self>(self), std::move(to));
}
template <typename Self>
[[nodiscard]] constexpr auto From(this Self&& self, Jid from) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"from", BaseImplWithPayload>(std::forward<Self>(self), std::move(from));
}
template <typename NewPayloadType, typename Self> template <typename NewPayloadType, typename Self>
[[nodiscard]] constexpr auto Payload(this Self&& self, NewPayloadType value) { [[nodiscard]] constexpr auto Payload(this Self&& self, NewPayloadType value) {
return utils::FieldSetHelper::With<"payload", BaseImplWithPayload, false>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"payload", BaseImplWithPayload, false>(std::forward<Self>(self), std::move(value));
} }
friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) { friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) {
element->set_attribute("id", self.id); element->set_attribute("id", self.id);
if(self.to) {
element->set_attribute("to", ToString(*self.to));
}
if(self.from) {
element->set_attribute("from", ToString(*self.from));
}
element->set_attribute("type", kName); element->set_attribute("type", kName);
using S = Serialization<PayloadType>; using S = Serialization<PayloadType>;
S::Serialize(element->add_child_element(S::kDefaultName, S::kPrefix), self.payload); S::Serialize(element->add_child_element(S::kDefaultName, S::kPrefix), self.payload);
} }
[[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<BaseImplWithPayload> {
return [&] -> std::optional<BaseImplWithPayload> {
auto node = element->get_attribute("type");
if(!node) {
return std::nullopt;
}
if(node->get_value() != Name) {
return std::nullopt;
}
return Parse(element);
}();
}
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> BaseImplWithPayload { [[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> BaseImplWithPayload {
auto node = element->get_attribute("type"); auto node = element->get_attribute("type");
if(!node) { if(!node) {
@ -41,6 +74,8 @@ struct BaseImplWithPayload {
if(!idNode) { if(!idNode) {
throw std::runtime_error("Not found attribute id for parse Iq"); throw std::runtime_error("Not found attribute id for parse Iq");
} }
auto from = element->get_attribute("from");
auto to = element->get_attribute("to");
using S = Serialization<PayloadType>; using S = Serialization<PayloadType>;
auto payload = element->get_first_child(S::kDefaultName); auto payload = element->get_first_child(S::kDefaultName);
@ -51,9 +86,13 @@ struct BaseImplWithPayload {
if(!payload2) { if(!payload2) {
throw std::runtime_error("Invalid payload for parse Iq"); throw std::runtime_error("Invalid payload for parse Iq");
} }
return {.id = idNode->get_value(), .payload = S::Parse(payload2)}; return {.id = idNode->get_value(),
.from = (from ? std::optional{Jid::Parse(from->get_value())} : std::nullopt),
Ivan-lis marked this conversation as resolved Outdated

Use BareJid::Parse when there might not be a BareJid

Use BareJid::Parse when there might not be a BareJid
.to = (to ? std::optional{Jid::Parse(to->get_value())} : std::nullopt),
.payload = S::Parse(payload2)};
} }
}; };
static constexpr auto kGetName = "get"; static constexpr auto kGetName = "get";
template <typename Payload> template <typename Payload>

View file

@ -0,0 +1,56 @@
#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 RosterItem {
BareJid jid;
friend constexpr auto ToString(const RosterItem& item) {
return ToString(item.jid);
}
constexpr auto operator==(const RosterItem&) const -> bool = default;
friend auto operator<<(xmlpp::Element* element, const RosterItem& item) -> void;
[[nodiscard]] static auto Parse(xmlpp::Element* element) -> RosterItem;
};
struct Roster {
static constexpr auto kDefaultName = "query";
static constexpr auto kDefaultNamespace = "jabber:iq:roster";
std::vector<RosterItem> 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.jid) + 3;
Ivan-lis marked this conversation as resolved Outdated

It would be better to move this to a .cpp file to make the header files lighter.

It would be better to move this to a .cpp file to make the header files lighter.
}),
prefix.length() + suffix.length(),
std::plus<>{});
std::string s;
s.resize(total_length);
sha512sum marked this conversation as resolved Outdated

Why not range based for?

Why not range based for?
s = prefix;
for(const auto& el : roster.items) {
s += ToString(el);
s += "\n\t";
}
return s += suffix;
}
friend auto operator<<(xmlpp::Element* element, const Roster& roster) -> void;
[[nodiscard]] static auto Parse(xmlpp::Element* element) -> Roster;
};
using GetRoster = Get<Roster>;
using ResultRoster = Result<Roster>;
Ivan-lis marked this conversation as resolved Outdated

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

Done

Done
} // 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>;
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 totalLength = 0;
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
boost::pfr::for_each_field(obj, [&](const LengthCalculatable auto& field) {
totalLength += field.length(); // Accumulate length of each field
});
return totalLength;
}
} // namespace larra::xmpp::utils } // namespace larra::xmpp::utils

30
library/src/roster.cpp Normal file
View file

@ -0,0 +1,30 @@
#include <larra/roster.hpp>
#include <larra/serialization/auto.hpp>
namespace larra::xmpp::serialization {
namespace iq = larra::xmpp::iq;
template <>
constexpr auto kSerializationConfig<iq::RosterItem> = SerializationConfig<iq::RosterItem>{};
template <>
constexpr auto kSerializationConfig<iq::Roster> = SerializationConfig<iq::Roster>{}.With<"items">({Config<std::vector<iq::RosterItem>>{}});
} // namespace larra::xmpp::serialization
namespace larra::xmpp::iq {
namespace S = larra::xmpp::serialization;
auto operator<<(xmlpp::Element* element, const RosterItem& self) -> void {
S::Serialize(element, self);
}
auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
return S::Parse<RosterItem>(element);
}
auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace);
S::Serialize(element, self);
}
auto Roster::Parse(xmlpp::Element* element) -> Roster {
return S::Parse<Roster>(element);
}
} // namespace larra::xmpp::iq

36
tests/roster.cpp Normal file
View file

@ -0,0 +1,36 @@
#include <gtest/gtest.h>
#include <larra/jid.hpp>
#include <larra/roster.hpp>
namespace larra::xmpp {
TEST(Roster, SerializeAndParse) {
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::GetRoster{.id = "1", .from = jid, .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
xmlpp::Document doc;
auto node = doc.create_root_node("iq");
Ivan-lis marked this conversation as resolved Outdated

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?
node << roster;
auto parseRes = decltype(roster)::Parse(node);
ASSERT_EQ(roster.payload.items.size(), parseRes.payload.items.size());
for(const auto& [idx, expectEl, parsedEl] : std::views::zip(std::views::iota(0), roster.payload.items, parseRes.payload.items)) {
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
EXPECT_EQ(expectEl, parsedEl) << "Mismatched on idx: " << idx;
}
}
static constexpr std::string_view kRosterPrintExpectedData = "Roster: [\n\tu1@s1\n\tu2@s2\n\tu3@s3\n\t]";
TEST(Roster, Print) {
Ivan-lis marked this conversation as resolved Outdated

Code left in the comment

Code left in the comment
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::GetRoster{.id = "1", .from = jid, .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
EXPECT_NO_THROW({
auto rosterStr = ToString(roster.payload);
EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.capacity());
EXPECT_EQ(kRosterPrintExpectedData, rosterStr);
});
}
} // namespace larra::xmpp
Ivan-lis marked this conversation as resolved Outdated

EXPECT_EQ and test content

EXPECT_EQ and test content