From 386bf5c861cdc74fc0874bf9da64da515deb27d9 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:49:00 +0800 Subject: [PATCH] citra_qt: Use the new verify backend; UI changes Displayed username along with nickname (when they are not identical); Requested and displayed user's avatar; Made the dialog bigger for extended names. Added a few functions to web_backend (GetImage, GetPlain) to support getting data in multiple content-types. Added a no_avatar icon for users without avatars. --- dist/license.md | 2 + dist/qt_themes/colorful_dark/style.qrc | 1 + dist/qt_themes/default/default.qrc | 2 + .../default/icons/48x48/no_avatar.png | Bin 0 -> 588 bytes .../qdarkstyle/icons/48x48/no_avatar.png | Bin 0 -> 708 bytes dist/qt_themes/qdarkstyle/style.qrc | 1 + src/citra_qt/CMakeLists.txt | 4 + src/citra_qt/multiplayer/chat_room.cpp | 133 +++++++++++++++--- src/citra_qt/multiplayer/chat_room.h | 3 + src/citra_qt/multiplayer/chat_room.ui | 2 +- src/citra_qt/multiplayer/client_room.ui | 2 +- src/citra_qt/multiplayer/host_room.cpp | 74 +++++++--- src/citra_qt/multiplayer/host_room.h | 12 +- src/citra_qt/multiplayer/lobby.cpp | 27 +++- src/citra_qt/multiplayer/lobby_p.h | 23 +-- src/web_service/verify_user_jwt.cpp | 2 +- src/web_service/web_backend.cpp | 39 +++-- src/web_service/web_backend.h | 16 +++ 18 files changed, 263 insertions(+), 80 deletions(-) create mode 100644 dist/qt_themes/default/icons/48x48/no_avatar.png create mode 100644 dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png diff --git a/dist/license.md b/dist/license.md index c469e21f1..f7f74ab5a 100644 --- a/dist/license.md +++ b/dist/license.md @@ -11,6 +11,7 @@ qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8. qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/checked.png | Free for non-commercial use @@ -22,6 +23,7 @@ qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icon qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/qdarkstyle/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc index 4b955998e..00a7598fe 100644 --- a/dist/qt_themes/colorful_dark/style.qrc +++ b/dist/qt_themes/colorful_dark/style.qrc @@ -7,6 +7,7 @@ ../colorful/icons/48x48/bad_folder.png ../colorful/icons/48x48/chip.png ../colorful/icons/48x48/folder.png + ../qdarkstyle/icons/48x48/no_avatar.png ../colorful/icons/48x48/plus.png ../colorful/icons/48x48/sd_card.png ../colorful/icons/256x256/plus_folder.png diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index 974079e78..4840532a2 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -18,6 +18,8 @@ icons/48x48/folder.png + icons/48x48/no_avatar.png + icons/48x48/plus.png icons/48x48/sd_card.png diff --git a/dist/qt_themes/default/icons/48x48/no_avatar.png b/dist/qt_themes/default/icons/48x48/no_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..d4bf82026a63d3498c57615ed744ef6ad1a11db4 GIT binary patch literal 588 zcmV-S0<-;zP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0pUqRK~!i%?bts^ z)L|UQ@k_%$K?EW+2x>Uh5Tcv9ID|t(AUGrhij56zIR}A+gAO9Jr08b!2O=6m2Zt72 zf~GJKrJ<=1OR>`Tcl&XCxO<+v^S%%9d&6t}-N*C({O&2_<>l49c1+H(cMu9ry_~F2o$%>&5}pz)}{_h$OO) zLp8FLVI))R7uq*mL^55%F4S1%5t8c`j-du;eT4mvPO?u>qqAIZpUbrkH99NlKOp2k za39IG12sD9A(E^gf1yTa*^%P~B-H@+pav_?k;D;W4;B&f%onro4Lgyj`$wvsFyF>I zR5HBScRWES&NX|c9^fm~kZReX?+f&yrr8^A68lgOTx)jR_zGR9Iy?7YgLmcf a6$)pIccPOKD6@6|0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0$E8!K~!i%?U+4C z98nlWS0ieKAc&Ach>BHGNYF0YqzD!%gn)&DplF%GF4lqk-Z(wLx~!5;{cLSkWI zlQe-;q9BTeRU~K>b=Nb#TM5I?JlFRgG6yc>;`{D*#@}Uf85tR+rBEo;=kxg?@`?Q5 z^-r#wvA0p3&g6)M?Qe5o~yzyh)l{YEq|GP zp&CCRQH#Wx@ktM}>#Fht4n0Q24sIagsp{Or@@ctPLr0hD+{5w(d00b7zv|q>@^A96 zhK@efxrhCqjfOqkK)34L!}1Av+=Z8_a}PH#Bnc~+KjBiTI`<&ZYEtG4KGTZ(fhEyH-1lVHtS&9|Mh|_zk~Xa@4Rn`LI!F%8s{4gK5zC}e3ii-fP0^yM zb_;utiTz_oI-;NTRa55&_MejuDW!w?0aN1_ZeW|F|4YTtA9Z*KmtJWlgMntv1_3?` qQcDIioeb>+lqIQUWMn)Xa=B}!H(avGT(DUH0000icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/no_avatar.png icons/48x48/plus.png icons/48x48/sd_card.png icons/256x256/plus_folder.png diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 4bb95a7f2..12affbd85 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -228,6 +228,10 @@ if (USE_DISCORD_PRESENCE) target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE) endif() +if (ENABLE_WEB_SERVICE) + target_compile_definitions(citra-qt PRIVATE -DENABLE_WEB_SERVICE) +endif() + if(UNIX AND NOT APPLE) install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 50bc5eb03..357fdca39 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include "citra_qt/game_list_p.h" #include "citra_qt/multiplayer/chat_room.h" @@ -19,6 +21,9 @@ #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" #include "ui_chat_room.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif class ChatMessage { public: @@ -27,14 +32,21 @@ public: QLocale locale; timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); nickname = QString::fromStdString(chat.nickname); + username = QString::fromStdString(chat.username); message = QString::fromStdString(chat.message); } /// Format the message using the players color QString GetPlayerChatMessage(u16 player) const { auto color = player_color[player % 16]; + QString name; + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QString("%1 (%2)").arg(nickname, username); + } return QString("[%1] <%3> %4") - .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped()); + .arg(timestamp, color, name.toHtmlEscaped(), message.toHtmlEscaped()); } private: @@ -44,6 +56,7 @@ private: QString timestamp; QString nickname; + QString username; QString message; }; @@ -67,22 +80,54 @@ private: QString message; }; +class PlayerListItem : public QStandardItem { +public: + static const int NicknameRole = Qt::UserRole + 1; + static const int UsernameRole = Qt::UserRole + 2; + static const int AvatarUrlRole = Qt::UserRole + 3; + static const int GameNameRole = Qt::UserRole + 4; + + PlayerListItem() = default; + explicit PlayerListItem(const std::string& nickname, const std::string& username, + const std::string& avatar_url, const std::string& game_name) { + setEditable(false); + setData(QString::fromStdString(nickname), NicknameRole); + setData(QString::fromStdString(username), UsernameRole); + setData(QString::fromStdString(avatar_url), AvatarUrlRole); + if (game_name.empty()) { + setData(QObject::tr("Not playing a game"), GameNameRole); + } else { + setData(QString::fromStdString(game_name), GameNameRole); + } + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return QStandardItem::data(role); + } + QString name; + const QString nickname = data(NicknameRole).toString(); + const QString username = data(UsernameRole).toString(); + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QString("%1 (%2)").arg(nickname, username); + } + return QString("%1\n %2").arg(name, data(GameNameRole).toString()); + } +}; + ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { ui->setupUi(this); // set the item_model for player_view - enum { - COLUMN_NAME, - COLUMN_GAME, - COLUMN_COUNT, // Number of columns - }; player_list = new QStandardItemModel(ui->player_view); ui->player_view->setModel(player_list); ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); - player_list->insertColumns(0, COLUMN_COUNT); - player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); - player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game")); + // set a header to make it look better though there is only one column + player_list->insertColumns(0, 1); + player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); @@ -157,7 +202,8 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { auto members = room->GetMemberInformation(); auto it = std::find_if(members.begin(), members.end(), [&chat](const Network::RoomMember::MemberInformation& member) { - return member.nickname == chat.nickname; + return member.nickname == chat.nickname && + member.username == chat.username; }); if (it == members.end()) { LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); @@ -184,12 +230,14 @@ void ChatRoom::OnSendChat() { return; } auto nick = room->GetNickname(); - Network::ChatEntry chat{nick, message}; + auto username = room->GetUsername(); + Network::ChatEntry chat{nick, username, message}; auto members = room->GetMemberInformation(); auto it = std::find_if(members.begin(), members.end(), [&chat](const Network::RoomMember::MemberInformation& member) { - return member.nickname == chat.nickname; + return member.nickname == chat.nickname && + member.username == chat.username; }); if (it == members.end()) { LOG_INFO(Network, "Cannot find self in the player list when sending a message."); @@ -202,20 +250,64 @@ void ChatRoom::OnSendChat() { } } +void ChatRoom::UpdateIconDisplay() { + for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) { + QStandardItem* item = player_list->invisibleRootItem()->child(row); + const std::string avatar_url = + item->data(PlayerListItem::AvatarUrlRole).toString().toStdString(); + if (icon_cache.count(avatar_url)) { + item->setData(icon_cache.at(avatar_url), Qt::DecorationRole); + } + } +} + void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) { // TODO(B3N30): Remember which row is selected player_list->removeRows(0, player_list->rowCount()); for (const auto& member : member_list) { if (member.nickname.empty()) continue; - QList l; - std::vector elements = {member.nickname, member.game_info.name}; - for (const auto& item : elements) { - QStandardItem* child = new QStandardItem(QString::fromStdString(item)); - child->setEditable(false); - l.append(child); + QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, + member.avatar_url, member.game_info.name); + + if (!icon_cache.count(member.avatar_url)) { + // Emplace a default question mark icon as avatar + icon_cache.emplace(member.avatar_url, QIcon::fromTheme("no_avatar").pixmap(48)); + if (!member.avatar_url.empty()) { +#ifdef ENABLE_WEB_SERVICE + // 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( + QString("%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()), + 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(l); + name_item->setData(icon_cache.at(member.avatar_url), Qt::DecorationRole); + + player_list->invisibleRootItem()->appendRow(name_item); } // TODO(B3N30): Restore row selection } @@ -230,7 +322,8 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { if (!item.isValid()) return; - std::string nickname = player_list->item(item.row())->text().toStdString(); + std::string nickname = + player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); if (auto room = Network::GetRoomMember().lock()) { // You can't block yourself if (nickname == room->GetNickname()) diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index f541af819..d76f995b8 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -52,9 +52,12 @@ private: static constexpr u32 max_chat_lines = 1000; void AppendChatMessage(const QString&); bool ValidateMessage(const std::string&); + void UpdateIconDisplay(); + QStandardItemModel* player_list; std::unique_ptr ui; std::unordered_set block_list; + std::unordered_map icon_cache; }; Q_DECLARE_METATYPE(Network::ChatEntry); diff --git a/src/citra_qt/multiplayer/chat_room.ui b/src/citra_qt/multiplayer/chat_room.ui index 8bb1899c0..f2b31b5da 100644 --- a/src/citra_qt/multiplayer/chat_room.ui +++ b/src/citra_qt/multiplayer/chat_room.ui @@ -6,7 +6,7 @@ 0 0 - 607 + 807 432 diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui index 35086ab28..22b969d3b 100644 --- a/src/citra_qt/multiplayer/client_room.ui +++ b/src/citra_qt/multiplayer/client_room.ui @@ -6,7 +6,7 @@ 0 0 - 607 + 807 432 diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 13ccdd95a..27ab37f46 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -22,6 +22,9 @@ #include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "ui_host_room.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) @@ -79,6 +82,21 @@ void HostRoomWindow::RetranslateUi() { ui->retranslateUi(this); } +std::unique_ptr HostRoomWindow::CreateVerifyBackend( + 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); +#else + verify_backend = std::make_unique(); +#endif + } else { + verify_backend = std::make_unique(); + } + return verify_backend; +} + void HostRoomWindow::Host() { if (!ui->username->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); @@ -108,11 +126,12 @@ void HostRoomWindow::Host() { auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; auto password = ui->password->text().toStdString(); + const bool is_public = ui->host_type->currentIndex() == 0; if (auto room = Network::GetRoom().lock()) { - bool created = - room->Create(ui->room_name->text().toStdString(), - ui->room_description->toPlainText().toStdString(), "", port, password, - ui->max_player->value(), game_name.toStdString(), game_id); + bool created = room->Create(ui->room_name->text().toStdString(), + ui->room_description->toPlainText().toStdString(), "", port, + password, ui->max_player->value(), game_name.toStdString(), + game_id, CreateVerifyBackend(is_public)); if (!created) { NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); LOG_ERROR(Network, "Could not create room!"); @@ -120,9 +139,34 @@ void HostRoomWindow::Host() { return; } } + // Start the announce session if they chose Public + if (is_public) { + if (auto session = announce_multiplayer_session.lock()) { + // Register the room first to ensure verify_UID is present when we connect + session->Register(); + session->Start(); + } else { + LOG_ERROR(Network, "Starting announce session failed"); + } + } + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (is_public) { + WebService::Client client(Settings::values.web_api_url, Settings::values.citra_username, + Settings::values.citra_token); + if (auto 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 member->Join(ui->username->text().toStdString(), Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), "127.0.0.1", port, - 0, Network::NoPreferredMac, password); + 0, Network::NoPreferredMac, password, token); // Store settings UISettings::values.room_nickname = ui->username->text(); @@ -137,24 +181,8 @@ void HostRoomWindow::Host() { : QString::number(Network::DefaultRoomPort); UISettings::values.room_description = ui->room_description->toPlainText(); Settings::Apply(); - OnConnection(); - } -} - -void HostRoomWindow::OnConnection() { - ui->host->setEnabled(true); - if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->GetState() == Network::RoomMember::State::Joining) { - // Start the announce session if they chose Public - if (ui->host_type->currentIndex() == 0) { - if (auto session = announce_multiplayer_session.lock()) { - session->Start(); - } else { - LOG_ERROR(Network, "Starting announce session failed"); - } - } - close(); - } + ui->host->setEnabled(true); + close(); } } diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h index 87ad96b9d..620574bd6 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/citra_qt/multiplayer/host_room.h @@ -26,6 +26,10 @@ class ComboBoxProxyModel; class ChatMessage; +namespace Network::VerifyUser { +class Backend; +}; + class HostRoomWindow : public QDialog { Q_OBJECT @@ -36,15 +40,9 @@ public: void RetranslateUi(); -private slots: - /** - * Handler for connection status changes. Launches the chat window if successful or - * displays an error - */ - void OnConnection(); - private: void Host(); + std::unique_ptr CreateVerifyBackend(bool use_validation) const; std::weak_ptr announce_multiplayer_session; QStandardItemModel* game_list; diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 1c23c8cef..bb1807776 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -18,6 +18,9 @@ #include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "network/network.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif Lobby::Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) @@ -136,12 +139,27 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { const std::string ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); + const std::string verify_UID = + proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); // attempt to connect in a different thread - QFuture f = QtConcurrent::run([nickname, ip, port, password] { + QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (!Settings::values.citra_username.empty() && !Settings::values.citra_token.empty()) { + WebService::Client client(Settings::values.web_api_url, Settings::values.citra_username, + Settings::values.citra_token); + 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 = Network::GetRoomMember().lock()) { room_member->Join(nickname, Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), - ip.c_str(), port, 0, Network::NoPreferredMac, password); + ip.c_str(), port, 0, Network::NoPreferredMac, password, token); } }); watcher->setFuture(f); @@ -193,7 +211,8 @@ void Lobby::OnRefreshLobby() { QList members; for (auto member : room.members) { QVariant var; - var.setValue(LobbyMember{QString::fromStdString(member.name), member.game_id, + var.setValue(LobbyMember{QString::fromStdString(member.username), + QString::fromStdString(member.nickname), member.game_id, QString::fromStdString(member.game_name)}); members.append(var); } @@ -205,7 +224,7 @@ void Lobby::OnRefreshLobby() { new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), smdh_icon), new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip), - room.port), + room.port, QString::fromStdString(room.verify_UID)), new LobbyItemMemberList(members, room.max_player), }); model->appendRow(row); diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index 7fe20b78b..e684fc816 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -120,12 +120,14 @@ public: static const int HostUsernameRole = Qt::UserRole + 1; static const int HostIPRole = Qt::UserRole + 2; static const int HostPortRole = Qt::UserRole + 3; + static const int HostVerifyUIDRole = Qt::UserRole + 4; LobbyItemHost() = default; - explicit LobbyItemHost(QString username, QString ip, u16 port) { + explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_UID) { setData(username, HostUsernameRole); setData(ip, HostIPRole); setData(port, HostPortRole); + setData(verify_UID, HostVerifyUIDRole); } QVariant data(int role) const override { @@ -146,12 +148,17 @@ class LobbyMember { public: LobbyMember() = default; LobbyMember(const LobbyMember& other) = default; - explicit LobbyMember(QString username, u64 title_id, QString game_name) - : username(std::move(username)), title_id(title_id), game_name(std::move(game_name)) {} + explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) + : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), + game_name(std::move(game_name)) {} ~LobbyMember() = default; - QString GetUsername() const { - return username; + QString GetName() const { + if (username.isEmpty() || username == nickname) { + return nickname; + } else { + return QString("%1 (%2)").arg(nickname, username); + } } u64 GetTitleId() const { return title_id; @@ -162,6 +169,7 @@ public: private: QString username; + QString nickname; u64 title_id; QString game_name; }; @@ -220,10 +228,9 @@ public: out += '\n'; const auto& m = member.value(); if (m.GetGameName().isEmpty()) { - out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetUsername()); + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName()); } else { - out += - QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName()); + out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); } first = false; } diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 50eeb8e8e..fb7b7281d 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -15,7 +15,7 @@ static std::string public_key; std::string GetPublicKey(const std::string& host) { if (public_key.empty()) { Client client(host, "", ""); // no need for credentials here - public_key = client.GetJson("/jwt/external/key.pem", true).returned_data; + public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data; if (public_key.empty()) { LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); } else { diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 689be8c9c..453c96574 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -33,8 +33,9 @@ struct Client::Impl { } /// A generic function handles POST, GET and DELETE request together - Common::WebResult GenericJson(const std::string& method, const std::string& path, - const std::string& data, bool allow_anonymous) { + Common::WebResult GenericRequest(const std::string& method, const std::string& path, + const std::string& data, bool allow_anonymous, + const std::string& accept) { if (jwt.empty()) { UpdateJWT(); } @@ -45,11 +46,11 @@ struct Client::Impl { "Credentials needed"}; } - auto result = GenericJson(method, path, data, jwt); + auto result = GenericRequest(method, path, data, accept, jwt); if (result.result_string == "401") { // Try again with new JWT UpdateJWT(); - result = GenericJson(method, path, data, jwt); + result = GenericRequest(method, path, data, accept, jwt); } return result; @@ -61,9 +62,10 @@ struct Client::Impl { * username + token is used if jwt is empty but username and token are * not empty anonymous if all of jwt, username and token are empty */ - Common::WebResult GenericJson(const std::string& method, const std::string& path, - const std::string& data, const std::string& jwt = "", - const std::string& username = "", const std::string& token = "") { + Common::WebResult GenericRequest(const std::string& method, const std::string& path, + const std::string& data, const std::string& accept, + const std::string& jwt = "", const std::string& username = "", + const std::string& token = "") { if (cli == nullptr) { auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); int port; @@ -134,9 +136,7 @@ struct Client::Impl { return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; } - if (content_type->second.find("application/json") == std::string::npos && - content_type->second.find("text/html; charset=utf-8") == std::string::npos && - content_type->second.find("text/plain; charset=utf-8") == std::string::npos) { + if (content_type->second.find(accept) == std::string::npos) { LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, content_type->second); return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; @@ -150,7 +150,7 @@ struct Client::Impl { return; } - auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); + auto result = GenericRequest("POST", "/jwt/internal", "", "text/html", "", username, token); if (result.result_code != Common::WebResult::Code::Success) { LOG_ERROR(WebService, "UpdateJWT failed"); } else { @@ -183,20 +183,29 @@ Client::~Client() = default; Common::WebResult Client::PostJson(const std::string& path, const std::string& data, bool allow_anonymous) { - return impl->GenericJson("POST", path, data, allow_anonymous); + return impl->GenericRequest("POST", path, data, allow_anonymous, "application/json"); } Common::WebResult Client::GetJson(const std::string& path, bool allow_anonymous) { - return impl->GenericJson("GET", path, "", allow_anonymous); + return impl->GenericRequest("GET", path, "", allow_anonymous, "application/json"); } Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data, bool allow_anonymous) { - return impl->GenericJson("DELETE", path, data, allow_anonymous); + return impl->GenericRequest("DELETE", path, data, allow_anonymous, "application/json"); +} + +Common::WebResult Client::GetPlain(const std::string& path, bool allow_anonymous) { + return impl->GenericRequest("GET", path, "", allow_anonymous, "text/plain"); +} + +Common::WebResult Client::GetImage(const std::string& path, bool allow_anonymous) { + return impl->GenericRequest("GET", path, "", allow_anonymous, "image/png"); } Common::WebResult Client::GetExternalJWT(const std::string& audience) { - return PostJson(fmt::format("/jwt/external/{}", audience), "", false); + return impl->GenericRequest("POST", fmt::format("/jwt/external/{}", audience), "", false, + "text/html"); } } // namespace WebService diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index d366d642c..04121f17e 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -46,6 +46,22 @@ public: Common::WebResult DeleteJson(const std::string& path, const std::string& data, bool allow_anonymous); + /** + * Gets a plain string from the specified path. + * @param path the URL segment after the host address. + * @param allow_anonymous If true, allow anonymous unauthenticated requests. + * @return the result of the request. + */ + Common::WebResult GetPlain(const std::string& path, bool allow_anonymous); + + /** + * Gets an PNG image from the specified path. + * @param path the URL segment after the host address. + * @param allow_anonymous If true, allow anonymous unauthenticated requests. + * @return the result of the request. + */ + Common::WebResult GetImage(const std::string& path, bool allow_anonymous); + /** * Requests an external JWT for the specific audience provided. * @param audience the audience of the JWT requested.