From 0432fc17eb5dba2189157e337da12341ee472606 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Tue, 31 Oct 2017 10:02:42 +0100 Subject: [PATCH] Add a service to announce multiplayer rooms to web service; Add the abiltiy to receive a list of all announced rooms from web service --- src/citra/config.cpp | 3 + src/citra/default_ini.h | 2 + src/common/CMakeLists.txt | 1 + src/common/announce_multiplayer_room.h | 105 +++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/announce_multiplayer_session.cpp | 120 ++++++++++++++++++++++ src/core/announce_multiplayer_session.h | 71 +++++++++++++ src/core/settings.h | 1 + src/web_service/CMakeLists.txt | 2 + src/web_service/announce_room_json.cpp | 112 ++++++++++++++++++++ src/web_service/announce_room_json.h | 42 ++++++++ src/web_service/telemetry_json.cpp | 5 +- src/web_service/web_backend.cpp | 88 +++++++++++++--- src/web_service/web_backend.h | 18 +++- 14 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 src/common/announce_multiplayer_room.h create mode 100644 src/core/announce_multiplayer_session.cpp create mode 100644 src/core/announce_multiplayer_session.h create mode 100644 src/web_service/announce_room_json.cpp create mode 100644 src/web_service/announce_room_json.h diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 45c28ad09..e4c23cc9b 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -164,6 +164,9 @@ void Config::ReadValues() { "WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry"); Settings::values.verify_endpoint_url = sdl2_config->Get( "WebService", "verify_endpoint_url", "https://services.citra-emu.org/api/profile"); + Settings::values.announce_multiplayer_room_endpoint_url = + sdl2_config->Get("WebService", "announce_multiplayer_room_endpoint_url", + "https://services.citra-emu.org/api/multiplayer/rooms"); Settings::values.citra_username = sdl2_config->Get("WebService", "citra_username", ""); Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", ""); } diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 7bac9474d..7ce5b7e32 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -187,6 +187,8 @@ enable_telemetry = telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry # Endpoint URL to verify the username and token verify_endpoint_url = https://services.citra-emu.org/api/profile +# Endpoint URL for announcing public rooms +announce_multiplayer_room_endpoint_url = https://services.citra-emu.org/api/multiplayer/rooms # Username and token for Citra Web Service # See https://services.citra-emu.org/ for more info citra_username = diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bdec3c43f..e0f5f4906 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -26,6 +26,7 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOU add_library(common STATIC alignment.h + announce_multiplayer_room.h assert.h bit_field.h bit_set.h diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h new file mode 100644 index 000000000..519b7050b --- /dev/null +++ b/src/common/announce_multiplayer_room.h @@ -0,0 +1,105 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_types.h" + +namespace Common { +struct WebResult { + enum Code : u32 { + Success, + InvalidURL, + CredentialsMissing, + CprError, + HttpError, + WrongContent, + NoWebservice, + }; + Code result_code; + std::string result_string; +}; +} // namespace Common + +namespace AnnounceMultiplayerRoom { + +using MacAddress = std::array; + +struct Room { + struct Member { + std::string name; + MacAddress mac_address; + std::string game_name; + u64 game_id; + }; + std::string name; + std::string GUID; + std::string owner; + std::string ip; + u16 port; + u32 max_player; + u32 net_version; + bool has_password; + std::string preferred_game; + u64 preferred_game_id; + + std::vector members; +}; +using RoomList = std::vector; + +/** + * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should + * implement this interface. + */ +class Backend : NonCopyable { +public: + virtual ~Backend() = default; + virtual void SetRoomInformation(const std::string& guid, const std::string& name, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) = 0; + virtual void AddPlayer(const std::string& nickname, const MacAddress& mac_address, + const u64 game_id, const std::string& game_name) = 0; + virtual std::future Announce() = 0; + virtual void ClearPlayers() = 0; + virtual std::future GetRoomList(std::function func) = 0; + virtual void Delete() = 0; +}; + +/** + * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a + * functional backend implementation is not available. + */ +class NullBackend : public Backend { +public: + ~NullBackend() = default; + void SetRoomInformation(const std::string& /*guid*/, const std::string& /*name*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const bool /*has_password*/, const std::string& /*preferred_game*/, + const u64 /*preferred_game_id*/) override {} + void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, + const u64 /*game_id*/, const std::string& /*game_name*/) override {} + std::future Announce() override { + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::NoWebservice, + "WebService is missing"}; + }); + } + void ClearPlayers() override {} + std::future GetRoomList(std::function func) override { + return std::async(std::launch::async, [func]() { + func(); + return RoomList{}; + }); + } + + void Delete() override {} +}; + +} // namespace AnnounceMultiplayerRoom diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8d22bd1a..192d5375e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(core STATIC 3ds.h + announce_multiplayer_session.cpp + announce_multiplayer_session.h arm/arm_interface.h arm/dynarmic/arm_dynarmic.cpp arm/dynarmic/arm_dynarmic.h diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp new file mode 100644 index 000000000..b4b732c7c --- /dev/null +++ b/src/core/announce_multiplayer_session.cpp @@ -0,0 +1,120 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "announce_multiplayer_session.h" +#include "common/announce_multiplayer_room.h" +#include "common/assert.h" +#include "core/settings.h" +#include "network/network.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/announce_room_json.h" +#endif + +namespace Core { + +// Time between room is announced to web_service +static constexpr std::chrono::seconds announce_time_interval(15); + +AnnounceMultiplayerSession::AnnounceMultiplayerSession() : announce(false), finished(true) { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique( + Settings::values.announce_multiplayer_room_endpoint_url, Settings::values.citra_username, + Settings::values.citra_token); +#else + backend = std::make_unique(); +#endif +} + +void AnnounceMultiplayerSession::Start() { + if (announce_multiplayer_thread) { + Stop(); + } + + announce_multiplayer_thread = + std::make_unique(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); +} + +void AnnounceMultiplayerSession::Stop() { + if (!announce && finished) + return; + announce = false; + // Detaching the loop, to not wait for the sleep to finish. The loop thread will finish soon. + if (announce_multiplayer_thread) { + announce_multiplayer_thread->detach(); + announce_multiplayer_thread.reset(); + backend->Delete(); + } +} + +std::shared_ptr> +AnnounceMultiplayerSession::BindErrorCallback( + std::function function) { + std::lock_guard lock(callback_mutex); + auto handle = std::make_shared>(function); + error_callbacks.insert(handle); + return handle; +} + +void AnnounceMultiplayerSession::UnbindErrorCallback( + std::shared_ptr> handle) { + std::lock_guard lock(callback_mutex); + error_callbacks.erase(handle); +} + +AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { + Stop(); +} + +void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + while (!finished) { + std::this_thread::sleep_for(announce_time_interval / 10); + } + announce = true; + finished = false; + std::future future; + while (announce) { + if (std::shared_ptr room = Network::GetRoom().lock()) { + if (room->GetState() == Network::Room::State::Open) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.guid, room_information.name, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, + member.game_info.name); + } + future = backend->Announce(); + } else { + announce = false; + } + } else { + announce = false; + } + if (future.valid()) { + Common::WebResult result = future.get(); + if (result.result_code != Common::WebResult::Success) { + std::lock_guard lock(callback_mutex); + for (auto callback : error_callbacks) { + (*callback)(result); + } + announce = false; + } + } + std::this_thread::sleep_for(announce_time_interval); + } + finished = true; +} + +std::future AnnounceMultiplayerSession::GetRoomList( + std::function func) { + return backend->GetRoomList(func); +} + +} // namespace Core diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h new file mode 100644 index 000000000..a7a36b82d --- /dev/null +++ b/src/core/announce_multiplayer_session.h @@ -0,0 +1,71 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" + +namespace Core { + +/** + * Instruments AnnounceMultiplayerRoom::Backend. + * Creates a thread that regularly updates the room information and submits them + * An async get of room information is also possible + */ +class AnnounceMultiplayerSession : NonCopyable { +public: + AnnounceMultiplayerSession(); + ~AnnounceMultiplayerSession(); + + /** + * Allows to bind a function that will get called if the announce encounters an error + * @param function The function that gets called + * @return A handle that can be used the unbind the function + */ + std::shared_ptr> BindErrorCallback( + std::function function); + + /** + * Unbind a function from the error callbacks + * @param handle The handle for the function that should get unbind + */ + void UnbindErrorCallback(std::shared_ptr> handle); + + /** + * Starts the announce of a room to web services + */ + void Start(); + + /** + * Stops the announce to web services + */ + void Stop(); + + /** + * Returns a list of all room information the backend got + * @param func A function that gets executed when the async get finished, e.g. a signal + * @return a list of rooms received from the web service + */ + std::future GetRoomList(std::function func); + +private: + std::atomic announce{false}; + std::atomic finished{true}; + std::mutex callback_mutex; + std::set>> error_callbacks; + std::unique_ptr announce_multiplayer_thread; + + std::unique_ptr + backend; ///< Backend interface that logs fields + + void AnnounceMultiplayerLoop(); +}; + +} // namespace Core diff --git a/src/core/settings.h b/src/core/settings.h index 8d78cb424..9657f7f8b 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -134,6 +134,7 @@ struct Values { bool enable_telemetry; std::string telemetry_endpoint_url; std::string verify_endpoint_url; + std::string announce_multiplayer_room_endpoint_url; std::string citra_username; std::string citra_token; } extern values; diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index b6de2632b..911fbe26b 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(web_service STATIC + announce_room_json.cpp + announce_room_json.h telemetry_json.cpp telemetry_json.h verify_login.cpp diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp new file mode 100644 index 000000000..b8af09720 --- /dev/null +++ b/src/web_service/announce_room_json.cpp @@ -0,0 +1,112 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "web_service/announce_room_json.h" +#include "web_service/web_backend.h" + +namespace AnnounceMultiplayerRoom { + +void to_json(nlohmann::json& json, const Room::Member& member) { + json["name"] = member.name; + json["gameName"] = member.game_name; + json["gameId"] = member.game_id; +} + +void from_json(const nlohmann::json& json, Room::Member& member) { + member.name = json.at("name").get(); + member.game_name = json.at("gameName").get(); + member.game_id = json.at("gameId").get(); +} + +void to_json(nlohmann::json& json, const Room& room) { + json["id"] = room.GUID; + json["port"] = room.port; + json["name"] = room.name; + json["preferredGameName"] = room.preferred_game; + json["preferredGameId"] = room.preferred_game_id; + json["maxPlayers"] = room.max_player; + json["netVersion"] = room.net_version; + json["hasPassword"] = room.has_password; + if (room.members.size() > 0) { + nlohmann::json member_json = room.members; + json["players"] = member_json; + } +} + +void from_json(const nlohmann::json& json, Room& room) { + room.ip = json.at("address").get(); + room.name = json.at("name").get(); + room.owner = json.at("owner").get(); + room.port = json.at("port").get(); + room.preferred_game = json.at("preferredGameName").get(); + room.preferred_game_id = json.at("preferredGameId").get(); + room.max_player = json.at("maxPlayers").get(); + room.net_version = json.at("netVersion").get(); + room.has_password = json.at("hasPassword").get(); + try { + room.members = json.at("players").get>(); + } catch (const nlohmann::detail::out_of_range& e) { + LOG_DEBUG(Network, "Out of range %s", e.what()); + } +} + +} // namespace AnnounceMultiplayerRoom + +namespace WebService { + +void RoomJson::SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, + const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) { + room.name = name; + room.GUID = guid; + room.port = port; + room.max_player = max_player; + room.net_version = net_version; + room.has_password = has_password; + room.preferred_game = preferred_game; + room.preferred_game_id = preferred_game_id; +} +void RoomJson::AddPlayer(const std::string& nickname, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) { + AnnounceMultiplayerRoom::Room::Member member; + member.name = nickname; + member.mac_address = mac_address; + member.game_id = game_id; + member.game_name = game_name; + room.members.push_back(member); +} + +std::future RoomJson::Announce() { + nlohmann::json json = room; + return PostJson(endpoint_url, json.dump(), false, username, token); +} + +void RoomJson::ClearPlayers() { + room.members.clear(); +} + +std::future RoomJson::GetRoomList(std::function func) { + auto DeSerialize = [func](const std::string& reply) -> AnnounceMultiplayerRoom::RoomList { + nlohmann::json json = nlohmann::json::parse(reply); + AnnounceMultiplayerRoom::RoomList room_list = + json.at("rooms").get(); + func(); + return room_list; + }; + return GetJson(DeSerialize, endpoint_url, true, username, + token); +} + +void RoomJson::Delete() { + nlohmann::json json; + json["id"] = room.GUID; + DeleteJson(endpoint_url, json.dump(), username, token); +} + +} // namespace WebService diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h new file mode 100644 index 000000000..907a76b3a --- /dev/null +++ b/src/web_service/announce_room_json.h @@ -0,0 +1,42 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/announce_multiplayer_room.h" + +namespace WebService { + +/** + * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from + * JSON, and submits/gets it to/from the Citra web service + */ +class RoomJson : public AnnounceMultiplayerRoom::Backend { +public: + RoomJson(const std::string& endpoint_url, const std::string& username, const std::string& token) + : endpoint_url(endpoint_url), username(username), token(token) {} + ~RoomJson() = default; + void SetRoomInformation(const std::string& guid, const std::string& name, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, + const std::string& preferred_game, + const u64 preferred_game_id) override; + void AddPlayer(const std::string& nickname, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) override; + std::future Announce() override; + void ClearPlayers() override; + std::future GetRoomList(std::function func) override; + void Delete() override; + +private: + AnnounceMultiplayerRoom::Room room; + std::string endpoint_url; + std::string username; + std::string token; +}; + +} // namespace WebService diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp index 6ad2ffcd4..642c2617a 100644 --- a/src/web_service/telemetry_json.cpp +++ b/src/web_service/telemetry_json.cpp @@ -80,7 +80,10 @@ void TelemetryJson::Complete() { SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback"); SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); - PostJson(endpoint_url, TopSection().dump(), true, username, token); + + // Send the telemetry async but don't handle the errors since the were written to the log + static std::future future = + PostJson(endpoint_url, TopSection().dump(), true, username, token); } } // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index b17d82f9c..2b0f5a482 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "common/announce_multiplayer_room.h" #include "common/logging/log.h" #include "web_service/web_backend.h" @@ -31,17 +32,23 @@ void Win32WSAStartup() { #endif } -void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, - const std::string& username, const std::string& token) { +std::future PostJson(const std::string& url, const std::string& data, + bool allow_anonymous, const std::string& username, + const std::string& token) { if (url.empty()) { LOG_ERROR(WebService, "URL is invalid"); - return; + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + }); } const bool are_credentials_provided{!token.empty() && !username.empty()}; if (!allow_anonymous && !are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); - return; + return std::async(std::launch::async, []() { + return Common::WebResult{Common::WebResult::Code::CredentialsMissing, + "Credentials needed"}; + }); } Win32WSAStartup(); @@ -60,23 +67,26 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym } // Post JSON asynchronously - static std::future future; - future = cpr::PostCallback( + return cpr::PostCallback( [](cpr::Response r) { if (r.error) { - LOG_ERROR(WebService, "POST returned cpr error: %u:%s", + LOG_ERROR(WebService, "POST to %s returned cpr error: %u:%s", r.url.c_str(), static_cast(r.error.code), r.error.message.c_str()); - return; + return Common::WebResult{Common::WebResult::Code::CprError, r.error.message}; } if (r.status_code >= 400) { - LOG_ERROR(WebService, "POST returned error status code: %u", r.status_code); - return; + LOG_ERROR(WebService, "POST to %s returned error status code: %u", r.url.c_str(), + r.status_code); + return Common::WebResult{Common::WebResult::Code::HttpError, + std::to_string(r.status_code)}; } if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "POST returned wrong content: %s", + LOG_ERROR(WebService, "POST to %s returned wrong content: %s", r.url.c_str(), r.header["content-type"].c_str()); - return; + return Common::WebResult{Common::WebResult::Code::WrongContent, + r.header["content-type"]}; } + return Common::WebResult{Common::WebResult::Code::Success, ""}; }, cpr::Url{url}, cpr::Body{data}, header); } @@ -115,16 +125,17 @@ std::future GetJson(std::function func, const std::str return cpr::GetCallback( [func{std::move(func)}](cpr::Response r) { if (r.error) { - LOG_ERROR(WebService, "GET returned cpr error: %u:%s", + LOG_ERROR(WebService, "GET to %s returned cpr error: %u:%s", r.url.c_str(), static_cast(r.error.code), r.error.message.c_str()); return func(""); } if (r.status_code >= 400) { - LOG_ERROR(WebService, "GET returned error code: %u", r.status_code); + LOG_ERROR(WebService, "GET to %s returned error code: %u", r.url.c_str(), + r.status_code); return func(""); } if (r.header["content-type"].find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "GET returned wrong content: %s", + LOG_ERROR(WebService, "GET to %s returned wrong content: %s", r.url.c_str(), r.header["content-type"].c_str()); return func(""); } @@ -136,5 +147,52 @@ std::future GetJson(std::function func, const std::str template std::future GetJson(std::function func, const std::string& url, bool allow_anonymous, const std::string& username, const std::string& token); +template std::future GetJson( + std::function func, + const std::string& url, bool allow_anonymous, const std::string& username, + const std::string& token); + +void DeleteJson(const std::string& url, const std::string& data, const std::string& username, + const std::string& token) { + if (url.empty()) { + LOG_ERROR(WebService, "URL is invalid"); + return; + } + + if (token.empty() || username.empty()) { + LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); + return; + } + + Win32WSAStartup(); + + // Built request header + cpr::Header header = {{"Content-Type", "application/json"}, + {"x-username", username.c_str()}, + {"x-token", token.c_str()}, + {"api-version", API_VERSION}}; + + // Delete JSON asynchronously + static std::future future; + future = cpr::DeleteCallback( + [](cpr::Response r) { + if (r.error) { + LOG_ERROR(WebService, "Delete to %s returned cpr error: %u:%s", r.url.c_str(), + static_cast(r.error.code), r.error.message.c_str()); + return; + } + if (r.status_code >= 400) { + LOG_ERROR(WebService, "Delete to %s returned error status code: %u", r.url.c_str(), + r.status_code); + return; + } + if (r.header["content-type"].find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "Delete to %s returned wrong content: %s", r.url.c_str(), + r.header["content-type"].c_str()); + return; + } + }, + cpr::Url{url}, cpr::Body{data}, header); +} } // namespace WebService diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index a63c75d13..29aab00cb 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" namespace WebService { @@ -18,9 +20,11 @@ namespace WebService { * @param allow_anonymous If true, allow anonymous unauthenticated requests. * @param username Citra username to use for authentication. * @param token Citra token to use for authentication. + * @return future with the error or result of the POST */ -void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, - const std::string& username = {}, const std::string& token = {}); +std::future PostJson(const std::string& url, const std::string& data, + bool allow_anonymous, const std::string& username = {}, + const std::string& token = {}); /** * Gets JSON from services.citra-emu.org. @@ -36,4 +40,14 @@ std::future GetJson(std::function func, const std::str bool allow_anonymous, const std::string& username = {}, const std::string& token = {}); +/** + * Delete JSON to services.citra-emu.org. + * @param url URL of the services.citra-emu.org endpoint to post data to. + * @param data String of JSON data to use for the body of the DELETE request. + * @param username Citra username to use for authentication. + * @param token Citra token to use for authentication. + */ +void DeleteJson(const std::string& url, const std::string& data, const std::string& username = {}, + const std::string& token = {}); + } // namespace WebService