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

Open
Ivan-lis wants to merge 4 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", "program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [ "args": [
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS] // --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
"--gtest_filter=Roster*" // "--gtest_filter=Roster*"
], ],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build" "preLaunchTask": "GCC: Build"

View file

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

View file

@ -80,21 +80,35 @@ struct Client {
auto CreateResourceBind() -> boost::asio::awaitable<void> { auto CreateResourceBind() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send IQ: Set::Bind"); 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>(); auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
jid.resource = std::move(bind_result.payload.jid->resource); 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; co_return;
} }
auto UpdateListOfContacts() -> boost::asio::awaitable<void> { 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"); 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);
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)); SPDLOG_INFO("New roster: {}", ToString(roster));
}),
get_roster_response);
co_return; co_return;
} }
@ -143,14 +157,15 @@ 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, ',') | std::views::transform([](auto param) { auto params = std::views::split(decoded, ',') //
return std::string_view{param}; | 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("="); | std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { //
return {param.substr(0, v), param.substr(v + 1)}; 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>>(); }) //
| 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

@ -48,6 +48,19 @@ struct BaseImplWithPayload {
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) {
@ -63,7 +76,6 @@ struct BaseImplWithPayload {
auto from = element->get_attribute("from"); auto from = element->get_attribute("from");
auto to = element->get_attribute("to"); 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);
if(!payload) { if(!payload) {
@ -73,13 +85,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 { return {.id = idNode->get_value(),
.id = idNode->get_value(),
.from = (from ? std::optional{from->get_value()} : std::nullopt), .from = (from ? std::optional{from->get_value()} : std::nullopt),
.to = (to ? std::optional{to->get_value()} : std::nullopt), .to = (to ? std::optional{to->get_value()} : std::nullopt),
.payload = S::Parse(payload2)}; .payload = S::Parse(payload2)};
} }
}; };
static constexpr auto kGetName = "get"; static constexpr auto kGetName = "get";
template <typename Payload> template <typename Payload>

View file

@ -54,9 +54,10 @@ struct FullJid {
struct BareJid { struct BareJid {
std::string username; std::string username;
std::string server; std::string server;
constexpr operator FullJid() const { constexpr operator FullJid(this auto&& self) {
return FullJid{.username = username, .server = server, .resource = ""}; 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; [[nodiscard]] static auto Parse(std::string_view jid) -> BareJid;
friend auto ToString(const BareJid& jid) -> std::string; 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) { friend constexpr auto operator<<(xmlpp::Element* element, const Roster& roster) {
element->set_attribute("xmlns", Roster::kDefaultNamespace); 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)); element->add_child_element("item")->set_attribute("jid", ToString(item));
}); }
} }
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> Roster { [[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> Roster {
const auto& item_nodes = element->get_children("item"); const auto& item_nodes = element->get_children("item");
@ -69,8 +69,4 @@ using GetRoster = Get<Roster>;
using ResultRoster = Result<Roster>; using ResultRoster = Result<Roster>;
using IqRoster = Iq<Roster>; using IqRoster = Iq<Roster>;
inline auto MakeGetRoster(const FullJid& jid) {
return GetRoster{.id = "1", .from = ToString(jid), .payload = Roster{}};
}
} // namespace larra::xmpp::iq } // 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 BadFormat : ErrorImpl<BadFormat> {};
struct BadNamespacePrefix : ErrorImpl<BadNamespacePrefix> {}; struct BadNamespacePrefix : ErrorImpl<BadNamespacePrefix> {};
struct Conflict : ErrorImpl<Conflict> {}; struct Conflict : ErrorImpl<Conflict> {};
@ -81,7 +100,7 @@ struct HostGone : ErrorImpl<HostGone> {};
struct HostUnknown : ErrorImpl<HostUnknown> {}; struct HostUnknown : ErrorImpl<HostUnknown> {};
struct ImproperAdressing : ErrorImpl<ImproperAdressing> {}; struct ImproperAdressing : ErrorImpl<ImproperAdressing> {};
struct InternalServerError : ErrorImpl<InternalServerError> {}; struct InternalServerError : ErrorImpl<InternalServerError> {};
struct InvalidForm : ErrorImpl<InvalidForm> {}; struct InvalidFrom : ErrorImpl<InvalidFrom> {};
struct InvalidNamespace : ErrorImpl<InvalidNamespace> {}; struct InvalidNamespace : ErrorImpl<InvalidNamespace> {};
struct InvalidXml : ErrorImpl<InvalidXml> {}; struct InvalidXml : ErrorImpl<InvalidXml> {};
struct NotAuthorized : ErrorImpl<NotAuthorized> {}; struct NotAuthorized : ErrorImpl<NotAuthorized> {};
@ -109,7 +128,7 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::HostUnknown, error::stream::HostUnknown,
error::stream::ImproperAdressing, error::stream::ImproperAdressing,
error::stream::InternalServerError, error::stream::InternalServerError,
error::stream::InvalidForm, error::stream::InvalidFrom,
error::stream::InvalidNamespace, error::stream::InvalidNamespace,
error::stream::InvalidXml, error::stream::InvalidXml,
error::stream::NotAuthorized, error::stream::NotAuthorized,
@ -123,6 +142,10 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedEncoding, error::stream::UnsupportedEncoding,
error::stream::UnsupportedFeature, error::stream::UnsupportedFeature,
error::stream::UnsupportedStanzaType, 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 } // namespace larra::xmpp

View file

@ -297,7 +297,7 @@ struct RangeToWrapper : T {
template <typename T> template <typename T>
concept LengthCalculatable = requires(const T& obj) { 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>; } || std::convertible_to<T, std::string>;
template <typename T> template <typename T>

View file

@ -1,17 +1,13 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <larra/jid.hpp>
#include <larra/roster.hpp> #include <larra/roster.hpp>
#include "larra/jid.hpp"
namespace larra::xmpp { namespace larra::xmpp {
TEST(Roster, SerializeAndParse) { TEST(Roster, SerializeAndParse) {
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::MakeGetRoster(jid); auto roster = iq::GetRoster{.id = "1", .from = ToString(jid), .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
roster.payload.items.emplace_back("u1", "s1");
roster.payload.items.emplace_back("u2", "s2");
roster.payload.items.emplace_back("u3", "s3");
xmlpp::Document doc; xmlpp::Document doc;
auto node = doc.create_root_node("iq"); 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()); 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)) { 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; 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) { 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 FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::MakeGetRoster(jid); auto roster = iq::GetRoster{.id = "1", .from = ToString(jid), .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
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'; }); EXPECT_NO_THROW({
auto roster_str = ToString(roster.payload);
EXPECT_EQ(kRosterPrintExpectedData.length(), roster_str.capacity());
EXPECT_EQ(kRosterPrintExpectedData, roster_str);
});
} }
} // namespace larra::xmpp } // 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/auto.hpp>
#include <larra/serialization/error.hpp> #include <larra/serialization/error.hpp>
#include <larra/stream_error.hpp> #include <larra/stream_error.hpp>
#include <variant>
#include "utempl/utils.hpp"
using namespace std::literals; using namespace std::literals;