mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	Add a service to announce multiplayer rooms to web service; Add the abiltiy to receive a list of all announced rooms from web service
This commit is contained in:
		
							parent
							
								
									4b8a7eb1ca
								
							
						
					
					
						commit
						0432fc17eb
					
				
					 14 changed files with 554 additions and 18 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										112
									
								
								src/web_service/announce_room_json.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/web_service/announce_room_json.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| // Copyright 2017 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <future> | ||||
| #include <json.hpp> | ||||
| #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<std::string>(); | ||||
|     member.game_name = json.at("gameName").get<std::string>(); | ||||
|     member.game_id = json.at("gameId").get<u64>(); | ||||
| } | ||||
| 
 | ||||
| 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<std::string>(); | ||||
|     room.name = json.at("name").get<std::string>(); | ||||
|     room.owner = json.at("owner").get<std::string>(); | ||||
|     room.port = json.at("port").get<u16>(); | ||||
|     room.preferred_game = json.at("preferredGameName").get<std::string>(); | ||||
|     room.preferred_game_id = json.at("preferredGameId").get<u64>(); | ||||
|     room.max_player = json.at("maxPlayers").get<u32>(); | ||||
|     room.net_version = json.at("netVersion").get<u32>(); | ||||
|     room.has_password = json.at("hasPassword").get<bool>(); | ||||
|     try { | ||||
|         room.members = json.at("players").get<std::vector<Room::Member>>(); | ||||
|     } 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<Common::WebResult> RoomJson::Announce() { | ||||
|     nlohmann::json json = room; | ||||
|     return PostJson(endpoint_url, json.dump(), false, username, token); | ||||
| } | ||||
| 
 | ||||
| void RoomJson::ClearPlayers() { | ||||
|     room.members.clear(); | ||||
| } | ||||
| 
 | ||||
| std::future<AnnounceMultiplayerRoom::RoomList> RoomJson::GetRoomList(std::function<void()> 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<AnnounceMultiplayerRoom::RoomList>(); | ||||
|         func(); | ||||
|         return room_list; | ||||
|     }; | ||||
|     return GetJson<AnnounceMultiplayerRoom::RoomList>(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
 | ||||
							
								
								
									
										42
									
								
								src/web_service/announce_room_json.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/web_service/announce_room_json.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <functional> | ||||
| #include <future> | ||||
| #include <string> | ||||
| #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<Common::WebResult> Announce() override; | ||||
|     void ClearPlayers() override; | ||||
|     std::future<AnnounceMultiplayerRoom::RoomList> GetRoomList(std::function<void()> func) override; | ||||
|     void Delete() override; | ||||
| 
 | ||||
| private: | ||||
|     AnnounceMultiplayerRoom::Room room; | ||||
|     std::string endpoint_url; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
| }; | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  | @ -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<Common::WebResult> future = | ||||
|         PostJson(endpoint_url, TopSection().dump(), true, username, token); | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ | |||
| #include <cstdlib> | ||||
| #include <thread> | ||||
| #include <cpr/cpr.h> | ||||
| #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<Common::WebResult> 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<void> 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<u32>(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<T> GetJson(std::function<T(const std::string&)> 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<u32>(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<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
| template std::future<bool> GetJson(std::function<bool(const std::string&)> func, | ||||
|                                    const std::string& url, bool allow_anonymous, | ||||
|                                    const std::string& username, const std::string& token); | ||||
| template std::future<AnnounceMultiplayerRoom::RoomList> GetJson( | ||||
|     std::function<AnnounceMultiplayerRoom::RoomList(const std::string&)> 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<void> 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<u32>(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
 | ||||
|  |  | |||
|  | @ -7,6 +7,8 @@ | |||
| #include <functional> | ||||
| #include <future> | ||||
| #include <string> | ||||
| #include <tuple> | ||||
| #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<Common::WebResult> 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<T> GetJson(std::function<T(const std::string&)> 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
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue