From c52d237dc4c34481ccda48467f6f48c68f9fd282 Mon Sep 17 00:00:00 2001 From: Ivan-lis Date: Mon, 25 Nov 2024 13:20:32 +0000 Subject: [PATCH] Feature: iq::Roster --- examples/src/connect.cpp | 6 +++ library/include/larra/client/client.hpp | 19 +++++++++ library/include/larra/iq.hpp | 41 +++++++++++++++++- library/include/larra/roster.hpp | 56 +++++++++++++++++++++++++ library/include/larra/utils.hpp | 14 +++++++ library/src/roster.cpp | 30 +++++++++++++ tests/roster.cpp | 36 ++++++++++++++++ 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 library/include/larra/roster.hpp create mode 100644 library/src/roster.cpp create mode 100644 tests/roster.cpp diff --git a/examples/src/connect.cpp b/examples/src/connect.cpp index ba9be27..9f24137 100644 --- a/examples/src/connect.cpp +++ b/examples/src/connect.cpp @@ -27,6 +27,12 @@ auto Coroutine() -> boost::asio::awaitable { }, client); + co_await std::visit( + [](auto& client) -> boost::asio::awaitable { + 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 [XMPP‑CORE]), // a client SHOULD request the roster before sending initial presence diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp index c10d239..876f13e 100644 --- a/library/include/larra/client/client.hpp +++ b/library/include/larra/client/client.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -94,10 +95,28 @@ struct Client { co_return; } + auto UpdateListOfContacts() -> boost::asio::awaitable { + 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>(); + 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: bool active = true; XmlStream connection{}; FullJid jid; + ::iq::Roster roster; }; struct StartTlsNegotiationError : std::runtime_error { diff --git a/library/include/larra/iq.hpp b/library/include/larra/iq.hpp index bd7b291..0ed886b 100644 --- a/library/include/larra/iq.hpp +++ b/library/include/larra/iq.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include #include +#include #include namespace larra::xmpp { @@ -11,6 +13,8 @@ namespace iq { template struct BaseImplWithPayload { std::string id; + std::optional from{}; + std::optional to{}; PayloadType payload; static const inline std::string kName = Name; static constexpr auto kDefaultName = "iq"; @@ -19,16 +23,45 @@ struct BaseImplWithPayload { [[nodiscard]] constexpr auto Id(this Self&& self, std::string id) -> std::decay_t { return utils::FieldSetHelper::With<"id", BaseImplWithPayload>(std::forward(self), std::move(id)); } + template + [[nodiscard]] constexpr auto To(this Self&& self, Jid to) -> std::decay_t { + return utils::FieldSetHelper::With<"to", BaseImplWithPayload>(std::forward(self), std::move(to)); + } + template + [[nodiscard]] constexpr auto From(this Self&& self, Jid from) -> std::decay_t { + return utils::FieldSetHelper::With<"from", BaseImplWithPayload>(std::forward(self), std::move(from)); + } template [[nodiscard]] constexpr auto Payload(this Self&& self, NewPayloadType value) { return utils::FieldSetHelper::With<"payload", BaseImplWithPayload, false>(std::forward(self), std::move(value)); } friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) { 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); using S = Serialization; S::Serialize(element->add_child_element(S::kDefaultName, S::kPrefix), self.payload); } + + [[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional { + return [&] -> std::optional { + 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 { auto node = element->get_attribute("type"); if(!node) { @@ -41,6 +74,8 @@ struct BaseImplWithPayload { if(!idNode) { 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; auto payload = element->get_first_child(S::kDefaultName); @@ -51,9 +86,13 @@ struct BaseImplWithPayload { if(!payload2) { 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), + .to = (to ? std::optional{Jid::Parse(to->get_value())} : std::nullopt), + .payload = S::Parse(payload2)}; } }; + static constexpr auto kGetName = "get"; template diff --git a/library/include/larra/roster.hpp b/library/include/larra/roster.hpp new file mode 100644 index 0000000..a0f04f0 --- /dev/null +++ b/library/include/larra/roster.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include + +#include +#include +#include +#include +#include +#include + +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 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; + }), + 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 auto operator<<(xmlpp::Element* element, const Roster& roster) -> void; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> Roster; +}; + +using GetRoster = Get; +using ResultRoster = Result; + +} // namespace larra::xmpp::iq diff --git a/library/include/larra/utils.hpp b/library/include/larra/utils.hpp index 1fb05a5..7534947 100644 --- a/library/include/larra/utils.hpp +++ b/library/include/larra/utils.hpp @@ -295,4 +295,18 @@ struct RangeToWrapper : T { : T{std::forward(args)...} {}; }; +template +concept LengthCalculatable = requires(const T& obj) { + { obj.length() } -> std::convertible_to; +} || std::convertible_to; + +template +auto AccumulateFieldLength(const T& obj) -> std::size_t { + std::size_t totalLength = 0; + boost::pfr::for_each_field(obj, [&](const LengthCalculatable auto& field) { + totalLength += field.length(); // Accumulate length of each field + }); + return totalLength; +} + } // namespace larra::xmpp::utils diff --git a/library/src/roster.cpp b/library/src/roster.cpp new file mode 100644 index 0000000..7b01dcc --- /dev/null +++ b/library/src/roster.cpp @@ -0,0 +1,30 @@ +#include +#include + +namespace larra::xmpp::serialization { +namespace iq = larra::xmpp::iq; + +template <> +constexpr auto kSerializationConfig = SerializationConfig{}; +template <> +constexpr auto kSerializationConfig = SerializationConfig{}.With<"items">({Config>{}}); +} // 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(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(element); +} +} // namespace larra::xmpp::iq diff --git a/tests/roster.cpp b/tests/roster.cpp new file mode 100644 index 0000000..6944a0c --- /dev/null +++ b/tests/roster.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +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"); + 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)) { + 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) { + 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