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

Open
Ivan-lis wants to merge 3 commits from feature_roster_on_login into main
10 changed files with 96 additions and 52 deletions
Showing only changes of commit 30a5e69d14 - Show all commits

2
.vscode/launch.json vendored
View file

@ -20,7 +20,7 @@
"program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
"--gtest_filter=Roster*"
// "--gtest_filter=Roster*"
],
"cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build"

View file

@ -47,7 +47,4 @@ using SetBind = Set<Bind>;
using ResultBind = Result<Bind>;
using IqBind = Iq<Bind>;
inline auto MakeSetBind() {
return SetBind{.id = "1", .payload = Bind{}};
}
} // namespace larra::xmpp::iq

View file

@ -80,21 +80,35 @@ struct Client {
auto CreateResourceBind() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send IQ: Set::Bind");
co_await this->Send(::iq::MakeSetBind());
co_await this->Send(::iq::SetBind{.id = "1", .payload = {}});
auto bind_result = co_await connection.template Read<::iq::ResultBind>();
jid.resource = std::move(bind_result.payload.jid->resource);
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource);
}),
set_bind_response);
co_return;
}
auto UpdateListOfContacts() -> boost::asio::awaitable<void> {
Review

Maybe we can change namings later

Maybe we can change namings later
SPDLOG_INFO("Send IQ: Get::Roster");
co_await this->Send(::iq::MakeGetRoster(jid));
co_await this->Send(::iq::GetRoster{.id = "1", .from = std::format("{}@{}", "invalid_user", jid.server), .payload = {}});
const auto roster_result = co_await connection.template Read<::iq::ResultRoster>();
roster = std::move(roster_result.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
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;
}
@ -143,14 +157,15 @@ 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}; //
}) //
| 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

@ -37,10 +37,10 @@ struct BaseImplWithPayload {
friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) {
element->set_attribute("id", self.id);
if (self.to) {
if(self.to) {
element->set_attribute("to", *self.to);
}
if (self.from) {
if(self.from) {
element->set_attribute("from", *self.from);
}
element->set_attribute("type", kName);
@ -48,6 +48,19 @@ struct BaseImplWithPayload {
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 {
auto node = element->get_attribute("type");
if(!node) {
@ -63,7 +76,6 @@ struct BaseImplWithPayload {
auto from = element->get_attribute("from");
auto to = element->get_attribute("to");
using S = Serialization<PayloadType>;
auto payload = element->get_first_child(S::kDefaultName);
if(!payload) {
@ -73,13 +85,13 @@ struct BaseImplWithPayload {
if(!payload2) {
throw std::runtime_error("Invalid payload for parse Iq");
}
return {
.id = idNode->get_value(),
.from = (from ? std::optional{from->get_value()} : std::nullopt),
.to = (to ? std::optional{to->get_value()} : std::nullopt),
.payload = S::Parse(payload2)};
return {.id = idNode->get_value(),
.from = (from ? std::optional{from->get_value()} : std::nullopt),
.to = (to ? std::optional{to->get_value()} : std::nullopt),
.payload = S::Parse(payload2)};
}
};
static constexpr auto kGetName = "get";
template <typename Payload>

View file

@ -54,9 +54,10 @@ struct FullJid {
struct BareJid {
std::string username;
std::string server;
constexpr operator FullJid() const {
return FullJid{.username = username, .server = server, .resource = ""};
constexpr operator FullJid(this auto&& self) {
return {.username = std::forward_like<decltype(self)>(self.username), .server = std::forward_like<decltype(self)>(self.server)};
}
[[nodiscard]] static auto Parse(std::string_view jid) -> BareJid;
friend auto ToString(const BareJid& jid) -> std::string;

View file

@ -38,9 +38,9 @@ struct Roster {
}
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) {
for(const auto& item : roster.items) {
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");
@ -69,8 +69,4 @@ 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

@ -73,6 +73,25 @@ struct ErrorImpl : BaseError {
}
};
// Helper class to prevent parsing response stream into an expected return type if its name is a 'stream:error'
struct UnknownXmppError : BaseError {
static constexpr auto kDefaultName = "stream:error";
static constexpr std::string_view kErrorMessage = "Unknown XMPP stream error";
static constexpr auto TryParse(xmlpp::Element* element) {
return std::optional{UnknownXmppError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage);
}
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadFormat : ErrorImpl<BadFormat> {};
struct BadNamespacePrefix : ErrorImpl<BadNamespacePrefix> {};
struct Conflict : ErrorImpl<Conflict> {};
@ -81,7 +100,7 @@ struct HostGone : ErrorImpl<HostGone> {};
struct HostUnknown : ErrorImpl<HostUnknown> {};
struct ImproperAdressing : ErrorImpl<ImproperAdressing> {};
struct InternalServerError : ErrorImpl<InternalServerError> {};
struct InvalidForm : ErrorImpl<InvalidForm> {};
struct InvalidFrom : ErrorImpl<InvalidFrom> {};
struct InvalidNamespace : ErrorImpl<InvalidNamespace> {};
struct InvalidXml : ErrorImpl<InvalidXml> {};
struct NotAuthorized : ErrorImpl<NotAuthorized> {};
@ -109,7 +128,7 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::HostUnknown,
error::stream::ImproperAdressing,
error::stream::InternalServerError,
error::stream::InvalidForm,
error::stream::InvalidFrom,
error::stream::InvalidNamespace,
error::stream::InvalidXml,
error::stream::NotAuthorized,
@ -123,6 +142,10 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedEncoding,
error::stream::UnsupportedFeature,
error::stream::UnsupportedStanzaType,
error::stream::UnsupportedVersion>;
error::stream::UnsupportedVersion,
error::stream::UnknownXmppError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, StreamError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant");
} // namespace larra::xmpp

View file

@ -297,7 +297,7 @@ struct RangeToWrapper : T {
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
{ obj.length() } -> std::convertible_to<std::size_t>;
} || std::convertible_to<T, std::string>;
template <typename T>

View file

@ -1,17 +1,13 @@
#include <gtest/gtest.h>
#include <larra/jid.hpp>
#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");
roster.payload.items.emplace_back("u3", "s3");
auto roster = iq::GetRoster{.id = "1", .from = ToString(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
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?
@ -22,18 +18,19 @@ TEST(Roster, SerializeAndParse) {
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';
}
}
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
Review

Code left in the comment

Code left in the comment
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");
auto roster = iq::GetRoster{.id = "1", .from = ToString(jid), .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
EXPECT_NO_THROW({ std::cerr << "[ ] Roster payload: " << ToString(roster.payload) << '\n'; });
EXPECT_NO_THROW({
auto roster_str = ToString(roster.payload);
EXPECT_EQ(kRosterPrintExpectedData.length(), roster_str.capacity());
EXPECT_EQ(kRosterPrintExpectedData, roster_str);
});
}
} // namespace larra::xmpp
Ivan-lis marked this conversation as resolved
Review

EXPECT_EQ and test content

EXPECT_EQ and test content

View file

@ -5,6 +5,9 @@
#include <larra/serialization/auto.hpp>
#include <larra/serialization/error.hpp>
#include <larra/stream_error.hpp>
#include <variant>
#include "utempl/utils.hpp"
using namespace std::literals;