diff --git a/.clang-tidy b/.clang-tidy index f7a36a5d..4612c0ef 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,6 @@ # taken from https://github.com/cpp-linter/cpp-linter-action/blob/main/demo/.clang-tidy --- -Checks: "clang-diagnostic-*,clang-analyzer-*,bugprone-*,misc-*,performance-*,readability-*,portability-*,modernize-*,cppcoreguidelines-*,google-*,llvm-*,cert-*,-modernize-use-trailing-return-type,-bugprone-argument-comment,-misc-include-cleaner,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-readability-avoid-nested-conditional-operator,-llvm-namespace-comment,-llvm-header-guard" +Checks: "clang-diagnostic-*,clang-analyzer-*,bugprone-*,misc-*,performance-*,readability-*,portability-*,modernize-*,cppcoreguidelines-*,google-*,llvm-*,cert-*,-modernize-use-trailing-return-type,-bugprone-argument-comment,-misc-include-cleaner,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-readability-avoid-nested-conditional-operator,-llvm-namespace-comment,-llvm-header-guard,-google-build-explicit-make-pair" WarningsAsErrors: "" HeaderFilterRegex: "oopetris/src/.*" FormatStyle: "file" diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index afb94790..4e440b3f 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -51,7 +51,7 @@ jobs: tidy-checks: '' step-summary: true file-annotations: true - ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library + ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library|src/lobby/curl_client.* - name: Fail CI run if linter checks failed if: steps.linter.outputs.checks-failed != 0 diff --git a/src/helper/graphic_utils.cpp b/src/helper/graphic_utils.cpp index d387f099..d25bb41e 100644 --- a/src/helper/graphic_utils.cpp +++ b/src/helper/graphic_utils.cpp @@ -11,9 +11,7 @@ std::vector utils::supported_features() { std::vector features{}; -#if !defined(_ONLINE_MULTIPLAYER_NOT_SUPPORTED) features.emplace_back("online multiplayer"); -#endif #if defined(_HAVE_FILE_DIALOGS) features.emplace_back("file dialogs"); diff --git a/src/lobby/api.cpp b/src/lobby/api.cpp index 27df24dd..29248eb3 100644 --- a/src/lobby/api.cpp +++ b/src/lobby/api.cpp @@ -2,109 +2,17 @@ #include "api.hpp" +#include -namespace { - - inline helper::expected is_json_response(const httplib::Result& result) { - if (not result->has_header("Content-Type")) { - return helper::unexpected{ "Content-Type not set!" }; - } - - if (const auto value = result->get_header_value("Content-Type"); value != constants::json_content_type) { - return helper::unexpected{ fmt::format("Content-Type is not json but {}", value) }; - } - - return result->body; - } - - - inline helper::expected is_error_message_response(const httplib::Result& result - ) { - - const auto body = is_json_response(result); - if (not body.has_value()) { - return helper::unexpected{ body.error() }; - } - - const auto parsed = json::try_parse_json(body.value()); - - if (parsed.has_value()) { - return parsed.value(); - } - - return helper::unexpected{ fmt::format("Couldn't parse json with error: {}", parsed.error()) }; - } - - inline helper::expected is_request_ok(const httplib::Result& result, int ok_code = 200) { - - if (not result) { - return helper::unexpected{ - fmt::format("Request failed with: {}", httplib::to_string(result.error())) - }; - } - - if (result->status == 401) { - - const auto error_type = is_error_message_response(result); - - if (error_type.has_value()) { - return helper::unexpected{ fmt::format("Unauthorized: {}", error_type.value().message) }; - } - - return helper::unexpected{ "Unauthorized" }; - } - - - if (result->status != ok_code) { - - const auto error_type = is_error_message_response(result); - - if (error_type.has_value()) { - return helper::unexpected{ fmt::format( - "Got error response with status code {}: '{}' and message: {}", result->status, - httplib::status_message(result->status), error_type.value().message - ) }; - } - - - return helper::unexpected{ fmt::format( - "Got error response with status code {}: '{}' but expected {}", result->status, - httplib::status_message(result->status), ok_code - ) }; - } - - return {}; - }; - - - template - helper::expected get_json_from_request(const httplib::Result& result, int ok_code = 200) { - - const auto temp = is_request_ok(result, ok_code); - if (not temp.has_value()) { - return helper::unexpected{ temp.error() }; - } - - const auto body = is_json_response(result); - if (not body.has_value()) { - return helper::unexpected{ body.error() }; - } - - const auto parsed = json::try_parse_json(body.value()); - - if (parsed.has_value()) { - return parsed.value(); - } - - return helper::unexpected{ fmt::format("Couldn't parse json with error: {}", parsed.error()) }; - } - - -} // namespace +#if defined(_OOPETRIS_ONLINE_USE_CURL) +#include "./curl_client.hpp" +#else +#include "./httplib_client.hpp" +#endif -helper::expected lobby::Client::check_compatibility() { +helper::expected lobby::API::check_compatibility() { const auto server_version = get_version(); if (not server_version.has_value()) { @@ -116,74 +24,48 @@ helper::expected lobby::Client::check_compatibility() { const auto& version = server_version.value(); //TODO(Totto): if version is semver, support semver comparison - if (Client::supported_version.string() != version.version) { + if (API::supported_version.string() != version.version) { return helper::unexpected{ fmt::format( "Connecting to unsupported server, version is {}, but we support only {}", - Client::supported_version.string(), version.version + API::supported_version.string(), version.version ) }; } return {}; } -helper::expected lobby::Client::check_reachability() { +helper::expected lobby::API::check_reachability() { - auto result = m_client.Get("/"); + auto result = m_client->Get("/"); - if (not result) { - return helper::unexpected{ - fmt::format("Server not reachable: {}", httplib::to_string(result.error())) - }; + if (auto error = result->get_error(); error.has_value()) { + return helper::unexpected{ fmt::format("Server not reachable: {}", error.value()) }; } return {}; } -lobby::Client::Client(const std::string& api_url) : m_client{ api_url } { +lobby::API::API(const std::string& api_url) + : m_client{ std::make_unique(api_url) } { } - // clang-format off - m_client.set_default_headers({ -#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT) - { "Accept-Encoding", - -#if defined(CPPHTTPLIB_ZLIB_SUPPORT) - "gzip, deflate" -#endif -#if defined(CPPHTTPLIB_ZLIB_SUPPORT) && defined(CPPHTTPLIB_BROTLI_SUPPORT) - ", " -#endif -#if defined(CPPHTTPLIB_BROTLI_SUPPORT) - "br" -#endif - }, -#endif - // clang-format on - { "Accept", constants::json_content_type } }); - -#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT) - m_client.set_compress(true); - m_client.set_decompress(true); -#endif -} - -helper::expected lobby::Client::get_version() { - auto res = m_client.Get("/version"); +helper::expected lobby::API::get_version() { + auto res = m_client->Get("/version"); return get_json_from_request(res); } -lobby::Client::Client(Client&& other) noexcept +lobby::API::API(API&& other) noexcept : m_client{ std::move(other.m_client) }, m_authentication_token{ std::move(other.m_authentication_token) } { } -lobby::Client::~Client() = default; +lobby::API::~API() = default; -helper::expected lobby::Client::get_client(const std::string& url) { +helper::expected lobby::API::get_api(const std::string& url) { - Client client{ url }; + API api{ url }; - const auto reachable = client.check_reachability(); + const auto reachable = api.check_reachability(); if (not reachable.has_value()) { return helper::unexpected{ reachable.error() }; @@ -191,27 +73,31 @@ helper::expected lobby::Client::get_client(const std //TODO(Totto): once version is standard, check here if the version is supported - return client; + return api; } -helper::expected lobby::Client::login(const Credentials& credentials) { - const auto json_result = json::try_json_to_string(credentials); +helper::expected lobby::API::login(const Credentials& credentials) { + auto json_result = json::try_json_to_string(credentials); if (not json_result.has_value()) { return helper::unexpected{ json_result.error() }; } - auto res = m_client.Post("/login", json_result.value(), constants::json_content_type); + auto payload = std::make_pair( + std::move(json_result.value()), ::http::constants::json_content_type + ); + + auto res = m_client->Post("/login", payload); return get_json_from_request(res); } -bool lobby::Client::is_authenticated() { +bool lobby::API::is_authenticated() { return m_authentication_token.has_value(); } -bool lobby::Client::authenticate(const Credentials& credentials) { +bool lobby::API::authenticate(const Credentials& credentials) { const auto result = login(credentials); @@ -223,19 +109,19 @@ bool lobby::Client::authenticate(const Credentials& credentials) { m_authentication_token = result.value().jwt; - m_client.set_bearer_token_auth(m_authentication_token.value()); + m_client->SetBearerAuth(m_authentication_token.value()); return true; } -helper::expected, std::string> lobby::Client::get_lobbies() { - auto res = m_client.Get("/lobbies"); +helper::expected, std::string> lobby::API::get_lobbies() { + auto res = m_client->Get("/lobbies"); return get_json_from_request>(res); } -helper::expected lobby::Client::join_lobby(int lobby_id) { +helper::expected lobby::API::join_lobby(int lobby_id) { if (not is_authenticated()) { return helper::unexpected{ "Authentication needed for this " @@ -243,12 +129,12 @@ helper::expected lobby::Client::join_lobby(int lobby_id) { }; } - auto res = m_client.Post(fmt::format("/lobbies/{}", lobby_id)); + auto res = m_client->Post(fmt::format("/lobbies/{}", lobby_id), std::nullopt); return is_request_ok(res, 204); } -helper::expected lobby::Client::get_lobby_detail(int lobby_id) { +helper::expected lobby::API::get_lobby_detail(int lobby_id) { if (not is_authenticated()) { return helper::unexpected{ "Authentication needed for this " @@ -256,12 +142,12 @@ helper::expected lobby::Client::get_lobby_detai }; } - auto res = m_client.Get(fmt::format("/lobbies/{}", lobby_id)); + auto res = m_client->Get(fmt::format("/lobbies/{}", lobby_id)); return get_json_from_request(res); } -helper::expected lobby::Client::delete_lobby(int lobby_id) { +helper::expected lobby::API::delete_lobby(int lobby_id) { if (not is_authenticated()) { return helper::unexpected{ "Authentication needed for this " @@ -269,12 +155,12 @@ helper::expected lobby::Client::delete_lobby(int lobby_id) { }; } - auto res = m_client.Delete(fmt::format("/lobbies/{}", lobby_id)); + auto res = m_client->Delete(fmt::format("/lobbies/{}", lobby_id)); return is_request_ok(res, 204); } -helper::expected lobby::Client::leave_lobby(int lobby_id) { +helper::expected lobby::API::leave_lobby(int lobby_id) { if (not is_authenticated()) { return helper::unexpected{ "Authentication needed for this " @@ -282,12 +168,12 @@ helper::expected lobby::Client::leave_lobby(int lobby_id) { }; } - auto res = m_client.Put(fmt::format("/lobbies/{}/leave", lobby_id)); + auto res = m_client->Put(fmt::format("/lobbies/{}/leave", lobby_id), std::nullopt); return is_request_ok(res, 204); } -helper::expected lobby::Client::start_lobby(int lobby_id) { +helper::expected lobby::API::start_lobby(int lobby_id) { if (not is_authenticated()) { return helper::unexpected{ "Authentication needed for this " @@ -295,13 +181,12 @@ helper::expected lobby::Client::start_lobby(int lobby_id) { }; } - auto res = m_client.Post(fmt::format("/lobbies/{}/start", lobby_id)); + auto res = m_client->Post(fmt::format("/lobbies/{}/start", lobby_id), std::nullopt); return is_request_ok(res, 204); } -helper::expected lobby::Client::create_lobby( - const CreateLobbyRequest& arguments +helper::expected lobby::API::create_lobby(const CreateLobbyRequest& arguments ) { if (not is_authenticated()) { return helper::unexpected{ @@ -310,30 +195,38 @@ helper::expected lobby::Client::create_ }; } - const auto json_result = json::try_json_to_string(arguments); + auto json_result = json::try_json_to_string(arguments); if (not json_result.has_value()) { return helper::unexpected{ json_result.error() }; } - auto res = m_client.Post("/lobbies", json_result.value(), constants::json_content_type); + auto payload = std::make_pair( + std::move(json_result.value()), ::http::constants::json_content_type + ); + + auto res = m_client->Post("/lobbies", payload); return get_json_from_request(res, 201); } -helper::expected, std::string> lobby::Client::get_users() { +helper::expected, std::string> lobby::API::get_users() { - auto res = m_client.Get("/users"); + auto res = m_client->Get("/users"); return get_json_from_request>(res); } -helper::expected lobby::Client::register_user(const RegisterRequest& register_request) { - const auto json_result = json::try_json_to_string(register_request); +helper::expected lobby::API::register_user(const RegisterRequest& register_request) { + auto json_result = json::try_json_to_string(register_request); if (not json_result.has_value()) { return helper::unexpected{ json_result.error() }; } - auto res = m_client.Post("/register", json_result.value(), constants::json_content_type); + auto payload = std::make_pair( + std::move(json_result.value()), ::http::constants::json_content_type + ); + + auto res = m_client->Post("/register", payload); return is_request_ok(res, 204); } diff --git a/src/lobby/api.hpp b/src/lobby/api.hpp index eca63d03..80ba02ba 100644 --- a/src/lobby/api.hpp +++ b/src/lobby/api.hpp @@ -2,45 +2,19 @@ #pragma once +#include "./client.hpp" -#if defined(__GNUC__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wold-style-cast" -#pragma GCC diagnostic ignored "-Warray-bounds" -#pragma GCC diagnostic ignored "-Wunused-parameter" -#elif defined(_MSC_VER) -#pragma warning(disable : 4100) -#endif - -#define CPPHTTPLIB_USE_POLL // NOLINT(cppcoreguidelines-macro-usage) - -#include - -#if defined(__GNUC__) -#pragma GCC diagnostic pop -#elif defined(_MSC_VER) -#pragma warning(default : 4100) -#endif - -#include -#include -#include - +#include "./types.hpp" #include "helper/windows.hpp" -#include "lobby/types.hpp" -#include -#include -namespace constants { - const constexpr auto json_content_type = "application/json"; -} +#include namespace lobby { - struct Client { + struct API { private: - httplib::Client m_client; + std::unique_ptr m_client; std::optional m_authentication_token; // lobby commit used: https://github.com/OpenBrickProtocolFoundation/lobby/commit/2e0c8d05592f4e4d08437e6cb754a30f02c4e97c @@ -50,7 +24,7 @@ namespace lobby { [[nodiscard]] helper::expected check_reachability(); - explicit Client(const std::string& api_url); + explicit API(const std::string& api_url); helper::expected get_version(); @@ -58,16 +32,16 @@ namespace lobby { public: - OOPETRIS_GRAPHICS_EXPORTED Client(Client&& other) noexcept; - OOPETRIS_GRAPHICS_EXPORTED Client& operator=(Client&& other) noexcept = delete; + OOPETRIS_GRAPHICS_EXPORTED API(API&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED API& operator=(API&& other) noexcept = delete; - OOPETRIS_GRAPHICS_EXPORTED Client(const Client& other) = delete; - OOPETRIS_GRAPHICS_EXPORTED Client& operator=(const Client& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED API(const API& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED API& operator=(const API& other) = delete; - OOPETRIS_GRAPHICS_EXPORTED ~Client(); + OOPETRIS_GRAPHICS_EXPORTED ~API(); OOPETRIS_GRAPHICS_EXPORTED - [[nodiscard]] helper::expected static get_client(const std::string& url); + [[nodiscard]] helper::expected static get_api(const std::string& url); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool is_authenticated(); diff --git a/src/lobby/client.cpp b/src/lobby/client.cpp new file mode 100644 index 00000000..c68fd11c --- /dev/null +++ b/src/lobby/client.cpp @@ -0,0 +1,100 @@ + + +#include "./client.hpp" + + +#if defined(_OOPETRIS_ONLINE_USE_CURL) +#include "./curl_client.hpp" + + +std::string oopetris::http::status_message([[maybe_unused]] int status) { + return "Not Available"; +} + + +#else + +#include "./httplib_client.hpp" + +std::string oopetris::http::status_message(int status) { + return httplib::status_message(status); +} + +#endif + +oopetris::http::Result::~Result() = default; + +oopetris::http::Client::~Client() = default; + +helper::expected oopetris::http::is_json_response( + const std::unique_ptr& result +) { + const auto content_type = result->get_header("Content-Type"); + if (not content_type.has_value()) { + return helper::unexpected{ "Content-Type not set!" }; + } + + if (content_type.value() != ::http::constants::json_content_type) { + return helper::unexpected{ fmt::format("Content-Type is not json but {}", content_type.value()) }; + } + + return result->body(); +} + +helper::expected oopetris::http::is_error_message_response( + const std::unique_ptr& result +) { + + const auto body = is_json_response(result); + if (not body.has_value()) { + return helper::unexpected{ body.error() }; + } + + const auto parsed = json::try_parse_json(body.value()); + + if (parsed.has_value()) { + return parsed.value(); + } + + return helper::unexpected{ fmt::format("Couldn't parse json with error: {}", parsed.error()) }; +} + +helper::expected +oopetris::http::is_request_ok(const std::unique_ptr& result, int ok_code) { + + if (auto error = result->get_error(); error.has_value()) { + return helper::unexpected{ fmt::format("Request failed with: {}", error.value()) }; + } + + if (result->status() == 401) { + + const auto error_type = is_error_message_response(result); + + if (error_type.has_value()) { + return helper::unexpected{ fmt::format("Unauthorized: {}", error_type.value().message) }; + } + + return helper::unexpected{ "Unauthorized" }; + } + + + if (result->status() != ok_code) { + + const auto error_type = is_error_message_response(result); + + if (error_type.has_value()) { + return helper::unexpected{ fmt::format( + "Got error response with status code {}: '{}' and message: {}", result->status(), + oopetris::http::status_message(result->status()), error_type.value().message + ) }; + } + + + return helper::unexpected{ fmt::format( + "Got error response with status code {}: '{}' but expected {}", result->status(), + oopetris::http::status_message(result->status()), ok_code + ) }; + } + + return {}; +}; diff --git a/src/lobby/client.hpp b/src/lobby/client.hpp new file mode 100644 index 00000000..e05f0bab --- /dev/null +++ b/src/lobby/client.hpp @@ -0,0 +1,101 @@ + +#pragma once + + +#include "./constants.hpp" +#include "./types.hpp" +#include "helper/windows.hpp" + +#include +#include +#include +#include + + +#include + + +namespace oopetris::http { + + + struct Result { //NOLINT(cppcoreguidelines-special-member-functions) + OOPETRIS_GRAPHICS_EXPORTED virtual ~Result(); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::optional get_header(const std::string& key + ) const = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::string body() const = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual int status() const = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::optional get_error() const = 0; + }; + + struct Client { //NOLINT(cppcoreguidelines-special-member-functions) + OOPETRIS_GRAPHICS_EXPORTED virtual ~Client(); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::unique_ptr + Get( //NOLINT(readability-identifier-naming) + const std::string& url + ) = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::unique_ptr + Delete( //NOLINT(readability-identifier-naming) + const std::string& url + ) = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::unique_ptr + Post( //NOLINT(readability-identifier-naming) + const std::string& url, + const std::optional>& payload + ) = 0; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] virtual std::unique_ptr + Put( //NOLINT(readability-identifier-naming) + const std::string& url, + const std::optional>& payload + ) = 0; + + OOPETRIS_GRAPHICS_EXPORTED virtual void SetBearerAuth( //NOLINT(readability-identifier-naming) + const std::string& token + ) = 0; + }; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::string status_message(int status); + + OOPETRIS_GRAPHICS_EXPORTED helper::expected is_json_response( + const std::unique_ptr& result + ); + + OOPETRIS_GRAPHICS_EXPORTED helper::expected is_error_message_response( + const std::unique_ptr& result + ); + + OOPETRIS_GRAPHICS_EXPORTED helper::expected + is_request_ok(const std::unique_ptr& result, int ok_code = 200); + + template + helper::expected + get_json_from_request(const std::unique_ptr& result, int ok_code = 200) { + + const auto temp = is_request_ok(result, ok_code); + if (not temp.has_value()) { + return helper::unexpected{ temp.error() }; + } + + const auto body = is_json_response(result); + if (not body.has_value()) { + return helper::unexpected{ body.error() }; + } + + const auto parsed = json::try_parse_json(body.value()); + + if (parsed.has_value()) { + return parsed.value(); + } + + return helper::unexpected{ fmt::format("Couldn't parse json with error: {}", parsed.error()) }; + } + + +} // namespace oopetris::http diff --git a/src/lobby/constants.hpp b/src/lobby/constants.hpp new file mode 100644 index 00000000..7d4f9b6c --- /dev/null +++ b/src/lobby/constants.hpp @@ -0,0 +1,6 @@ + +#pragma once + +namespace http::constants { + const constexpr auto json_content_type = "application/json"; +} diff --git a/src/lobby/curl_client.cpp b/src/lobby/curl_client.cpp new file mode 100644 index 00000000..bc5e7a32 --- /dev/null +++ b/src/lobby/curl_client.cpp @@ -0,0 +1,135 @@ + +#include "./curl_client.hpp" + + +#define TRANSFORM_RESULT(result) std::make_unique((result)) //NOLINT(cppcoreguidelines-macro-usage) + + +oopetris::http::implementation::ActualResult::ActualResult(cpr::Response&& result) : m_result{ std::move(result) } { } + +oopetris::http::implementation::ActualResult::~ActualResult() = default; + +oopetris::http::implementation::ActualResult::ActualResult(ActualResult&& other) noexcept + : m_result{ std::move(other.m_result) } { } + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_header(const std::string& key +) const { + + if (not m_result.header.contains(key)) { + return std::nullopt; + } + + return m_result.header.at(key); +} + +[[nodiscard]] std::string oopetris::http::implementation::ActualResult::body() const { + return m_result.text; +} + +[[nodiscard]] int oopetris::http::implementation::ActualResult::status() const { + return static_cast(m_result.status_code); +} + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_error() const { + + if (static_cast(m_result.error) || m_result.status_code == 0) { + return m_result.error.message; + } + + return std::nullopt; +} + + +namespace { + std::string normalize_url(const std::string& value) { + if (value.ends_with("/")) { + return value.substr(0, value.size() - 1); + } + + return value; + } +} // namespace + +oopetris::http::implementation::ActualClient::ActualClient(ActualClient&& other) noexcept + : m_session{ std::move(other.m_session) } { } + +oopetris::http::implementation::ActualClient::~ActualClient() = default; + + +oopetris::http::implementation::ActualClient::ActualClient(const std::string& api_url) + : m_base_url{ normalize_url(api_url) } { + + m_session->SetUrl(cpr::Url{ api_url }); + m_session->SetAcceptEncoding(cpr::AcceptEncoding{ + { cpr::AcceptEncodingMethods::deflate, cpr::AcceptEncodingMethods::gzip, + cpr::AcceptEncodingMethods::zlib } + }); + m_session->SetHeader(cpr::Header{ + { "Accept", ::http::constants::json_content_type } + }); +} + + +void oopetris::http::implementation::ActualClient::set_url(const std::string& url) { + m_session->SetUrl(cpr::Url{ m_base_url, url }); +} + + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Get( + const std::string& url +) { + set_url(url); + return TRANSFORM_RESULT(m_session->Get()); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Delete( + const std::string& url +) { + set_url(url); + return TRANSFORM_RESULT(m_session->Delete()); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Post( + const std::string& url, + const std::optional>& payload +) { + + set_url(url); + + if (not payload.has_value()) { + return TRANSFORM_RESULT(m_session->Post()); + } + + auto [content, content_type] = payload.value(); + + m_session->SetBody(cpr::Body{ content }); + + return TRANSFORM_RESULT(m_session->Post()); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Put( + const std::string& url, + const std::optional>& payload +) { + set_url(url); + + if (not payload.has_value()) { + return TRANSFORM_RESULT(m_session->Put()); + } + + auto [content, content_type] = payload.value(); + + m_session->SetBody(cpr::Body{ content }); + + return TRANSFORM_RESULT(m_session->Put()); +} + +void oopetris::http::implementation::ActualClient::SetBearerAuth(const std::string& token) { + + +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 + m_session->SetBearer(token); +#else + m_session->SetHeader(cpr::Header{ "Authorization", fmt::format("Bearer {}", token) }); +#endif +} diff --git a/src/lobby/curl_client.hpp b/src/lobby/curl_client.hpp new file mode 100644 index 00000000..1c9abce5 --- /dev/null +++ b/src/lobby/curl_client.hpp @@ -0,0 +1,82 @@ + +#pragma once + + +#include "./client.hpp" + +#if defined(__3DS__) + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#endif + +#include + + +#if defined(__3DS__) +#pragma GCC diagnostic pop +#endif + + +namespace oopetris::http::implementation { + + struct ActualResult : ::oopetris::http::Result { + private: + cpr::Response m_result; + + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit ActualResult(cpr::Response&& result); + + OOPETRIS_GRAPHICS_EXPORTED ~ActualResult() override; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(ActualResult&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(ActualResult&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(const ActualResult& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(const ActualResult& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_header(const std::string& key + ) const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::string body() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] int status() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_error() const override; + }; + + + struct ActualClient : ::oopetris::http::Client { + private: + std::unique_ptr m_session; + std::string m_base_url; + + void set_url(const std::string& url); + + public: + OOPETRIS_GRAPHICS_EXPORTED ActualClient(ActualClient&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(ActualClient&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualClient(const ActualClient& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(const ActualClient& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED ~ActualClient() override; + + OOPETRIS_GRAPHICS_EXPORTED explicit ActualClient(const std::string& api_url); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Get(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Delete(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Post(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Put(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED void SetBearerAuth(const std::string& token) override; + }; + + +} // namespace oopetris::http::implementation diff --git a/src/lobby/httplib_client.cpp b/src/lobby/httplib_client.cpp new file mode 100644 index 00000000..a394d3b0 --- /dev/null +++ b/src/lobby/httplib_client.cpp @@ -0,0 +1,117 @@ + +#include "./httplib_client.hpp" + + +#define TRANSFORM_RESULT(result) std::make_unique((result)) //NOLINT(cppcoreguidelines-macro-usage + + +oopetris::http::implementation::ActualResult::ActualResult(httplib::Result&& result) : m_result{ std::move(result) } { } + +oopetris::http::implementation::ActualResult::~ActualResult() = default; + + +oopetris::http::implementation::ActualResult::ActualResult(ActualResult&& other) noexcept + : m_result{ std::move(other.m_result) } { } + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_header(const std::string& key +) const { + if (m_result->has_header(key)) { + return std::nullopt; + } + + return m_result->get_header_value(key); +} + +[[nodiscard]] std::string oopetris::http::implementation::ActualResult::body() const { + return m_result->body; +} + +[[nodiscard]] int oopetris::http::implementation::ActualResult::status() const { + return m_result->status; +} + +[[nodiscard]] std::optional oopetris::http::implementation::ActualResult::get_error() const { + + if (not m_result) { + return httplib::to_string(m_result.error()); + } + + return std::nullopt; +} + + +oopetris::http::implementation::ActualClient::ActualClient(ActualClient&& other) noexcept + : m_client{ std::move(other.m_client) } { } + +oopetris::http::implementation::ActualClient::~ActualClient() = default; + +oopetris::http::implementation::ActualClient::ActualClient(const std::string& api_url) : m_client{ api_url } { + // clang-format off + m_client.set_default_headers({ +#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT) + { "Accept-Encoding", + +#if defined(CPPHTTPLIB_ZLIB_SUPPORT) + "gzip, deflate" +#endif +#if defined(CPPHTTPLIB_ZLIB_SUPPORT) && defined(CPPHTTPLIB_BROTLI_SUPPORT) + ", " +#endif +#if defined(CPPHTTPLIB_BROTLI_SUPPORT) + "br" +#endif + }, +#endif + // clang-format on + { "Accept", ::http::constants::json_content_type } }); + +#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT) + m_client.set_compress(true); + m_client.set_decompress(true); +#endif +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Get( + const std::string& url +) { + return TRANSFORM_RESULT(m_client.Get(url)); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Delete( + const std::string& url +) { + return TRANSFORM_RESULT(m_client.Delete(url)); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Post( + const std::string& url, + const std::optional>& payload +) { + + if (not payload.has_value()) { + return TRANSFORM_RESULT(m_client.Post(url)); + } + + auto [content, content_type] = payload.value(); + + return TRANSFORM_RESULT(m_client.Post(url, content, content_type)); +} + +[[nodiscard]] std::unique_ptr oopetris::http::implementation::ActualClient::Put( + const std::string& url, + const std::optional>& payload +) { + + if (not payload.has_value()) { + return TRANSFORM_RESULT(m_client.Put(url)); + } + + auto [content, content_type] = payload.value(); + + return TRANSFORM_RESULT(m_client.Put(url, content, content_type)); +} + +void oopetris::http::implementation::ActualClient::SetBearerAuth(const std::string& token) { + + m_client.set_bearer_token_auth(token); +} diff --git a/src/lobby/httplib_client.hpp b/src/lobby/httplib_client.hpp new file mode 100644 index 00000000..c8d1858a --- /dev/null +++ b/src/lobby/httplib_client.hpp @@ -0,0 +1,82 @@ + +#pragma once + +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#elif defined(_MSC_VER) +#pragma warning(disable : 4100) +#endif + +#define CPPHTTPLIB_USE_POLL // NOLINT(cppcoreguidelines-macro-usage) + +#include + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#elif defined(_MSC_VER) +#pragma warning(default : 4100) +#endif + +#include "./client.hpp" + +namespace oopetris::http::implementation { + + struct ActualResult : ::oopetris::http::Result { + private: + httplib::Result m_result; + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit ActualResult(httplib::Result&& result); + + OOPETRIS_GRAPHICS_EXPORTED ~ActualResult() override; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(ActualResult&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(ActualResult&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualResult(const ActualResult& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualResult& operator=(const ActualResult& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_header(const std::string& key + ) const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::string body() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] int status() const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional get_error() const override; + }; + + + struct ActualClient : ::oopetris::http::Client { + private: + httplib::Client m_client; + + public: + OOPETRIS_GRAPHICS_EXPORTED ActualClient(ActualClient&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(ActualClient&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED ActualClient(const ActualClient& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED ActualClient& operator=(const ActualClient& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED ~ActualClient() override; + + OOPETRIS_GRAPHICS_EXPORTED explicit ActualClient(const std::string& api_url); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Get(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr Delete(const std::string& url) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Post(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::unique_ptr + Put(const std::string& url, const std::optional>& payload) override; + + OOPETRIS_GRAPHICS_EXPORTED void SetBearerAuth(const std::string& token) override; + }; + + +} // namespace oopetris::http::implementation diff --git a/src/lobby/meson.build b/src/lobby/meson.build index 6bf0b698..e8632334 100644 --- a/src/lobby/meson.build +++ b/src/lobby/meson.build @@ -1 +1,20 @@ -graphics_src_files += files('api.cpp', 'api.hpp', 'types.hpp') +if online_multiplayer_user_fallback + graphics_src_files += files( + 'curl_client.cpp', + 'curl_client.hpp', + ) +else + graphics_src_files += files( + 'httplib_client.cpp', + 'httplib_client.hpp', + ) +endif + +graphics_src_files += files( + 'api.cpp', + 'api.hpp', + 'client.cpp', + 'client.hpp', + 'constants.hpp', + 'types.hpp', +) diff --git a/src/meson.build b/src/meson.build index 75d0d473..f3f4571a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,10 +11,7 @@ if build_application subdir('manager') subdir('scenes') subdir('ui') - - if online_multiplayer_supported - subdir('lobby') - endif + subdir('lobby') if have_discord_sdk subdir('discord') diff --git a/src/scenes/meson.build b/src/scenes/meson.build index daf05632..80bbad87 100644 --- a/src/scenes/meson.build +++ b/src/scenes/meson.build @@ -10,7 +10,4 @@ subdir('recording_selector') subdir('replay_game') subdir('settings_menu') subdir('single_player_game') - -if online_multiplayer_supported - subdir('online_lobby') -endif +subdir('online_lobby') diff --git a/src/scenes/multiplayer_menu/multiplayer_menu.cpp b/src/scenes/multiplayer_menu/multiplayer_menu.cpp index c4852bde..4f32bd2b 100644 --- a/src/scenes/multiplayer_menu/multiplayer_menu.cpp +++ b/src/scenes/multiplayer_menu/multiplayer_menu.cpp @@ -43,7 +43,7 @@ namespace scenes { ); m_main_grid.get(local_button_id)->disable(); - const auto online_button_id = m_main_grid.add( + m_main_grid.add( service_provider, "Online", service_provider->font_manager().get(FontId::Default), Color::white(), focus_helper.focus_id(), [this](const ui::TextButton&) -> bool { @@ -52,11 +52,6 @@ namespace scenes { }, button_size, button_alignment, button_margins ); -#ifdef _ONLINE_MULTIPLAYER_NOT_SUPPORTED - m_main_grid.get(online_button_id)->disable(); -#else - UNUSED(online_button_id); -#endif const auto ai_button_id = m_main_grid.add( service_provider, "vs AI", service_provider->font_manager().get(FontId::Default), Color::white(), diff --git a/src/scenes/online_lobby/online_lobby.cpp b/src/scenes/online_lobby/online_lobby.cpp index 9f2156a8..97f6670e 100644 --- a/src/scenes/online_lobby/online_lobby.cpp +++ b/src/scenes/online_lobby/online_lobby.cpp @@ -29,11 +29,12 @@ namespace scenes { } { //TODO(Totto): after the settings have been reworked, make this url changeable! - auto maybe_client = lobby::Client::get_client("http://127.0.0.1:5000"); - if (maybe_client.has_value()) { - m_client = std::make_unique(std::move(maybe_client.value())); + auto maybe_api = lobby::API::get_api("http://127.0.0.1:5000"); + if (maybe_api.has_value()) { + m_api = std::make_unique(std::move(maybe_api.value())); } else { - spdlog::error("Error in connecting to lobby client: {}", maybe_client.error()); + spdlog::error("Error in connecting to lobby API: {}", maybe_api.error()); + m_api = nullptr; } auto focus_helper = ui::FocusHelper{ 1 }; diff --git a/src/scenes/online_lobby/online_lobby.hpp b/src/scenes/online_lobby/online_lobby.hpp index 271907cb..6f31428b 100644 --- a/src/scenes/online_lobby/online_lobby.hpp +++ b/src/scenes/online_lobby/online_lobby.hpp @@ -17,7 +17,7 @@ namespace scenes { ui::TileLayout m_main_layout; std::optional m_next_command; - std::unique_ptr m_client{ nullptr }; + std::unique_ptr m_api; public: OOPETRIS_GRAPHICS_EXPORTED explicit OnlineLobby(ServiceProvider* service_provider, const ui::Layout& layout); diff --git a/src/scenes/scene.cpp b/src/scenes/scene.cpp index b1e27f9c..0e2cc551 100644 --- a/src/scenes/scene.cpp +++ b/src/scenes/scene.cpp @@ -1,16 +1,12 @@ -#if !defined(_ONLINE_MULTIPLAYER_NOT_SUPPORTED) -#include "online_lobby/online_lobby.hpp" -#endif - - +#include "scenes/scene.hpp" #include "about_page/about_page.hpp" #include "main_menu/main_menu.hpp" #include "multiplayer_menu/multiplayer_menu.hpp" +#include "online_lobby/online_lobby.hpp" #include "play_select_menu/play_select_menu.hpp" #include "recording_selector/recording_selector.hpp" #include "replay_game/replay_game.hpp" -#include "scenes/scene.hpp" #include "settings_menu/settings_menu.hpp" #include "single_player_game/single_player_game.hpp" @@ -37,10 +33,8 @@ namespace scenes { return std::make_unique(&service_provider, layout); case SceneId::RecordingSelectorMenu: return std::make_unique(&service_provider, layout); -#if !defined(_ONLINE_MULTIPLAYER_NOT_SUPPORTED) case SceneId::OnlineLobby: return std::make_unique(&service_provider, layout); -#endif //TODO(Totto): implement those /* case SceneId::LocalMultiPlayerGame: diff --git a/subprojects/cpr.wrap b/subprojects/cpr.wrap new file mode 100644 index 00000000..3ff2dbdb --- /dev/null +++ b/subprojects/cpr.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = cpr-1.11.0 +source_url = https://github.com/libcpr/cpr/archive/1.11.0.tar.gz +source_filename = cpr-1.11.0.tar.gz +source_hash = fdafa3e3a87448b5ddbd9c7a16e7276a78f28bbe84a3fc6edcfef85eca977784 +patch_filename = cpr_1.11.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/cpr_1.11.0-1/get_patch +patch_hash = 63b94f2524d1d2ab433583c3490d80f1027d68fb7a709e44ee43dbb51f4b3040 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cpr_1.11.0-1/cpr-1.11.0.tar.gz +wrapdb_version = 1.11.0-1 + +[provide] +cpr = cpr_dep diff --git a/subprojects/curl.wrap b/subprojects/curl.wrap new file mode 100644 index 00000000..f7e384b8 --- /dev/null +++ b/subprojects/curl.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = curl-8.10.1 +source_url = https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.1-1/curl-8.10.1.tar.xz +source_filename = curl-8.10.1.tar.xz +source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee +patch_filename = curl_8.10.1-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.1-1/get_patch +patch_hash = 707c28f35fc9b0e8d68c0c2800712007612f922a31da9637ce706a2159f3ddd8 +wrapdb_version = 8.10.1-1 + +[provide] +dependency_names = libcurl diff --git a/tools/dependencies/meson.build b/tools/dependencies/meson.build index c115da32..204bf626 100644 --- a/tools/dependencies/meson.build +++ b/tools/dependencies/meson.build @@ -229,37 +229,40 @@ if build_application } endif - online_multiplayer_supported = true - - if ( - meson.is_cross_build() - and ( - host_machine.system() == 'switch' - or host_machine.system() == '3ds' - ) + cpp_httlib_dep = dependency( + 'cpp-httplib', + required: false, + allow_fallback: true, + default_options: { + 'cpp-httplib_openssl': 'enabled', + 'cpp-httplib_zlib': 'enabled', + }, ) - online_multiplayer_supported = false - # TODO: use libcurl and - # https://github.com/uctakeoff/uc-curl - # or https://github.com/JosephP91/curlcpp + online_multiplayer_user_fallback = false + + if cpp_httlib_dep.found() + + graphics_lib += {'deps': [graphics_lib.get('deps'), cpp_httlib_dep]} + + else + + online_multiplayer_user_fallback = true + + curl_cpp_wrapper = dependency( + 'cpr', + required: true, + default_options: {'tests': 'disabled'}, + ) graphics_lib += { + 'deps': [graphics_lib.get('deps'), curl_cpp_wrapper], 'compile_args': [ graphics_lib.get('compile_args'), - '-D_ONLINE_MULTIPLAYER_NOT_SUPPORTED', + '-D_OOPETRIS_ONLINE_USE_CURL', ], } - else - cpp_httlib_dep = dependency( - 'cpp-httplib', - required: true, - default_options: { - 'cpp-httplib_openssl': 'enabled', - 'cpp-httplib_zlib': 'enabled', - }, - ) - graphics_lib += {'deps': [graphics_lib.get('deps'), cpp_httlib_dep]} + endif build_installer = get_option('build_installer')