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 000000000..d4bf82026
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/no_avatar.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png b/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png
new file mode 100644
index 000000000..43e0dd267
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png differ
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index bcb121d4b..c151238e1 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -7,6 +7,7 @@
icons/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.