diff --git a/.gitmodules b/.gitmodules index 9dfd6df00..9a53460c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,9 @@ [submodule "vcpkg"] path = externals/vcpkg url = https://github.com/microsoft/vcpkg.git +[submodule "cpp-jwt"] + path = externals/cpp-jwt + url = https://github.com/arun11299/cpp-jwt.git [submodule "libadrenotools"] path = externals/libadrenotools url = https://github.com/bylaws/libadrenotools.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 30b8c773a..55b514a10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,8 @@ set(QT6_LOCATION "" CACHE PATH "Additional Location to search for Qt6 libraries option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF) +option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) + option(YUZU_USE_BUNDLED_FFMPEG "Download/Build bundled FFmpeg" "${WIN32}") option(YUZU_USE_EXTERNAL_VULKAN_HEADERS "Use Vulkan-Headers from externals" ON) @@ -141,6 +143,9 @@ if (YUZU_USE_BUNDLED_VCPKG) if (YUZU_TESTS) list(APPEND VCPKG_MANIFEST_FEATURES "yuzu-tests") endif() + if (ENABLE_WEB_SERVICE) + list(APPEND VCPKG_MANIFEST_FEATURES "web-service") + endif() if (ANDROID) list(APPEND VCPKG_MANIFEST_FEATURES "android") endif() @@ -340,7 +345,10 @@ if (USE_DISCORD_PRESENCE) find_package(DiscordRPC MODULE) endif() -find_package(httplib 0.12 MODULE COMPONENTS OpenSSL) +if (ENABLE_WEB_SERVICE) + find_package(cpp-jwt 1.4 CONFIG) + find_package(httplib 0.12 MODULE COMPONENTS OpenSSL) +endif() if (YUZU_TESTS) find_package(Catch2 3.0.1 REQUIRED) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 50d3190e2..2421fc234 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -133,11 +133,19 @@ endif() add_subdirectory(sirit) # httplib -if (NOT TARGET httplib::httplib) +if (ENABLE_WEB_SERVICE AND NOT TARGET httplib::httplib) set(HTTPLIB_REQUIRE_OPENSSL ON) add_subdirectory(cpp-httplib) endif() +# cpp-jwt +if (ENABLE_WEB_SERVICE AND NOT TARGET cpp-jwt::cpp-jwt) + set(CPP_JWT_BUILD_EXAMPLES OFF) + set(CPP_JWT_BUILD_TESTS OFF) + set(CPP_JWT_USE_VENDORED_NLOHMANN_JSON OFF) + add_subdirectory(cpp-jwt) +endif() + # Opus if (NOT TARGET Opus::opus) set(OPUS_BUILD_TESTING OFF) diff --git a/externals/cpp-jwt b/externals/cpp-jwt new file mode 160000 index 000000000..10ef5735d --- /dev/null +++ b/externals/cpp-jwt @@ -0,0 +1 @@ +Subproject commit 10ef5735d842b31025f1257ae78899f50a40fb14 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c4357ecb..8615a0ca8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -208,6 +208,10 @@ if (ENABLE_QT) add_subdirectory(yuzu) endif() +if (ENABLE_WEB_SERVICE) + add_subdirectory(web_service) +endif() + if (ANDROID) add_subdirectory(android/app/src/main/jni) target_include_directories(yuzu-android PRIVATE android/app/src/main) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index cf427ed2a..d67ca881b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1165,6 +1165,11 @@ if (MINGW) target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) endif() +if (ENABLE_WEB_SERVICE) + target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE) + target_link_libraries(core PRIVATE web_service) +endif() + if (HAS_NCE) enable_language(C ASM) set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp") diff --git a/src/dedicated_room/CMakeLists.txt b/src/dedicated_room/CMakeLists.txt index 1d26e00bf..c0dcc0241 100644 --- a/src/dedicated_room/CMakeLists.txt +++ b/src/dedicated_room/CMakeLists.txt @@ -8,6 +8,10 @@ add_executable(yuzu-room ) target_link_libraries(yuzu-room PRIVATE common network) +if (ENABLE_WEB_SERVICE) + target_compile_definitions(yuzu-room PRIVATE -DENABLE_WEB_SERVICE) + target_link_libraries(yuzu-room PRIVATE web_service) +endif() target_link_libraries(yuzu-room PRIVATE mbedtls mbedcrypto) if (MSVC) diff --git a/src/dedicated_room/yuzu_room.cpp b/src/dedicated_room/yuzu_room.cpp index ae483830d..93038f161 100644 --- a/src/dedicated_room/yuzu_room.cpp +++ b/src/dedicated_room/yuzu_room.cpp @@ -33,6 +33,10 @@ #include "network/room.h" #include "network/verify_user.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif + #undef _UNICODE #include #ifndef _MSC_VER @@ -347,9 +351,14 @@ int main(int argc, char** argv) { std::unique_ptr verify_backend; if (announce) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = + std::make_unique(Settings::values.web_api_url.GetValue()); +#else LOG_INFO(Network, "yuzu Web Services is not available with this build: validation is disabled."); verify_backend = std::make_unique(); +#endif } else { verify_backend = std::make_unique(); } diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 47bafee80..8e306219f 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -20,6 +20,10 @@ add_library(network STATIC create_target_directory_groups(network) target_link_libraries(network PRIVATE common enet::enet Boost::headers) +if (ENABLE_WEB_SERVICE) + target_compile_definitions(network PRIVATE -DENABLE_WEB_SERVICE) + target_link_libraries(network PRIVATE web_service) +endif() if (YUZU_USE_PRECOMPILED_HEADERS) target_precompile_headers(network PRIVATE precompiled_headers.h) diff --git a/src/network/announce_multiplayer_session.cpp b/src/network/announce_multiplayer_session.cpp index c61141efd..1f83ae084 100644 --- a/src/network/announce_multiplayer_session.cpp +++ b/src/network/announce_multiplayer_session.cpp @@ -9,6 +9,10 @@ #include "common/assert.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 @@ -16,7 +20,13 @@ static constexpr std::chrono::seconds announce_time_interval(15); AnnounceMultiplayerSession::AnnounceMultiplayerSession(Network::RoomNetwork& room_network_) : room_network{room_network_} { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#else backend = std::make_unique(); +#endif } WebService::WebResult AnnounceMultiplayerSession::Register() { @@ -142,6 +152,12 @@ bool AnnounceMultiplayerSession::IsRunning() const { void AnnounceMultiplayerSession::UpdateCredentials() { ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running"); + +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#endif } } // namespace Core diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index acdf710f7..b67df498f 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -425,6 +425,10 @@ if (USE_DISCORD_PRESENCE) target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) endif() +if (ENABLE_WEB_SERVICE) + target_compile_definitions(yuzu PRIVATE -DENABLE_WEB_SERVICE) +endif() + if (YUZU_USE_QT_MULTIMEDIA) target_link_libraries(yuzu PRIVATE Qt${QT_MAJOR_VERSION}::Multimedia) target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_MULTIMEDIA) diff --git a/src/yuzu/applets/qt_amiibo_settings.cpp b/src/yuzu/applets/qt_amiibo_settings.cpp index f5b423fca..b91796dde 100644 --- a/src/yuzu/applets/qt_amiibo_settings.cpp +++ b/src/yuzu/applets/qt_amiibo_settings.cpp @@ -13,6 +13,9 @@ #include "input_common/drivers/virtual_amiibo.h" #include "input_common/main.h" #include "ui_qt_amiibo_settings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif #include "yuzu/applets/qt_amiibo_settings.h" #include "yuzu/main.h" @@ -88,6 +91,55 @@ void QtAmiiboSettingsDialog::LoadAmiiboInfo() { ui->amiiboInfoGroup->setVisible(false); } +void QtAmiiboSettingsDialog::LoadAmiiboApiInfo(std::string_view amiibo_id) { +#ifdef ENABLE_WEB_SERVICE + // TODO: Host this data on our website + WebService::Client client{"https://amiiboapi.com", {}, {}}; + WebService::Client image_client{"https://raw.githubusercontent.com", {}, {}}; + const auto url_path = fmt::format("/api/amiibo/?id={}", amiibo_id); + + const auto amiibo_json = client.GetJson(url_path, true).returned_data; + if (amiibo_json.empty()) { + ui->amiiboImageLabel->setVisible(false); + ui->amiiboInfoGroup->setVisible(false); + return; + } + + std::string amiibo_series{}; + std::string amiibo_name{}; + std::string amiibo_image_url{}; + std::string amiibo_type{}; + + const auto parsed_amiibo_json_json = nlohmann::json::parse(amiibo_json).at("amiibo"); + parsed_amiibo_json_json.at("amiiboSeries").get_to(amiibo_series); + parsed_amiibo_json_json.at("name").get_to(amiibo_name); + parsed_amiibo_json_json.at("image").get_to(amiibo_image_url); + parsed_amiibo_json_json.at("type").get_to(amiibo_type); + + ui->amiiboSeriesValue->setText(QString::fromStdString(amiibo_series)); + ui->amiiboNameValue->setText(QString::fromStdString(amiibo_name)); + ui->amiiboTypeValue->setText(QString::fromStdString(amiibo_type)); + + if (amiibo_image_url.size() < 34) { + ui->amiiboImageLabel->setVisible(false); + } + + const auto image_url_path = amiibo_image_url.substr(34, amiibo_image_url.size() - 34); + const auto image_data = image_client.GetImage(image_url_path, true).returned_data; + + if (image_data.empty()) { + ui->amiiboImageLabel->setVisible(false); + } + + QPixmap pixmap; + pixmap.loadFromData(reinterpret_cast(image_data.data()), + static_cast(image_data.size())); + pixmap = pixmap.scaled(250, 350, Qt::AspectRatioMode::KeepAspectRatio, + Qt::TransformationMode::SmoothTransformation); + ui->amiiboImageLabel->setPixmap(pixmap); +#endif +} + void QtAmiiboSettingsDialog::LoadAmiiboData() { Service::NFP::RegisterInfo register_info{}; Service::NFP::CommonInfo common_info{}; diff --git a/src/yuzu/applets/qt_amiibo_settings.h b/src/yuzu/applets/qt_amiibo_settings.h index 0cdb86ef3..3833cf6f2 100644 --- a/src/yuzu/applets/qt_amiibo_settings.h +++ b/src/yuzu/applets/qt_amiibo_settings.h @@ -43,6 +43,7 @@ public: private: void LoadInfo(); void LoadAmiiboInfo(); + void LoadAmiiboApiInfo(std::string_view amiibo_id); void LoadAmiiboData(); void LoadAmiiboGameInfo(); void SetGameDataName(u32 application_area_id); diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp index e0b84853c..4463616b4 100644 --- a/src/yuzu/multiplayer/chat_room.cpp +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -21,6 +21,9 @@ #include "yuzu/game_list_p.h" #include "yuzu/multiplayer/chat_room.h" #include "yuzu/multiplayer/message.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif class ChatMessage { public: @@ -384,6 +387,38 @@ void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, member.avatar_url, member.game_info); +#ifdef ENABLE_WEB_SERVICE + if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) { + // Start a request to get the member's avatar + const QUrl url(QString::fromStdString(member.avatar_url)); + QFuture future = QtConcurrent::run([url] { + WebService::Client client( + QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", ""); + auto result = client.GetImage(url.path().toStdString(), true); + if (result.returned_data.empty()) { + LOG_ERROR(WebService, "Failed to get avatar"); + } + return result.returned_data; + }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, + [this, future_watcher, avatar_url = member.avatar_url] { + const std::string result = future_watcher->result(); + if (result.empty()) + return; + QPixmap pixmap; + if (!pixmap.loadFromData(reinterpret_cast(result.data()), + static_cast(result.size()))) + return; + icon_cache[avatar_url] = + pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Update all the displayed icons with the new icon_cache + UpdateIconDisplay(); + }); + future_watcher->setFuture(future); + } +#endif + player_list->invisibleRootItem()->appendRow(name_item); } UpdateIconDisplay(); diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 6ac4693fc..ef364ee43 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -23,6 +23,9 @@ #include "yuzu/multiplayer/state.h" #include "yuzu/multiplayer/validation.h" #include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session, @@ -95,7 +98,12 @@ std::unique_ptr HostRoomWindow::CreateVerifyBacken bool use_validation) const { std::unique_ptr verify_backend; if (use_validation) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = + std::make_unique(Settings::values.web_api_url.GetValue()); +#else verify_backend = std::make_unique(); +#endif } else { verify_backend = std::make_unique(); } @@ -193,6 +201,21 @@ void HostRoomWindow::Host() { } } std::string token; +#ifdef ENABLE_WEB_SERVICE + if (is_public) { + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); + if (auto room = room_network.GetRoom().lock()) { + token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; + } + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif // TODO: Check what to do with this member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, Network::NoPreferredIP, password, token); diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 68ea16d0e..77ac84295 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -20,6 +20,9 @@ #include "yuzu/multiplayer/state.h" #include "yuzu/multiplayer/validation.h" #include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif Lobby::Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session, Core::System& system_) @@ -183,6 +186,20 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { // attempt to connect in a different thread QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] { std::string token; +#ifdef ENABLE_WEB_SERVICE + if (!Settings::values.yuzu_username.GetValue().empty() && + !Settings::values.yuzu_token.GetValue().empty()) { + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); + token = client.GetExternalJWT(verify_uid).returned_data; + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif if (auto room_member = room_network.GetRoomMember().lock()) { room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredIP, password, token);