From a9c9ffd32cf0df024619fd1a1751fd98332da45c Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 1 Oct 2018 09:53:20 +0800 Subject: [PATCH 01/27] network: bump multiplayer version --- src/network/room.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/room.h b/src/network/room.h index bff190c55..6f8d15aeb 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -12,7 +12,7 @@ namespace Network { -constexpr u32 network_version = 3; ///< The version of this Room and RoomMember +constexpr u32 network_version = 4; ///< The version of this Room and RoomMember constexpr u16 DefaultRoomPort = 24872; From e040bc9355db924bfc4cd9f2b60001afd41d4739 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 20 Apr 2018 01:34:37 -0600 Subject: [PATCH 02/27] Multiplayer: Send an error message when connecting to a full room --- src/citra_qt/multiplayer/message.cpp | 2 ++ src/citra_qt/multiplayer/message.h | 1 + src/citra_qt/multiplayer/state.cpp | 3 +++ src/network/room.cpp | 26 +++++++++++++++++++++++++- src/network/room.h | 3 ++- src/network/room_member.cpp | 3 +++ src/network/room_member.h | 11 ++++++----- 7 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index d2275e0e0..2cf76256f 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -22,6 +22,8 @@ const ConnectionError UNABLE_TO_CONNECT( QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If " "you still cannot connect, contact the room host and verify that the host is " "properly configured with the external port forwarded.")); +const ConnectionError ROOM_IS_FULL( + QT_TR_NOOP("Unable to connect to the room because it is already full.")); const ConnectionError COULD_NOT_CREATE_ROOM( QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); const ConnectionError HOST_BANNED( diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h index 3b8613199..8e719de8e 100644 --- a/src/citra_qt/multiplayer/message.h +++ b/src/citra_qt/multiplayer/message.h @@ -27,6 +27,7 @@ extern const ConnectionError IP_ADDRESS_NOT_VALID; extern const ConnectionError PORT_NOT_VALID; extern const ConnectionError NO_INTERNET; extern const ConnectionError UNABLE_TO_CONNECT; +extern const ConnectionError ROOM_IS_FULL; extern const ConnectionError COULD_NOT_CREATE_ROOM; extern const ConnectionError HOST_BANNED; extern const ConnectionError WRONG_VERSION; diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index f1dec2ba3..461e1b5ca 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -102,6 +102,9 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s case Network::RoomMember::State::MacCollision: NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); break; + case Network::RoomMember::State::RoomIsFull: + NetworkMessage::ShowError(NetworkMessage::ROOM_IS_FULL); + break; case Network::RoomMember::State::WrongPassword: NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); break; diff --git a/src/network/room.cpp b/src/network/room.cpp index f0d229d40..db2a1a1fe 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -68,6 +68,11 @@ public: */ bool IsValidMacAddress(const MacAddress& address) const; + /** + * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. + */ + void SendRoomIsFull(ENetPeer* client); + /** * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. */ @@ -193,6 +198,13 @@ void Room::RoomImpl::StartLoop() { } void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { + { + std::lock_guard lock(member_mutex); + if (members.size() >= room_information.member_slots) { + SendRoomIsFull(event->peer); + return; + } + } Packet packet; packet.Append(event->packet->data, event->packet->dataLength); packet.IgnoreBytes(sizeof(u8)); // Ignore the message type @@ -295,6 +307,16 @@ void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { enet_host_flush(server); } +void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { + Packet packet; + packet << static_cast(IdRoomIsFull); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { Packet packet; packet << static_cast(IdVersionMismatch); @@ -527,7 +549,9 @@ bool Room::Create(const std::string& name, const std::string& server_address, u1 } address.port = server_port; - room_impl->server = enet_host_create(&address, max_connections, NumChannels, 0, 0); + // In order to send the room is full message to the connecting client, we need to leave one slot + // open so enet won't reject the incoming connection without telling us + room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); if (!room_impl->server) { return false; } diff --git a/src/network/room.h b/src/network/room.h index 6f8d15aeb..19bb5acce 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -57,7 +57,8 @@ enum RoomMessageTypes : u8 { IdMacCollision, IdVersionMismatch, IdWrongPassword, - IdCloseRoom + IdCloseRoom, + IdRoomIsFull, }; /// This is what a server [person creating a server] would use. diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 601c67b0e..971c05c18 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -154,6 +154,9 @@ void RoomMember::RoomMemberImpl::MemberLoop() { HandleJoinPacket(&event); // Get the MAC Address for the client SetState(State::Joined); break; + case IdRoomIsFull: + SetState(State::RoomIsFull); + break; case IdNameCollision: SetState(State::NameCollision); break; diff --git a/src/network/room_member.h b/src/network/room_member.h index 12cff102a..b8a648f1d 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -54,11 +54,12 @@ public: LostConnection, ///< Connection closed // Reasons why connection was rejected - NameCollision, ///< Somebody is already using this name - MacCollision, ///< Somebody is already using that mac-address - WrongVersion, ///< The room version is not the same as for this RoomMember - WrongPassword, ///< The password doesn't match the one from the Room - CouldNotConnect ///< The room is not responding to a connection attempt + NameCollision, ///< Somebody is already using this name + MacCollision, ///< Somebody is already using that mac-address + WrongVersion, ///< The room version is not the same as for this RoomMember + WrongPassword, ///< The password doesn't match the one from the Room + CouldNotConnect, ///< The room is not responding to a connection attempt + RoomIsFull ///< Room is already at the maximum number of players }; struct MemberInformation { From 3c589f473f0c140be1c20a27c6b164a977dda442 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 2 Nov 2018 20:17:25 +0800 Subject: [PATCH 03/27] multiplayer: check nickname regex server side --- src/citra_qt/multiplayer/message.cpp | 4 ++-- src/citra_qt/multiplayer/message.h | 4 +++- src/citra_qt/multiplayer/state.cpp | 2 +- src/network/room.cpp | 9 +++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index 2cf76256f..2489a6ffd 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -12,8 +12,8 @@ const ConnectionError USERNAME_NOT_VALID( QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); const ConnectionError ROOMNAME_NOT_VALID( QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); -const ConnectionError USERNAME_IN_USE( - QT_TR_NOOP("Username is already in use. Please choose another.")); +const ConnectionError USERNAME_NOT_VALID_SERVER( + QT_TR_NOOP("Username is already in use or not valid. Please choose another.")); const ConnectionError IP_ADDRESS_NOT_VALID(QT_TR_NOOP("IP is not a valid IPv4 address.")); const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535.")); const ConnectionError NO_INTERNET( diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h index 8e719de8e..46461c3c1 100644 --- a/src/citra_qt/multiplayer/message.h +++ b/src/citra_qt/multiplayer/message.h @@ -20,9 +20,11 @@ private: std::string err; }; +/// When the nickname is considered invalid by the client extern const ConnectionError USERNAME_NOT_VALID; extern const ConnectionError ROOMNAME_NOT_VALID; -extern const ConnectionError USERNAME_IN_USE; +/// When the nickname is considered invalid by the room server +extern const ConnectionError USERNAME_NOT_VALID_SERVER; extern const ConnectionError IP_ADDRESS_NOT_VALID; extern const ConnectionError PORT_NOT_VALID; extern const ConnectionError NO_INTERNET; diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 461e1b5ca..d5c15dcb6 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -97,7 +97,7 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); break; case Network::RoomMember::State::NameCollision: - NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); + NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID_SERVER); break; case Network::RoomMember::State::MacCollision: NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); diff --git a/src/network/room.cpp b/src/network/room.cpp index db2a1a1fe..a5c39d2b6 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "common/logging/log.h" @@ -263,8 +264,12 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { } bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const { - // A nickname is valid if it is not already taken by anybody else in the room. - // TODO(B3N30): Check for empty names, spaces, etc. + // A nickname is valid if it matches the regex and is not already taken by anybody else in the + // room. + const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$"); + if (!std::regex_match(nickname, nickname_regex)) + return false; + std::lock_guard lock(member_mutex); return std::all_of(members.begin(), members.end(), [&nickname](const auto& member) { return member.nickname != nickname; }); From c396e3c6e5a9c128499919477ff952737bdf5802 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 31 Oct 2018 23:07:03 +0800 Subject: [PATCH 04/27] network: check Console ID conflicts As Console ID can be sensitive data sometimes, this implementation sent a SHA256 hash of it instead. --- externals/cpp-jwt | 1 + src/citra/citra.cpp | 4 +- src/citra_qt/multiplayer/direct_connect.cpp | 2 + src/citra_qt/multiplayer/host_room.cpp | 6 ++- src/citra_qt/multiplayer/lobby.cpp | 4 +- src/citra_qt/multiplayer/message.cpp | 3 ++ src/citra_qt/multiplayer/message.h | 1 + src/citra_qt/multiplayer/state.cpp | 3 ++ src/core/hle/service/cfg/cfg.cpp | 17 ++++++++ src/core/hle/service/cfg/cfg.h | 3 ++ src/network/room.cpp | 48 +++++++++++++++++++-- src/network/room.h | 1 + src/network/room_member.cpp | 16 ++++--- src/network/room_member.h | 23 +++++----- 14 files changed, 109 insertions(+), 23 deletions(-) create mode 160000 externals/cpp-jwt diff --git a/externals/cpp-jwt b/externals/cpp-jwt new file mode 160000 index 000000000..6e27aa4c8 --- /dev/null +++ b/externals/cpp-jwt @@ -0,0 +1 @@ +Subproject commit 6e27aa4c8671e183f11e327a2e1f556c64fdc4a9 diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index b64510957..d09a6ddc1 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -39,6 +39,7 @@ #include "core/frontend/applets/default_applets.h" #include "core/gdbstub/gdbstub.h" #include "core/hle/service/am/am.h" +#include "core/hle/service/cfg/cfg.h" #include "core/loader/loader.h" #include "core/movie.h" #include "core/settings.h" @@ -336,7 +337,8 @@ int main(int argc, char** argv) { member->BindOnStateChanged(OnStateChanged); LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname); - member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredMac, password); + member->Join(nickname, Service::CFG::GetConsoleIdHash(system), address.c_str(), port, 0, + Network::NoPreferredMac, password); } else { LOG_ERROR(Network, "Could not access RoomMember"); return 0; diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 2d6e28c91..3870c1e25 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -15,6 +15,7 @@ #include "citra_qt/multiplayer/state.h" #include "citra_qt/multiplayer/validation.h" #include "citra_qt/ui_settings.h" +#include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "network/network.h" #include "ui_direct_connect.h" @@ -97,6 +98,7 @@ void DirectConnectWindow::Connect() { if (auto room_member = Network::GetRoomMember().lock()) { auto port = UISettings::values.port.toUInt(); room_member->Join(ui->nickname->text().toStdString(), + Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), ui->ip->text().toStdString().c_str(), port, 0, Network::NoPreferredMac, ui->password->text().toStdString().c_str()); } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 1d389539d..fa335bfae 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -19,6 +19,7 @@ #include "citra_qt/ui_settings.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" +#include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "ui_host_room.h" @@ -116,8 +117,9 @@ void HostRoomWindow::Host() { return; } } - member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, - Network::NoPreferredMac, password); + member->Join(ui->username->text().toStdString(), + Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), "127.0.0.1", port, + 0, Network::NoPreferredMac, password); // Store settings UISettings::values.room_nickname = ui->username->text(); diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 8a5630289..6b22c277c 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -15,6 +15,7 @@ #include "citra_qt/multiplayer/validation.h" #include "citra_qt/ui_settings.h" #include "common/logging/log.h" +#include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "network/network.h" @@ -139,7 +140,8 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { // attempt to connect in a different thread QFuture f = QtConcurrent::run([nickname, ip, port, password] { if (auto room_member = Network::GetRoomMember().lock()) { - room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredMac, password); + room_member->Join(nickname, Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), + ip.c_str(), port, 0, Network::NoPreferredMac, password); } }); watcher->setFuture(f); diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index 2489a6ffd..e13463a21 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -38,6 +38,9 @@ const ConnectionError GENERIC_ERROR( const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect.")); const ConnectionError MAC_COLLISION( QT_TR_NOOP("MAC address is already in use. Please choose another.")); +const ConnectionError CONSOLE_ID_COLLISION(QT_TR_NOOP( + "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " + "> Configure > System to regenerate your Console ID.")); static bool WarnMessage(const std::string& title, const std::string& text) { return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h index 46461c3c1..cc8e0f4a4 100644 --- a/src/citra_qt/multiplayer/message.h +++ b/src/citra_qt/multiplayer/message.h @@ -37,6 +37,7 @@ extern const ConnectionError WRONG_PASSWORD; extern const ConnectionError GENERIC_ERROR; extern const ConnectionError LOST_CONNECTION; extern const ConnectionError MAC_COLLISION; +extern const ConnectionError CONSOLE_ID_COLLISION; /** * Shows a standard QMessageBox with a error message diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index d5c15dcb6..d819a3d0f 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -102,6 +102,9 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s case Network::RoomMember::State::MacCollision: NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); break; + case Network::RoomMember::State::ConsoleIdCollision: + NetworkMessage::ShowError(NetworkMessage::CONSOLE_ID_COLLISION); + break; case Network::RoomMember::State::RoomIsFull: NetworkMessage::ShowError(NetworkMessage::ROOM_IS_FULL); break; diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index d5c08a5a7..8c6859bb3 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -734,4 +734,21 @@ void InstallInterfaces(Core::System& system) { std::make_shared()->InstallAsService(service_manager); } +std::string GetConsoleIdHash(Core::System& system) { + u64_le console_id{}; + std::array buffer; + if (system.IsPoweredOn()) { + auto cfg = GetModule(system); + ASSERT_MSG(cfg, "CFG Module missing!"); + console_id = cfg->GetConsoleUniqueId(); + } else { + console_id = std::make_unique()->GetConsoleUniqueId(); + } + std::memcpy(buffer.data(), &console_id, sizeof(console_id)); + + std::array hash; + CryptoPP::SHA256().CalculateDigest(hash.data(), buffer.data(), sizeof(buffer)); + return fmt::format("{:02x}", fmt::join(hash.begin(), hash.end(), "")); +} + } // namespace Service::CFG diff --git a/src/core/hle/service/cfg/cfg.h b/src/core/hle/service/cfg/cfg.h index b069ad728..af63c6f91 100644 --- a/src/core/hle/service/cfg/cfg.h +++ b/src/core/hle/service/cfg/cfg.h @@ -415,4 +415,7 @@ std::shared_ptr GetModule(Core::System& system); void InstallInterfaces(Core::System& system); +/// Convenience function for getting a SHA256 hash of the Console ID +std::string GetConsoleIdHash(Core::System& system); + } // namespace Service::CFG diff --git a/src/network/room.cpp b/src/network/room.cpp index a5c39d2b6..9ae26f524 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -31,10 +31,11 @@ public: std::string password; ///< The password required to connect to this room. struct Member { - std::string nickname; ///< The nickname of the member. - GameInfo game_info; ///< The current game of the member - MacAddress mac_address; ///< The assigned mac address of the member. - ENetPeer* peer; ///< The remote peer. + std::string nickname; ///< The nickname of the member. + std::string console_id_hash; ///< A hash of the console ID of the member. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. + ENetPeer* peer; ///< The remote peer. }; using MemberList = std::vector; MemberList members; ///< Information about the members of this room @@ -69,6 +70,12 @@ public: */ bool IsValidMacAddress(const MacAddress& address) const; + /** + * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in + * the room. + */ + bool IsValidConsoleId(const std::string& console_id_hash) const; + /** * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. */ @@ -84,6 +91,12 @@ public: */ void SendMacCollision(ENetPeer* client); + /** + * Sends a IdConsoleIdCollison message telling the client that another member with the same + * console ID exists. + */ + void SendConsoleIdCollision(ENetPeer* client); + /** * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid. */ @@ -212,6 +225,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { std::string nickname; packet >> nickname; + std::string console_id_hash; + packet >> console_id_hash; + MacAddress preferred_mac; packet >> preferred_mac; @@ -242,6 +258,11 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { preferred_mac = GenerateMacAddress(); } + if (!IsValidConsoleId(console_id_hash)) { + SendConsoleIdCollision(event->peer); + return; + } + if (client_version != network_version) { SendVersionMismatch(event->peer); return; @@ -250,6 +271,7 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { // At this point the client is ready to be added to the room. Member member{}; member.mac_address = preferred_mac; + member.console_id_hash = console_id_hash; member.nickname = nickname; member.peer = event->peer; @@ -282,6 +304,14 @@ bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const { [&address](const auto& member) { return member.mac_address != address; }); } +bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const { + // A Console ID is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) { + return member.console_id_hash != console_id_hash; + }); +} + void Room::RoomImpl::SendNameCollision(ENetPeer* client) { Packet packet; packet << static_cast(IdNameCollision); @@ -302,6 +332,16 @@ void Room::RoomImpl::SendMacCollision(ENetPeer* client) { enet_host_flush(server); } +void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { + Packet packet; + packet << static_cast(IdConsoleIdCollision); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { Packet packet; packet << static_cast(IdWrongPassword); diff --git a/src/network/room.h b/src/network/room.h index 19bb5acce..7a73022bc 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -59,6 +59,7 @@ enum RoomMessageTypes : u8 { IdWrongPassword, IdCloseRoom, IdRoomIsFull, + IdConsoleIdCollision, }; /// This is what a server [person creating a server] would use. diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 971c05c18..682821aa3 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -73,11 +73,12 @@ public: * Sends a request to the server, asking for permission to join a room with the specified * nickname and preferred mac. * @params nickname The desired nickname. + * @params console_id_hash A hash of the Console ID. * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells * @params password The password for the room * the server to assign one for us. */ - void SendJoinRequest(const std::string& nickname, + void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, const MacAddress& preferred_mac = NoPreferredMac, const std::string& password = ""); @@ -163,6 +164,9 @@ void RoomMember::RoomMemberImpl::MemberLoop() { case IdMacCollision: SetState(State::MacCollision); break; + case IdConsoleIdCollision: + SetState(State::ConsoleIdCollision); + break; case IdVersionMismatch: SetState(State::WrongVersion); break; @@ -204,11 +208,13 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { } void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, + const std::string& console_id_hash, const MacAddress& preferred_mac, const std::string& password) { Packet packet; packet << static_cast(IdJoinRequest); packet << nickname; + packet << console_id_hash; packet << preferred_mac; packet << network_version; packet << password; @@ -392,9 +398,9 @@ RoomInformation RoomMember::GetRoomInformation() const { return room_member_impl->room_information; } -void RoomMember::Join(const std::string& nick, const char* server_addr, u16 server_port, - u16 client_port, const MacAddress& preferred_mac, - const std::string& password) { +void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, + const char* server_addr, u16 server_port, u16 client_port, + const MacAddress& preferred_mac, const std::string& password) { // If the member is connected, kill the connection first if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { Leave(); @@ -427,7 +433,7 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { room_member_impl->nickname = nick; room_member_impl->StartLoop(); - room_member_impl->SendJoinRequest(nick, preferred_mac, password); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password); SendGameInfo(room_member_impl->current_game_info); } else { enet_peer_disconnect(room_member_impl->server, 0); diff --git a/src/network/room_member.h b/src/network/room_member.h index b8a648f1d..58fca96b7 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -54,12 +54,13 @@ public: LostConnection, ///< Connection closed // Reasons why connection was rejected - NameCollision, ///< Somebody is already using this name - MacCollision, ///< Somebody is already using that mac-address - WrongVersion, ///< The room version is not the same as for this RoomMember - WrongPassword, ///< The password doesn't match the one from the Room - CouldNotConnect, ///< The room is not responding to a connection attempt - RoomIsFull ///< Room is already at the maximum number of players + NameCollision, ///< Somebody is already using this name + MacCollision, ///< Somebody is already using that mac-address + ConsoleIdCollision, ///< Somebody in the room has the same Console ID + WrongVersion, ///< The room version is not the same as for this RoomMember + WrongPassword, ///< The password doesn't match the one from the Room + CouldNotConnect, ///< The room is not responding to a connection attempt + RoomIsFull ///< Room is already at the maximum number of players }; struct MemberInformation { @@ -116,11 +117,13 @@ public: /** * Attempts to join a room at the specified address and port, using the specified nickname. - * This may fail if the username is already taken. + * A console ID hash is passed in to check console ID conflicts. + * This may fail if the username or console ID is already taken. */ - void Join(const std::string& nickname, const char* server_addr = "127.0.0.1", - const u16 server_port = DefaultRoomPort, const u16 client_port = 0, - const MacAddress& preferred_mac = NoPreferredMac, const std::string& password = ""); + void Join(const std::string& nickname, const std::string& console_id_hash, + const char* server_addr = "127.0.0.1", const u16 server_port = DefaultRoomPort, + const u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = ""); /** * Sends a WiFi packet to the room. From 5f0e18923838ce8c49c54593d2378e3df71826bb Mon Sep 17 00:00:00 2001 From: adityaruplaha Date: Mon, 30 Apr 2018 13:10:51 +0530 Subject: [PATCH 05/27] Add Support for Room Descriptions --- src/citra_qt/configuration/config.cpp | 2 ++ src/citra_qt/multiplayer/client_room.cpp | 1 + src/citra_qt/multiplayer/client_room.ui | 7 +++++++ src/citra_qt/multiplayer/host_room.cpp | 8 ++++++-- src/citra_qt/multiplayer/host_room.ui | 16 ++++++++++++++- src/citra_qt/multiplayer/lobby.cpp | 10 ++++++--- src/citra_qt/multiplayer/lobby_p.h | 25 +++++++++++++++++++++++ src/citra_qt/ui_settings.h | 1 + src/common/announce_multiplayer_room.h | 8 ++++++-- src/core/announce_multiplayer_session.cpp | 9 ++++---- src/dedicated_room/citra-room.cpp | 12 ++++++++--- src/network/room.cpp | 10 ++++++--- src/network/room.h | 6 ++++-- src/network/room_member.cpp | 2 ++ src/web_service/announce_room_json.cpp | 11 +++++++++- src/web_service/announce_room_json.h | 5 +++-- 16 files changed, 110 insertions(+), 23 deletions(-) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index d696ed98d..5a69a2d30 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -329,6 +329,7 @@ void Config::ReadValues() { } UISettings::values.max_player = ReadSetting("max_player", 8).toUInt(); UISettings::values.game_id = ReadSetting("game_id", 0).toULongLong(); + UISettings::values.room_description = ReadSetting("room_description", "").toString(); qt_config->endGroup(); qt_config->endGroup(); @@ -533,6 +534,7 @@ void Config::SaveValues() { WriteSetting("host_type", UISettings::values.host_type, 0); WriteSetting("max_player", UISettings::values.max_player, 8); WriteSetting("game_id", UISettings::values.game_id, 0); + WriteSetting("room_description", UISettings::values.room_description, ""); qt_config->endGroup(); qt_config->endGroup(); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 63227f6e0..3c18cc474 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -82,6 +82,7 @@ void ClientRoomWindow::UpdateView() { .arg(QString::fromStdString(information.name)) .arg(memberlist.size()) .arg(information.member_slots)); + ui->description->setText(QString::fromStdString(information.description)); return; } } diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui index d83c088c2..35086ab28 100644 --- a/src/citra_qt/multiplayer/client_room.ui +++ b/src/citra_qt/multiplayer/client_room.ui @@ -21,6 +21,13 @@ 0 + + + + Room Description + + + diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index fa335bfae..13ccdd95a 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -70,6 +70,7 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, if (index != -1) { ui->game_list->setCurrentIndex(index); } + ui->room_description->setText(UISettings::values.room_description); } HostRoomWindow::~HostRoomWindow() = default; @@ -108,8 +109,10 @@ void HostRoomWindow::Host() { auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; auto password = ui->password->text().toStdString(); if (auto room = Network::GetRoom().lock()) { - bool created = room->Create(ui->room_name->text().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); if (!created) { NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); LOG_ERROR(Network, "Could not create room!"); @@ -132,6 +135,7 @@ void HostRoomWindow::Host() { UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) ? ui->port->text() : QString::number(Network::DefaultRoomPort); + UISettings::values.room_description = ui->room_description->toPlainText(); Settings::Apply(); OnConnection(); } diff --git a/src/citra_qt/multiplayer/host_room.ui b/src/citra_qt/multiplayer/host_room.ui index 68bfdb0da..5084ef7f4 100644 --- a/src/citra_qt/multiplayer/host_room.ui +++ b/src/citra_qt/multiplayer/host_room.ui @@ -7,7 +7,7 @@ 0 0 607 - 165 + 211 @@ -131,6 +131,20 @@ + + + + + + Room Description + + + + + + + + diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 6b22c277c..1c23c8cef 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -212,7 +212,11 @@ void Lobby::OnRefreshLobby() { // To make the rows expandable, add the member data as a child of the first column of the // rows with people in them and have qt set them to colspan after the model is finished // resetting - if (room.members.size() > 0) { + if (!room.description.empty()) { + first_item->appendRow( + new LobbyItemDescription(QString::fromStdString(room.description))); + } + if (!room.members.empty()) { first_item->appendRow(new LobbyItemExpandedMemberList(members)); } } @@ -228,8 +232,8 @@ void Lobby::OnRefreshLobby() { // Set the member list child items to span all columns for (int i = 0; i < proxy->rowCount(); i++) { auto parent = model->item(i, 0); - if (parent->hasChildren()) { - ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true); + for (int j = 0; j < parent->rowCount(); j++) { + ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true); } } } diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index 3773f99de..7fe20b78b 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -55,6 +55,31 @@ public: } }; +class LobbyItemDescription : public LobbyItem { +public: + static const int DescriptionRole = Qt::UserRole + 1; + + LobbyItemDescription() = default; + explicit LobbyItemDescription(QString description) { + setData(description, DescriptionRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto description = data(DescriptionRole).toString(); + description.prepend("Description: "); + return description; + } + + bool operator<(const QStandardItem& other) const override { + return data(DescriptionRole) + .toString() + .localeAwareCompare(other.data(DescriptionRole).toString()) < 0; + } +}; + class LobbyItemGame : public LobbyItem { public: static const int TitleIDRole = Qt::UserRole + 1; diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index 1e09dd7e8..b26d69364 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -109,6 +109,7 @@ struct Values { QString room_port; uint host_type; qulonglong game_id; + QString room_description; // logging bool show_console; diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 5f9fc8ec2..21f8c5a08 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -23,6 +23,7 @@ struct Room { u64 game_id; }; std::string name; + std::string description; std::string UID; std::string owner; std::string ip; @@ -49,13 +50,15 @@ public: * Sets the Information that gets used for the announce * @param uid The Id of the room * @param name The name of the room + * @param description The room description * @param port The port of the room * @param net_version The version of the libNetwork that gets used * @param has_password True if the room is passowrd protected * @param preferred_game The preferred game of the room * @param preferred_game_id The title id of the preferred game */ - virtual void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, + virtual void SetRoomInformation(const std::string& uid, const std::string& name, + const std::string& description, const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) = 0; @@ -100,7 +103,8 @@ class NullBackend : public Backend { public: ~NullBackend() = default; void SetRoomInformation(const std::string& /*uid*/, const std::string& /*name*/, - const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const std::string& /*description*/, const u16 /*port*/, + const u32 /*max_player*/, const u32 /*net_version*/, const bool /*has_password*/, const std::string& /*preferred_game*/, const u64 /*preferred_game_id*/) override {} void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index 69dbf6fce..3d2301e0f 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -78,10 +78,11 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { } Network::RoomInformation room_information = room->GetRoomInformation(); std::vector memberlist = room->GetRoomMemberList(); - backend->SetRoomInformation( - room_information.uid, room_information.name, room_information.port, - room_information.member_slots, Network::network_version, room->HasPassword(), - room_information.preferred_game, room_information.preferred_game_id); + backend->SetRoomInformation(room_information.uid, room_information.name, + room_information.description, room_information.port, + room_information.member_slots, Network::network_version, + room->HasPassword(), room_information.preferred_game, + room_information.preferred_game_id); backend->ClearPlayers(); for (const auto& member : memberlist) { backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, diff --git a/src/dedicated_room/citra-room.cpp b/src/dedicated_room/citra-room.cpp index 7d23c93f2..8ccce245d 100644 --- a/src/dedicated_room/citra-room.cpp +++ b/src/dedicated_room/citra-room.cpp @@ -36,6 +36,7 @@ static void PrintHelp(const char* argv0) { std::cout << "Usage: " << argv0 << " [options] \n" "--room-name The name of the room\n" + "--room-description The room description\n" "--port The port used for the room\n" "--max_members The maximum number of players for this room\n" "--password The password for the room\n" @@ -63,6 +64,7 @@ int main(int argc, char** argv) { gladLoadGL(); std::string room_name; + std::string room_description; std::string password; std::string preferred_game; std::string username; @@ -74,6 +76,7 @@ int main(int argc, char** argv) { static struct option long_options[] = { {"room-name", required_argument, 0, 'n'}, + {"room-description", required_argument, 0, 'd'}, {"port", required_argument, 0, 'p'}, {"max_members", required_argument, 0, 'm'}, {"password", required_argument, 0, 'w'}, @@ -88,12 +91,15 @@ int main(int argc, char** argv) { }; while (optind < argc) { - char arg = getopt_long(argc, argv, "n:p:m:w:g:u:t:a:i:hv", long_options, &option_index); + char arg = getopt_long(argc, argv, "n:d:p:m:w:g:u:t:a:i:hv", long_options, &option_index); if (arg != -1) { switch (arg) { case 'n': room_name.assign(optarg); break; + case 'd': + room_description.assign(optarg); + break; case 'p': port = strtoul(optarg, &endarg, 0); break; @@ -175,8 +181,8 @@ int main(int argc, char** argv) { Network::Init(); if (std::shared_ptr room = Network::GetRoom().lock()) { - if (!room->Create(room_name, "", port, password, max_members, preferred_game, - preferred_game_id)) { + if (!room->Create(room_name, room_description, "", port, password, max_members, + preferred_game, preferred_game_id)) { std::cout << "Failed to create room: \n\n"; return -1; } diff --git a/src/network/room.cpp b/src/network/room.cpp index 9ae26f524..c5b960925 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -124,6 +124,7 @@ public: * The packet has the structure: * ID_ROOM_INFORMATION * room_name + * room_description * member_slots: The max number of clients allowed in this room * uid * port @@ -404,6 +405,7 @@ void Room::RoomImpl::BroadcastRoomInformation() { Packet packet; packet << static_cast(IdRoomInformation); packet << room_information.name; + packet << room_information.description; packet << room_information.member_slots; packet << room_information.uid; packet << room_information.port; @@ -584,9 +586,10 @@ Room::Room() : room_impl{std::make_unique()} {} Room::~Room() = default; -bool Room::Create(const std::string& name, const std::string& server_address, u16 server_port, - const std::string& password, const u32 max_connections, - const std::string& preferred_game, u64 preferred_game_id) { +bool Room::Create(const std::string& name, const std::string& description, + const std::string& server_address, u16 server_port, const std::string& password, + const u32 max_connections, const std::string& preferred_game, + u64 preferred_game_id) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -603,6 +606,7 @@ bool Room::Create(const std::string& name, const std::string& server_address, u1 room_impl->state = State::Open; room_impl->room_information.name = name; + room_impl->room_information.description = description; room_impl->room_information.member_slots = max_connections; room_impl->room_information.port = server_port; room_impl->room_information.preferred_game = preferred_game; diff --git a/src/network/room.h b/src/network/room.h index 7a73022bc..9b2889a0c 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -25,6 +25,7 @@ constexpr std::size_t NumChannels = 1; // Number of channels used for the connec struct RoomInformation { std::string name; ///< Name of the server + std::string description; ///< Server description u32 member_slots; ///< Maximum number of members in this room std::string uid; ///< The unique ID of the room u16 port; ///< The port of this room @@ -103,8 +104,9 @@ public: * Creates the socket for this room. Will bind to default address if * server is empty string. */ - bool Create(const std::string& name, const std::string& server = "", - u16 server_port = DefaultRoomPort, const std::string& password = "", + bool Create(const std::string& name, const std::string& description = "", + const std::string& server = "", u16 server_port = DefaultRoomPort, + const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, const std::string& preferred_game = "", u64 preferred_game_id = 0); diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 682821aa3..14fc201f5 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -230,11 +230,13 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev RoomInformation info{}; packet >> info.name; + packet >> info.description; packet >> info.member_slots; packet >> info.uid; packet >> info.port; packet >> info.preferred_game; room_information.name = info.name; + room_information.description = info.description; room_information.member_slots = info.member_slots; room_information.port = info.port; room_information.preferred_game = info.preferred_game; diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index c0d543e73..970c5e0b6 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -27,6 +27,7 @@ void to_json(nlohmann::json& json, const Room& room) { json["id"] = room.UID; json["port"] = room.port; json["name"] = room.name; + json["description"] = room.description; json["preferredGameName"] = room.preferred_game; json["preferredGameId"] = room.preferred_game_id; json["maxPlayers"] = room.max_player; @@ -41,6 +42,12 @@ void to_json(nlohmann::json& json, const Room& room) { void from_json(const nlohmann::json& json, Room& room) { room.ip = json.at("address").get(); room.name = json.at("name").get(); + try { + room.description = json.at("description").get(); + } catch (const nlohmann::detail::out_of_range& e) { + room.description = ""; + LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.name); + } room.owner = json.at("owner").get(); room.port = json.at("port").get(); room.preferred_game = json.at("preferredGameName").get(); @@ -59,11 +66,13 @@ void from_json(const nlohmann::json& json, Room& room) { namespace WebService { -void RoomJson::SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, +void RoomJson::SetRoomInformation(const std::string& uid, const std::string& name, + const std::string& description, 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.description = description; room.UID = uid; room.port = port; room.max_player = max_player; diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index 735605213..57152c243 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -20,8 +20,9 @@ public: RoomJson(const std::string& host, const std::string& username, const std::string& token) : client(host, username, token), host(host), username(username), token(token) {} ~RoomJson() = default; - void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, - const u32 max_player, const u32 net_version, const bool has_password, + void SetRoomInformation(const std::string& uid, const std::string& name, + const std::string& description, 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, From 1a8841f96e04ba70307a609ea905c02101912877 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:35:01 +0800 Subject: [PATCH 06/27] network, web_service: Add Verification backend and use new lobby API Added verify_backend to load user_data for members. and removed method to generate UID as this is now done server-side. Added GetUsername function and a "token" param to room_member. Also added a username to ChatEntry, so that the username can be shown (along with nicknames) in the chat dialog. --- .gitmodules | 5 ++- externals/CMakeLists.txt | 10 +++-- src/network/CMakeLists.txt | 4 +- src/network/room.cpp | 66 ++++++++++++++++++----------- src/network/room.h | 24 ++++++++--- src/network/room_member.cpp | 34 ++++++++++++--- src/network/room_member.h | 16 +++++-- src/network/verify_user.cpp | 18 ++++++++ src/network/verify_user.h | 45 ++++++++++++++++++++ src/web_service/CMakeLists.txt | 4 +- src/web_service/verify_user_jwt.cpp | 56 ++++++++++++++++++++++++ src/web_service/verify_user_jwt.h | 25 +++++++++++ 12 files changed, 262 insertions(+), 45 deletions(-) create mode 100644 src/network/verify_user.cpp create mode 100644 src/network/verify_user.h create mode 100644 src/web_service/verify_user_jwt.cpp create mode 100644 src/web_service/verify_user_jwt.h diff --git a/.gitmodules b/.gitmodules index f79819a95..b7f41e5f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,4 +42,7 @@ url = https://github.com/zeromq/libzmq [submodule "externals/cppzmq"] path = externals/cppzmq - url = https://github.com/zeromq/cppzmq \ No newline at end of file + url = https://github.com/zeromq/cppzmq +[submodule "cpp-jwt"] + path = externals/cpp-jwt + url = https://github.com/arun11299/cpp-jwt.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 13b347d89..b96bebb7f 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -82,6 +82,10 @@ if (ENABLE_WEB_SERVICE) target_include_directories(ssl INTERFACE ./libressl/include) target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP) + # JSON + add_library(json-headers INTERFACE) + target_include_directories(json-headers INTERFACE ./json) + # lurlparser add_subdirectory(lurlparser EXCLUDE_FROM_ALL) @@ -89,9 +93,9 @@ if (ENABLE_WEB_SERVICE) add_library(httplib INTERFACE) target_include_directories(httplib INTERFACE ./httplib) - # JSON - add_library(json-headers INTERFACE) - target_include_directories(json-headers INTERFACE ./json) + # cpp-jwt + add_library(cpp-jwt INTERFACE) + target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) endif() if (ENABLE_SCRIPTING) diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index f72da9439..b98cf1654 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -7,8 +7,10 @@ add_library(network STATIC room.h room_member.cpp room_member.h + verify_user.cpp + verify_user.h ) create_target_directory_groups(network) -target_link_libraries(network PRIVATE common enet) +target_link_libraries(network PRIVATE common cpp-jwt enet) diff --git a/src/network/room.cpp b/src/network/room.cpp index c5b960925..cae4a7258 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -14,6 +14,7 @@ #include "enet/enet.h" #include "network/packet.h" #include "network/room.h" +#include "network/verify_user.h" namespace Network { @@ -28,6 +29,9 @@ public: std::atomic state{State::Closed}; ///< Current state of the room. RoomInformation room_information; ///< Information about this room. + std::string verify_UID; ///< A GUID which may be used for verfication. + mutable std::mutex verify_UID_mutex; ///< Mutex for verify_UID + std::string password; ///< The password required to connect to this room. struct Member { @@ -35,7 +39,9 @@ public: std::string console_id_hash; ///< A hash of the console ID of the member. GameInfo game_info; ///< The current game of the member MacAddress mac_address; ///< The assigned mac address of the member. - ENetPeer* peer; ///< The remote peer. + /// Data of the user, often including authenticated forum username. + VerifyUser::UserData user_data; + ENetPeer* peer; ///< The remote peer. }; using MemberList = std::vector; MemberList members; ///< Information about the members of this room @@ -48,6 +54,9 @@ public: /// Thread that receives and dispatches network packets std::unique_ptr room_thread; + /// Verification backend of the room + std::unique_ptr verify_backend; + /// Thread function that will receive and dispatch messages until the room is destroyed. void ServerLoop(); void StartLoop(); @@ -165,11 +174,6 @@ public: * to all other clients. */ void HandleClientDisconnection(ENetPeer* client); - - /** - * Creates a random ID in the form 12345678-1234-1234-1234-123456789012 - */ - void CreateUniqueID(); }; // RoomImpl @@ -238,6 +242,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { std::string pass; packet >> pass; + std::string token; + packet >> token; + if (pass != password) { SendWrongPassword(event->peer); return; @@ -276,6 +283,13 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { member.nickname = nickname; member.peer = event->peer; + std::string uid; + { + std::lock_guard lock(verify_UID_mutex); + uid = verify_UID; + } + member.user_data = verify_backend->LoadUserData(uid, token); + { std::lock_guard lock(member_mutex); members.push_back(std::move(member)); @@ -407,7 +421,6 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << room_information.name; packet << room_information.description; packet << room_information.member_slots; - packet << room_information.uid; packet << room_information.port; packet << room_information.preferred_game; @@ -419,6 +432,9 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << member.mac_address; packet << member.game_info.name; packet << member.game_info.id; + packet << member.user_data.username; + packet << member.user_data.display_name; + packet << member.user_data.avatar_url; } } @@ -511,6 +527,7 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { Packet out_packet; out_packet << static_cast(IdChatMessage); out_packet << sending_member->nickname; + out_packet << sending_member->user_data.username; out_packet << message; ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), @@ -567,20 +584,6 @@ void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { BroadcastRoomInformation(); } -void Room::RoomImpl::CreateUniqueID() { - std::uniform_int_distribution<> dis(0, 9999); - std::ostringstream stream; - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - room_information.uid = stream.str(); -} - // Room Room::Room() : room_impl{std::make_unique()} {} @@ -589,7 +592,7 @@ Room::~Room() = default; bool Room::Create(const std::string& name, const std::string& description, const std::string& server_address, u16 server_port, const std::string& password, const u32 max_connections, const std::string& preferred_game, - u64 preferred_game_id) { + u64 preferred_game_id, std::unique_ptr verify_backend) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -597,8 +600,8 @@ bool Room::Create(const std::string& name, const std::string& description, } address.port = server_port; - // In order to send the room is full message to the connecting client, we need to leave one slot - // open so enet won't reject the incoming connection without telling us + // In order to send the room is full message to the connecting client, we need to leave one + // slot open so enet won't reject the incoming connection without telling us room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); if (!room_impl->server) { return false; @@ -612,7 +615,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->password = password; - room_impl->CreateUniqueID(); + room_impl->verify_backend = std::move(verify_backend); room_impl->StartLoop(); return true; @@ -626,12 +629,20 @@ const RoomInformation& Room::GetRoomInformation() const { return room_impl->room_information; } +std::string Room::GetVerifyUID() const { + std::lock_guard lock(room_impl->verify_UID_mutex); + return room_impl->verify_UID; +} + std::vector Room::GetRoomMemberList() const { std::vector member_list; std::lock_guard lock(room_impl->member_mutex); for (const auto& member_impl : room_impl->members) { Member member; member.nickname = member_impl.nickname; + member.username = member_impl.user_data.username; + member.display_name = member_impl.user_data.display_name; + member.avatar_url = member_impl.user_data.avatar_url; member.mac_address = member_impl.mac_address; member.game_info = member_impl.game_info; member_list.push_back(member); @@ -643,6 +654,11 @@ bool Room::HasPassword() const { return !room_impl->password.empty(); } +void Room::SetVerifyUID(const std::string& uid) { + std::lock_guard lock(room_impl->verify_UID_mutex); + room_impl->verify_UID = uid; +} + void Room::Destroy() { room_impl->state = State::Closed; room_impl->room_thread->join(); diff --git a/src/network/room.h b/src/network/room.h index 9b2889a0c..d1a26e62d 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -9,6 +9,7 @@ #include #include #include "common/common_types.h" +#include "network/verify_user.h" namespace Network { @@ -27,7 +28,6 @@ struct RoomInformation { std::string name; ///< Name of the server std::string description; ///< Server description u32 member_slots; ///< Maximum number of members in this room - std::string uid; ///< The unique ID of the room u16 port; ///< The port of this room std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game @@ -72,9 +72,12 @@ public: }; struct Member { - std::string nickname; ///< The nickname of the member. - GameInfo game_info; ///< The current game of the member - MacAddress mac_address; ///< The assigned mac address of the member. + std::string nickname; ///< The nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. }; Room(); @@ -90,6 +93,11 @@ public: */ const RoomInformation& GetRoomInformation() const; + /** + * Gets the verify UID of this room. + */ + std::string GetVerifyUID() const; + /** * Gets a list of the mbmers connected to the room. */ @@ -108,7 +116,13 @@ public: const std::string& server = "", u16 server_port = DefaultRoomPort, const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, - const std::string& preferred_game = "", u64 preferred_game_id = 0); + const std::string& preferred_game = "", u64 preferred_game_id = 0, + std::unique_ptr verify_backend = nullptr); + + /** + * Sets the verification GUID of the room. + */ + void SetVerifyUID(const std::string& uid); /** * Destroys the socket diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 14fc201f5..8fe67c086 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -33,7 +33,11 @@ public: void SetState(const State new_state); bool IsConnected() const; - std::string nickname; ///< The nickname of this member. + std::string nickname; ///< The nickname of this member. + + std::string username; ///< The username of this member. + mutable std::mutex username_mutex; ///< Mutex for locking username. + MacAddress mac_address; ///< The mac_address of this member. std::mutex network_mutex; ///< Mutex that controls access to the `client` variable. @@ -80,7 +84,7 @@ public: */ void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, const MacAddress& preferred_mac = NoPreferredMac, - const std::string& password = ""); + const std::string& password = "", const std::string& token = ""); /** * Extracts a MAC Address from a received ENet packet. @@ -210,7 +214,8 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, const MacAddress& preferred_mac, - const std::string& password) { + const std::string& password, + const std::string& token) { Packet packet; packet << static_cast(IdJoinRequest); packet << nickname; @@ -218,6 +223,7 @@ void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, packet << preferred_mac; packet << network_version; packet << password; + packet << token; Send(std::move(packet)); } @@ -232,7 +238,6 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> info.name; packet >> info.description; packet >> info.member_slots; - packet >> info.uid; packet >> info.port; packet >> info.preferred_game; room_information.name = info.name; @@ -250,6 +255,16 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> member.mac_address; packet >> member.game_info.name; packet >> member.game_info.id; + packet >> member.username; + packet >> member.display_name; + packet >> member.avatar_url; + + { + std::lock_guard lock(username_mutex); + if (member.nickname == nickname) { + username = member.username; + } + } } Invoke(room_information); } @@ -297,6 +312,7 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { ChatEntry chat_entry{}; packet >> chat_entry.nickname; + packet >> chat_entry.username; packet >> chat_entry.message; Invoke(chat_entry); } @@ -391,6 +407,11 @@ const std::string& RoomMember::GetNickname() const { return room_member_impl->nickname; } +const std::string& RoomMember::GetUsername() const { + std::lock_guard lock(room_member_impl->username_mutex); + return room_member_impl->username; +} + const MacAddress& RoomMember::GetMacAddress() const { ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); return room_member_impl->mac_address; @@ -402,7 +423,8 @@ RoomInformation RoomMember::GetRoomInformation() const { void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, const char* server_addr, u16 server_port, u16 client_port, - const MacAddress& preferred_mac, const std::string& password) { + const MacAddress& preferred_mac, const std::string& password, + const std::string& token) { // If the member is connected, kill the connection first if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { Leave(); @@ -435,7 +457,7 @@ void RoomMember::Join(const std::string& nick, const std::string& console_id_has if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { room_member_impl->nickname = nick; room_member_impl->StartLoop(); - room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); SendGameInfo(room_member_impl->current_game_info); } else { enet_peer_disconnect(room_member_impl->server, 0); diff --git a/src/network/room_member.h b/src/network/room_member.h index 58fca96b7..4329263aa 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -35,7 +35,9 @@ struct WifiPacket { /// Represents a chat message. struct ChatEntry { std::string nickname; ///< Nickname of the client who sent this message. - std::string message; ///< Body of the message. + /// Web services username of the client who sent this message, can be empty. + std::string username; + std::string message; ///< Body of the message. }; /** @@ -64,7 +66,10 @@ public: }; struct MemberInformation { - std::string nickname; ///< Nickname of the member. + std::string nickname; ///< Nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're /// not playing anything. MacAddress mac_address; ///< MAC address associated with this member. @@ -100,6 +105,11 @@ public: */ const std::string& GetNickname() const; + /** + * Returns the username of the RoomMember. + */ + const std::string& GetUsername() const; + /** * Returns the MAC address of the RoomMember. */ @@ -123,7 +133,7 @@ public: void Join(const std::string& nickname, const std::string& console_id_hash, const char* server_addr = "127.0.0.1", const u16 server_port = DefaultRoomPort, const u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, - const std::string& password = ""); + const std::string& password = "", const std::string& token = ""); /** * Sends a WiFi packet to the room. diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp new file mode 100644 index 000000000..d9d98e495 --- /dev/null +++ b/src/network/verify_user.cpp @@ -0,0 +1,18 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "network/verify_user.h" + +namespace Network::VerifyUser { + +Backend::~Backend() = default; + +NullBackend::~NullBackend() = default; + +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID, + [[maybe_unused]] const std::string& token) { + return {}; +} + +} // namespace Network::VerifyUser diff --git a/src/network/verify_user.h b/src/network/verify_user.h new file mode 100644 index 000000000..74e154331 --- /dev/null +++ b/src/network/verify_user.h @@ -0,0 +1,45 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/logging/log.h" + +namespace Network::VerifyUser { + +struct UserData { + std::string username; + std::string display_name; + std::string avatar_url; +}; + +/** + * A backend used for verifying users and loading user data. + */ +class Backend { +public: + virtual ~Backend(); + + /** + * Verifies the given token and loads the information into a UserData struct. + * @param verify_UID A GUID that may be used for verification. + * @param token A token that contains user data and verification data. The format and content is + * decided by backends. + */ + virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0; +}; + +/** + * A null backend where the token is ignored. + * No verification is performed here and the function returns an empty UserData. + */ +class NullBackend final : public Backend { +public: + ~NullBackend(); + + UserData LoadUserData(const std::string& verify_UID, const std::string& token) override; +}; + +} // namespace Network::VerifyUser diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 71bf8977d..c3d42fe8b 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -5,6 +5,8 @@ add_library(web_service STATIC telemetry_json.h verify_login.cpp verify_login.h + verify_user_jwt.cpp + verify_user_jwt.h web_backend.cpp web_backend.h ) @@ -15,4 +17,4 @@ get_directory_property(OPENSSL_LIBS DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl DEFINITION OPENSSL_LIBS) target_compile_definitions(web_service PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT) -target_link_libraries(web_service PRIVATE common json-headers ${OPENSSL_LIBS} httplib lurlparser) +target_link_libraries(web_service PRIVATE common network json-headers ${OPENSSL_LIBS} httplib lurlparser cpp-jwt) diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp new file mode 100644 index 000000000..50eeb8e8e --- /dev/null +++ b/src/web_service/verify_user_jwt.cpp @@ -0,0 +1,56 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "common/web_result.h" +#include "web_service/verify_user_jwt.h" +#include "web_service/web_backend.h" + +namespace WebService { + +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; + if (public_key.empty()) { + LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); + } else { + LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size()); + } + } + return public_key; +} + +VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} + +Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_UID, + const std::string& token) { + const std::string audience = fmt::format("external-{}", verify_UID); + using namespace jwt::params; + std::error_code error; + auto decoded = + jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("citra-core"), + aud(audience), validate_iat(true), validate_jti(true)); + if (error) { + LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}", + error.category().name(), error.value(), error.message()); + return {}; + } + Network::VerifyUser::UserData user_data{}; + if (decoded.payload().has_claim("username")) { + user_data.username = decoded.payload().get_claim_value("username"); + } + if (decoded.payload().has_claim("displayName")) { + user_data.display_name = decoded.payload().get_claim_value("displayName"); + } + if (decoded.payload().has_claim("avatarUrl")) { + user_data.avatar_url = decoded.payload().get_claim_value("avatarUrl"); + } + return user_data; +} + +} // namespace WebService diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h new file mode 100644 index 000000000..826e01eed --- /dev/null +++ b/src/web_service/verify_user_jwt.h @@ -0,0 +1,25 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "network/verify_user.h" +#include "web_service/web_backend.h" + +namespace WebService { + +class VerifyUserJWT final : public Network::VerifyUser::Backend { +public: + VerifyUserJWT(const std::string& host); + ~VerifyUserJWT() = default; + + Network::VerifyUser::UserData LoadUserData(const std::string& verify_UID, + const std::string& token) override; + +private: + std::string pub_key; +}; + +} // namespace WebService From ab335ccf1bf43cd49da6eb93e541f0af3b88f72a Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:40:15 +0800 Subject: [PATCH 07/27] core, web_service: Changes to announce service Separated registering and updating to correspond to the new announce API endpoint. Also added a verify_UID for JWT audience verification. --- src/common/announce_multiplayer_room.h | 40 ++++++++----- src/core/announce_multiplayer_session.cpp | 53 +++++++++++++----- src/core/announce_multiplayer_session.h | 11 ++++ src/web_service/announce_room_json.cpp | 68 +++++++++++++++++------ src/web_service/announce_room_json.h | 12 ++-- 5 files changed, 135 insertions(+), 49 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 21f8c5a08..f0cf79386 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -17,14 +17,17 @@ using MacAddress = std::array; struct Room { struct Member { - std::string name; + std::string username; + std::string nickname; + std::string avatar_url; MacAddress mac_address; std::string game_name; u64 game_id; }; + std::string id; + std::string verify_UID; ///< UID used for verification std::string name; std::string description; - std::string UID; std::string owner; std::string ip; u16 port; @@ -57,9 +60,8 @@ public: * @param preferred_game The preferred game of the room * @param preferred_game_id The title id of the preferred game */ - virtual void SetRoomInformation(const std::string& uid, const std::string& name, - const std::string& description, const u16 port, - const u32 max_player, const u32 net_version, + virtual void SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, const bool has_password, const std::string& preferred_game, const u64 preferred_game_id) = 0; /** @@ -69,14 +71,21 @@ public: * @param game_id The title id of the game the player plays * @param game_name The name of the game the player plays */ - virtual void AddPlayer(const std::string& nickname, const MacAddress& mac_address, + virtual void AddPlayer(const std::string& username, const std::string& nickname, + const std::string& avatar_url, const MacAddress& mac_address, const u64 game_id, const std::string& game_name) = 0; /** - * Send the data to the announce service - * @result The result of the announce attempt + * Updates the data in the announce service. Re-register the room when required. + * @result The result of the update attempt */ - virtual Common::WebResult Announce() = 0; + virtual Common::WebResult Update() = 0; + + /** + * Registers the data in the announce service + * @result A global Guid of the room which may be used for verification + */ + virtual std::string Register() = 0; /** * Empties the stored players @@ -102,16 +111,19 @@ public: class NullBackend : public Backend { public: ~NullBackend() = default; - void SetRoomInformation(const std::string& /*uid*/, const std::string& /*name*/, - const std::string& /*description*/, const u16 /*port*/, - const u32 /*max_player*/, const u32 /*net_version*/, + void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, const bool /*has_password*/, const std::string& /*preferred_game*/, const u64 /*preferred_game_id*/) override {} - void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, + void AddPlayer(const std::string& /*username*/, const std::string& /*nickname*/, + const std::string& /*avatar_url*/, const MacAddress& /*mac_address*/, const u64 /*game_id*/, const std::string& /*game_name*/) override {} - Common::WebResult Announce() override { + Common::WebResult Update() override { return Common::WebResult{Common::WebResult::Code::NoWebservice, "WebService is missing"}; } + std::string Register() override { + return ""; + } void ClearPlayers() override {} RoomList GetRoomList() override { return RoomList{}; diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp index 3d2301e0f..e255581f9 100644 --- a/src/core/announce_multiplayer_session.cpp +++ b/src/core/announce_multiplayer_session.cpp @@ -29,6 +29,21 @@ AnnounceMultiplayerSession::AnnounceMultiplayerSession() { #endif } +void AnnounceMultiplayerSession::Register() { + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { + return; + } + if (room->GetState() != Network::Room::State::Open) { + return; + } + UpdateBackendData(room); + std::string result = backend->Register(); + LOG_INFO(WebService, "Room has been registered"); + room->SetVerifyUID(result); + registered = true; +} + void AnnounceMultiplayerSession::Start() { if (announce_multiplayer_thread) { Stop(); @@ -44,6 +59,7 @@ void AnnounceMultiplayerSession::Stop() { announce_multiplayer_thread->join(); announce_multiplayer_thread.reset(); backend->Delete(); + registered = false; } } @@ -64,7 +80,24 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { Stop(); } +void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr room) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.name, room_information.description, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.username, member.nickname, member.avatar_url, member.mac_address, + member.game_info.id, member.game_info.name); + } +} + void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + if (!registered) { + Register(); + } auto update_time = std::chrono::steady_clock::now(); std::future future; while (!shutdown_event.WaitUntil(update_time)) { @@ -76,25 +109,19 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { if (room->GetState() != Network::Room::State::Open) { break; } - Network::RoomInformation room_information = room->GetRoomInformation(); - std::vector memberlist = room->GetRoomMemberList(); - backend->SetRoomInformation(room_information.uid, room_information.name, - room_information.description, room_information.port, - room_information.member_slots, Network::network_version, - room->HasPassword(), room_information.preferred_game, - room_information.preferred_game_id); - backend->ClearPlayers(); - for (const auto& member : memberlist) { - backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, - member.game_info.name); - } - Common::WebResult result = backend->Announce(); + UpdateBackendData(room); + Common::WebResult result = backend->Update(); if (result.result_code != Common::WebResult::Code::Success) { std::lock_guard lock(callback_mutex); for (auto callback : error_callbacks) { (*callback)(result); } } + if (result.result_string == "404") { + registered = false; + // Needs to register the room again + Register(); + } } } diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h index b9ba4c48a..79b30fb24 100644 --- a/src/core/announce_multiplayer_session.h +++ b/src/core/announce_multiplayer_session.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -13,6 +14,10 @@ #include "common/common_types.h" #include "common/thread.h" +namespace Network { +class Room; +} + namespace Core { /** @@ -39,6 +44,9 @@ public: */ void UnbindErrorCallback(CallbackHandle handle); + /// Registers a room to web services + void Register(); + /** * Starts the announce of a room to web services */ @@ -65,6 +73,9 @@ private: /// Backend interface that logs fields std::unique_ptr backend; + std::atomic_bool registered = false; ///< Whether the room has been registered + + void UpdateBackendData(std::shared_ptr room); void AnnounceMultiplayerLoop(); }; diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index 970c5e0b6..f1a690443 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -12,22 +12,36 @@ namespace AnnounceMultiplayerRoom { void to_json(nlohmann::json& json, const Room::Member& member) { - json["name"] = member.name; + if (!member.username.empty()) { + json["username"] = member.username; + } + json["nickname"] = member.nickname; + if (!member.avatar_url.empty()) { + json["avatarUrl"] = member.avatar_url; + } json["gameName"] = member.game_name; json["gameId"] = member.game_id; } void from_json(const nlohmann::json& json, Room::Member& member) { - member.name = json.at("name").get(); + member.nickname = json.at("nickname").get(); member.game_name = json.at("gameName").get(); member.game_id = json.at("gameId").get(); + try { + member.username = json.at("username").get(); + member.avatar_url = json.at("avatarUrl").get(); + } catch (const nlohmann::detail::out_of_range& e) { + member.username = member.avatar_url = ""; + LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname); + } } void to_json(nlohmann::json& json, const Room& room) { - json["id"] = room.UID; json["port"] = room.port; json["name"] = room.name; - json["description"] = room.description; + if (!room.description.empty()) { + json["description"] = room.description; + } json["preferredGameName"] = room.preferred_game; json["preferredGameId"] = room.preferred_game_id; json["maxPlayers"] = room.max_player; @@ -40,6 +54,7 @@ void to_json(nlohmann::json& json, const Room& room) { } void from_json(const nlohmann::json& json, Room& room) { + room.verify_UID = json.at("externalGuid").get(); room.ip = json.at("address").get(); room.name = json.at("name").get(); try { @@ -66,14 +81,12 @@ void from_json(const nlohmann::json& json, Room& room) { namespace WebService { -void RoomJson::SetRoomInformation(const std::string& uid, const std::string& name, - const std::string& description, const u16 port, - const u32 max_player, const u32 net_version, +void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, + 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.description = description; - room.UID = uid; room.port = port; room.max_player = max_player; room.net_version = net_version; @@ -81,20 +94,39 @@ void RoomJson::SetRoomInformation(const std::string& uid, const std::string& nam room.preferred_game = preferred_game; room.preferred_game_id = preferred_game_id; } -void RoomJson::AddPlayer(const std::string& nickname, +void RoomJson::AddPlayer(const std::string& username, const std::string& nickname, + const std::string& avatar_url, const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, const std::string& game_name) { AnnounceMultiplayerRoom::Room::Member member; - member.name = nickname; + member.username = username; + member.nickname = nickname; + member.avatar_url = avatar_url; member.mac_address = mac_address; member.game_id = game_id; member.game_name = game_name; room.members.push_back(member); } -Common::WebResult RoomJson::Announce() { +Common::WebResult RoomJson::Update() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be updated"); + return Common::WebResult{Common::WebResult::Code::LibError, "Room is not registered"}; + } + nlohmann::json json{{"players", room.members}}; + return client.PostJson(fmt::format("/lobby2/{}", room_id), json.dump(), false); +} + +std::string RoomJson::Register() { nlohmann::json json = room; - return client.PostJson("/lobby", json.dump(), false); + auto reply = client.PostJson("/lobby2", json.dump(), false).returned_data; + if (reply.empty()) { + return ""; + } + auto reply_json = nlohmann::json::parse(reply); + room = reply_json.get(); + room_id = reply_json.at("id").get(); + return room.verify_UID; } void RoomJson::ClearPlayers() { @@ -102,7 +134,7 @@ void RoomJson::ClearPlayers() { } AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { - auto reply = client.GetJson("/lobby", true).returned_data; + auto reply = client.GetJson("/lobby2", true).returned_data; if (reply.empty()) { return {}; } @@ -110,12 +142,14 @@ AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { } void RoomJson::Delete() { - nlohmann::json json; - json["id"] = room.UID; + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be deleted"); + return; + } Common::DetachedTasks::AddTask( - [host{this->host}, username{this->username}, token{this->token}, content{json.dump()}]() { + [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() { // create a new client here because the this->client might be destroyed. - Client{host, username, token}.DeleteJson("/lobby", content, false); + Client{host, username, token}.DeleteJson(fmt::format("/lobby2/{}", room_id), "", false); }); } diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h index 57152c243..16d630863 100644 --- a/src/web_service/announce_room_json.h +++ b/src/web_service/announce_room_json.h @@ -20,15 +20,16 @@ public: RoomJson(const std::string& host, const std::string& username, const std::string& token) : client(host, username, token), host(host), username(username), token(token) {} ~RoomJson() = default; - void SetRoomInformation(const std::string& uid, const std::string& name, - const std::string& description, const u16 port, const u32 max_player, - const u32 net_version, const bool has_password, + void SetRoomInformation(const std::string& name, const std::string& description, 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, + void AddPlayer(const std::string& username, const std::string& nickname, + const std::string& avatar_url, const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, const std::string& game_name) override; - Common::WebResult Announce() override; + Common::WebResult Update() override; + std::string Register() override; void ClearPlayers() override; AnnounceMultiplayerRoom::RoomList GetRoomList() override; void Delete() override; @@ -39,6 +40,7 @@ private: std::string host; std::string username; std::string token; + std::string room_id; }; } // namespace WebService From e04f75e1bfd2ed49665a46c8ddd5e72b99b56536 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:45:21 +0800 Subject: [PATCH 08/27] web_backend: added GetExternalJWT function To support requesting external JWTs to use them as verification tokens. --- src/web_service/web_backend.cpp | 8 +++++++- src/web_service/web_backend.h | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 84d6105d7..689be8c9c 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "common/common_types.h" #include "common/logging/log.h" @@ -134,7 +135,8 @@ struct Client::Impl { } 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/html; charset=utf-8") == std::string::npos && + content_type->second.find("text/plain; charset=utf-8") == 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"}; @@ -193,4 +195,8 @@ Common::WebResult Client::DeleteJson(const std::string& path, const std::string& return impl->GenericJson("DELETE", path, data, allow_anonymous); } +Common::WebResult Client::GetExternalJWT(const std::string& audience) { + return PostJson(fmt::format("/jwt/external/{}", audience), "", false); +} + } // namespace WebService diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index c637e09df..d366d642c 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -46,6 +46,13 @@ public: Common::WebResult DeleteJson(const std::string& path, const std::string& data, bool allow_anonymous); + /** + * Requests an external JWT for the specific audience provided. + * @param audience the audience of the JWT requested. + * @return the result of the request. + */ + Common::WebResult GetExternalJWT(const std::string& audience); + private: struct Impl; std::unique_ptr impl; From 4906c8ce7b64318b6309f26fbc0dcfad13198ccd Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:46:58 +0800 Subject: [PATCH 09/27] citra-room: Add verify backend and use new announce api --- src/dedicated_room/CMakeLists.txt | 5 +++++ src/dedicated_room/citra-room.cpp | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/dedicated_room/CMakeLists.txt b/src/dedicated_room/CMakeLists.txt index cbd5d9726..2492bc523 100644 --- a/src/dedicated_room/CMakeLists.txt +++ b/src/dedicated_room/CMakeLists.txt @@ -8,6 +8,11 @@ add_executable(citra-room create_target_directory_groups(citra-room) target_link_libraries(citra-room PRIVATE common core network) +if (ENABLE_WEB_SERVICE) + target_compile_definitions(citra-room PRIVATE -DENABLE_WEB_SERVICE) + target_link_libraries(citra-room PRIVATE web_service) +endif() + target_link_libraries(citra-room PRIVATE glad) if (MSVC) target_link_libraries(citra-room PRIVATE getopt) diff --git a/src/dedicated_room/citra-room.cpp b/src/dedicated_room/citra-room.cpp index 8ccce245d..9246b5797 100644 --- a/src/dedicated_room/citra-room.cpp +++ b/src/dedicated_room/citra-room.cpp @@ -31,6 +31,11 @@ #include "core/core.h" #include "core/settings.h" #include "network/network.h" +#include "network/verify_user.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif static void PrintHelp(const char* argv0) { std::cout << "Usage: " << argv0 @@ -179,10 +184,23 @@ int main(int argc, char** argv) { Settings::values.citra_token = token; } + std::unique_ptr verify_backend; + if (announce) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = std::make_unique(Settings::values.web_api_url); +#else + std::cout + << "Citra Web Services is not available with this build: validation is disabled.\n\n"; + verify_backend = std::make_unique(); +#endif + } else { + verify_backend = std::make_unique(); + } + Network::Init(); if (std::shared_ptr room = Network::GetRoom().lock()) { if (!room->Create(room_name, room_description, "", port, password, max_members, - preferred_game, preferred_game_id)) { + preferred_game, preferred_game_id, std::move(verify_backend))) { std::cout << "Failed to create room: \n\n"; return -1; } From 386bf5c861cdc74fc0874bf9da64da515deb27d9 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 27 Oct 2018 15:49:00 +0800 Subject: [PATCH 10/27] 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. From 0319e519602e3587a19babf9dcc2c6413c652f8b Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 9 Nov 2018 21:55:57 +0800 Subject: [PATCH 11/27] multiplayer: Add status message for user joining/leaving The room server is now able to send a new type of packet: IdStatusMessage which is parsed and displayed by the client. --- src/citra_qt/multiplayer/chat_room.cpp | 32 ++++++++++++++++++-- src/citra_qt/multiplayer/chat_room.h | 3 ++ src/network/room.cpp | 42 +++++++++++++++++++++++--- src/network/room.h | 7 +++++ src/network/room_member.cpp | 39 ++++++++++++++++++++++++ src/network/room_member.h | 18 +++++++++++ 6 files changed, 134 insertions(+), 7 deletions(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 357fdca39..ff12b3f1a 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -70,12 +70,11 @@ public: } QString GetSystemChatMessage() const { - return QString("[%1] %3") - .arg(timestamp, system_color, message); + return QString("[%1] * %3").arg(timestamp, system_color, message); } private: - static constexpr const char system_color[] = "#888888"; + static constexpr const char system_color[] = "#FF8C00"; QString timestamp; QString message; }; @@ -133,6 +132,7 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); @@ -140,7 +140,12 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_uniqueBindOnChatMessageRecieved( [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); + member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); } else { // TODO (jroweboy) network was not initialized? } @@ -220,6 +225,27 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { } } +void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) { + QString name; + if (status_message.username.empty() || status_message.username == status_message.nickname) { + name = QString::fromStdString(status_message.nickname); + } else { + name = QString("%1 (%2)").arg(QString::fromStdString(status_message.nickname), + QString::fromStdString(status_message.username)); + } + QString message; + switch (status_message.type) { + case Network::IdMemberJoin: + message = tr("%1 has joined").arg(name); + break; + case Network::IdMemberLeave: + message = tr("%1 has left").arg(name); + break; + } + if (!message.isEmpty()) + AppendStatusMessage(message); +} + void ChatRoom::OnSendChat() { if (auto room = Network::GetRoomMember().lock()) { if (room->GetState() != Network::RoomMember::State::Joined) { diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index d76f995b8..7a7c84b48 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -39,6 +39,7 @@ public: public slots: void OnRoomUpdate(const Network::RoomInformation& info); void OnChatReceive(const Network::ChatEntry&); + void OnStatusMessageReceive(const Network::StatusMessageEntry&); void OnSendChat(); void OnChatTextChanged(); void PopupContextMenu(const QPoint& menu_location); @@ -47,6 +48,7 @@ public slots: signals: void ChatReceived(const Network::ChatEntry&); + void StatusMessageReceived(const Network::StatusMessageEntry&); private: static constexpr u32 max_chat_lines = 1000; @@ -61,5 +63,6 @@ private: }; Q_DECLARE_METATYPE(Network::ChatEntry); +Q_DECLARE_METATYPE(Network::StatusMessageEntry); Q_DECLARE_METATYPE(Network::RoomInformation); Q_DECLARE_METATYPE(Network::RoomMember::State); diff --git a/src/network/room.cpp b/src/network/room.cpp index cae4a7258..ee62df220 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -127,6 +127,12 @@ public: */ void SendCloseMessage(); + /** + * Sends a system message to all the connected clients. + */ + void SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username); + /** * Sends the information about the room, along with the list of members * to every connected client in the room. @@ -290,6 +296,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { } member.user_data = verify_backend->LoadUserData(uid, token); + // Notify everyone that the user has joined. + SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username); + { std::lock_guard lock(member_mutex); members.push_back(std::move(member)); @@ -415,6 +424,24 @@ void Room::RoomImpl::SendCloseMessage() { } } +void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username) { + Packet packet; + packet << static_cast(IdStatusMessage); + packet << static_cast(type); + packet << nickname; + packet << username; + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); +} + void Room::RoomImpl::BroadcastRoomInformation() { Packet packet; packet << static_cast(IdRoomInformation); @@ -571,16 +598,23 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { // Remove the client from the members list. + std::string nickname, username; { std::lock_guard lock(member_mutex); - members.erase( - std::remove_if(members.begin(), members.end(), - [client](const Member& member) { return member.peer == client; }), - members.end()); + auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) { + return member.peer == client; + }); + if (member != members.end()) { + nickname = member->nickname; + username = member->user_data.username; + members.erase(member); + } } // Announce the change to all clients. enet_peer_disconnect(client, 0); + if (!nickname.empty()) + SendStatusMessage(IdMemberLeave, nickname, username); BroadcastRoomInformation(); } diff --git a/src/network/room.h b/src/network/room.h index d1a26e62d..a3d93eea9 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -61,6 +61,13 @@ enum RoomMessageTypes : u8 { IdCloseRoom, IdRoomIsFull, IdConsoleIdCollision, + IdStatusMessage, +}; + +/// Types of system status messages +enum StatusMessageTypes : u8 { + IdMemberJoin = 1, ///< Member joining + IdMemberLeave, ///< Member leaving }; /// This is what a server [person creating a server] would use. diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 8fe67c086..79ba71da8 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -58,6 +58,7 @@ public: private: CallbackSet callback_set_wifi_packet; CallbackSet callback_set_chat_messages; + CallbackSet callback_set_status_messages; CallbackSet callback_set_room_information; CallbackSet callback_set_state; }; @@ -109,6 +110,13 @@ public: */ void HandleChatPacket(const ENetEvent* event); + /** + * Extracts a system message entry from a received ENet packet and adds it to the system message + * queue. + * @param event The ENet event that was received. + */ + void HandleStatusMessagePacket(const ENetEvent* event); + /** * Disconnects the RoomMember from the Room */ @@ -148,6 +156,9 @@ void RoomMember::RoomMemberImpl::MemberLoop() { case IdChatMessage: HandleChatPacket(&event); break; + case IdStatusMessage: + HandleStatusMessagePacket(&event); + break; case IdRoomInformation: HandleRoomInformationPacket(&event); break; @@ -317,6 +328,22 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { Invoke(chat_entry); } +void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + StatusMessageEntry status_message_entry{}; + u8 type{}; + packet >> type; + status_message_entry.type = static_cast(type); + packet >> status_message_entry.nickname; + packet >> status_message_entry.username; + Invoke(status_message_entry); +} + void RoomMember::RoomMemberImpl::Disconnect() { member_information.clear(); room_information.member_slots = 0; @@ -367,6 +394,12 @@ RoomMember::RoomMemberImpl::CallbackSet& RoomMember::RoomMemberImpl:: return callback_set_chat_messages; } +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_status_messages; +} + template void RoomMember::RoomMemberImpl::Invoke(const T& data) { std::lock_guard lock(callback_mutex); @@ -519,6 +552,11 @@ RoomMember::CallbackHandle RoomMember::BindOnChatMessageRecieved( return room_member_impl->Bind(callback); } +RoomMember::CallbackHandle RoomMember::BindOnStatusMessageReceived( + std::function callback) { + return room_member_impl->Bind(callback); +} + template void RoomMember::Unbind(CallbackHandle handle) { std::lock_guard lock(room_member_impl->callback_mutex); @@ -538,5 +576,6 @@ template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); } // namespace Network diff --git a/src/network/room_member.h b/src/network/room_member.h index 4329263aa..5062b225e 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -40,6 +40,14 @@ struct ChatEntry { std::string message; ///< Body of the message. }; +/// Represents a system status message. +struct StatusMessageEntry { + StatusMessageTypes type; ///< Type of the message + /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc. + std::string nickname; + std::string username; +}; + /** * This is what a client [person joining a server] would use. * It also has to be used if you host a game yourself (You'd create both, a Room and a @@ -192,6 +200,16 @@ public: CallbackHandle BindOnChatMessageRecieved( std::function callback); + /** + * Binds a function to an event that will be triggered every time a StatusMessage is + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnStatusMessageReceived( + std::function callback); + /** * Leaves the current room. */ From 0823d8e009aa556c64dba6b2963b3cae52b7693a Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 9 Nov 2018 21:56:58 +0800 Subject: [PATCH 12/27] citra: add status messages and fix missing errors --- src/citra/citra.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index d09a6ddc1..631e4d766 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -99,6 +99,10 @@ static void OnStateChanged(const Network::RoomMember::State& state) { "connected to the Room"); exit(1); break; + case Network::RoomMember::State::ConsoleIdCollision: + LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); + exit(1); + break; case Network::RoomMember::State::WrongPassword: LOG_ERROR(Network, "Room replied with: Wrong password"); exit(1); @@ -108,6 +112,10 @@ static void OnStateChanged(const Network::RoomMember::State& state) { "You are using a different version than the room you are trying to connect to"); exit(1); break; + case Network::RoomMember::State::RoomIsFull: + LOG_ERROR(Network, "The room is full"); + exit(1); + break; default: break; } @@ -117,6 +125,20 @@ static void OnMessageReceived(const Network::ChatEntry& msg) { std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; } +static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { + std::string message; + switch (msg.type) { + case Network::IdMemberJoin: + message = fmt::format("{} has joined", msg.nickname); + break; + case Network::IdMemberLeave: + message = fmt::format("{} has left", msg.nickname); + break; + } + if (!message.empty()) + std::cout << std::endl << "* " << message << std::endl << std::endl; +} + static void InitializeLogging() { Log::Filter log_filter(Log::Level::Debug); log_filter.ParseFilterString(Settings::values.log_filter); @@ -334,6 +356,7 @@ int main(int argc, char** argv) { if (use_multiplayer) { if (auto member = Network::GetRoomMember().lock()) { member->BindOnChatMessageRecieved(OnMessageReceived); + member->BindOnStatusMessageReceived(OnStatusMessageReceived); member->BindOnStateChanged(OnStateChanged); LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname); From 6c29d441f4cb9e6038fe1d7aed06d842551157f0 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 7 Nov 2018 22:41:56 +0800 Subject: [PATCH 13/27] multiplayer: fix "Connected" message not appearing on first connection --- src/citra_qt/multiplayer/client_room.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 3c18cc474..16f9a58d7 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -33,6 +33,8 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) connect(this, &ClientRoomWindow::RoomInformationChanged, this, &ClientRoomWindow::OnRoomUpdate); connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); + // Update the state + OnStateChange(member->GetState()); } else { // TODO (jroweboy) network was not initialized? } From 38f86cce948ce732da8b35cfd800193bcba66c02 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:08:32 +0800 Subject: [PATCH 14/27] network/room: Moderation implementation Currently consist of 4 moderation commands (kick, ban, unban and get ban list). --- src/network/room.cpp | 328 ++++++++++++++++++++++++++++++++++++++++++- src/network/room.h | 35 ++++- 2 files changed, 357 insertions(+), 6 deletions(-) diff --git a/src/network/room.cpp b/src/network/room.cpp index ee62df220..394e8644f 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -48,6 +48,10 @@ public: mutable std::mutex member_mutex; ///< Mutex for locking the members list /// This should be a std::shared_mutex as soon as C++17 is supported + UsernameBanList username_ban_list; ///< List of banned usernames + IPBanList ip_ban_list; ///< List of banned IP addresses + mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists + RoomImpl() : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} @@ -68,6 +72,30 @@ public: */ void HandleJoinRequest(const ENetEvent* event); + /** + * Parses and answers a kick request from a client. + * Validates the permissions and that the given user exists and then kicks the member. + */ + void HandleModKickPacket(const ENetEvent* event); + + /** + * Parses and answers a ban request from a client. + * Validates the permissions and bans the user (by forum username or IP). + */ + void HandleModBanPacket(const ENetEvent* event); + + /** + * Parses and answers a unban request from a client. + * Validates the permissions and unbans the address. + */ + void HandleModUnbanPacket(const ENetEvent* event); + + /** + * Parses and answers a get ban list request from a client. + * Validates the permissions and returns the ban list. + */ + void HandleModGetBanListPacket(const ENetEvent* event); + /** * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room. */ @@ -85,6 +113,11 @@ public: */ bool IsValidConsoleId(const std::string& console_id_hash) const; + /** + * Returns whether a user has mod permissions. + */ + bool HasModPermission(const ENetPeer* client) const; + /** * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. */ @@ -122,6 +155,32 @@ public: */ void SendJoinSuccess(ENetPeer* client, MacAddress mac_address); + /** + * Sends a IdHostKicked message telling the client that they have been kicked. + */ + void SendUserKicked(ENetPeer* client); + + /** + * Sends a IdHostBanned message telling the client that they have been banned. + */ + void SendUserBanned(ENetPeer* client); + + /** + * Sends a IdModPermissionDenied message telling the client that they do not have mod + * permission. + */ + void SendModPermissionDenied(ENetPeer* client); + + /** + * Sends a IdModNoSuchUser message telling the client that the given user could not be found. + */ + void SendModNoSuchUser(ENetPeer* client); + + /** + * Sends the ban list in response to a client's request for getting ban list. + */ + void SendModBanListResponse(ENetPeer* client); + /** * Notifies the members that the room is closed, */ @@ -202,6 +261,19 @@ void Room::RoomImpl::ServerLoop() { case IdChatMessage: HandleChatPacket(&event); break; + // Moderation + case IdModKick: + HandleModKickPacket(&event); + break; + case IdModBan: + HandleModBanPacket(&event); + break; + case IdModUnban: + HandleModUnbanPacket(&event); + break; + case IdModGetBanList: + HandleModGetBanListPacket(&event); + break; } enet_packet_destroy(event.packet); break; @@ -296,6 +368,29 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { } member.user_data = verify_backend->LoadUserData(uid, token); + { + std::lock_guard lock(ban_list_mutex); + + // Check username ban + if (!member.user_data.username.empty() && + std::find(username_ban_list.begin(), username_ban_list.end(), + member.user_data.username) != username_ban_list.end()) { + + SendUserBanned(event->peer); + return; + } + + // Check IP ban + char ip_raw[256]; + enet_address_get_host_ip(&event->peer->address, ip_raw, sizeof(ip_raw) - 1); + std::string ip = ip_raw; + + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) { + SendUserBanned(event->peer); + return; + } + } + // Notify everyone that the user has joined. SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username); @@ -309,6 +404,153 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { SendJoinSuccess(event->peer, preferred_mac); } +void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet >> nickname; + + std::string username; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the kicked member + SendUserKicked(target_member->peer); + + username = target_member->user_data.username; + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberKicked, nickname, username); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet >> nickname; + + std::string username; + std::string ip; + + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the banned member + SendUserBanned(target_member->peer); + + nickname = target_member->nickname; + username = target_member->user_data.username; + + char ip_raw[256]; + enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + { + std::lock_guard lock(ban_list_mutex); + + if (!username.empty()) { + // Ban the forum username + if (std::find(username_ban_list.begin(), username_ban_list.end(), username) == + username_ban_list.end()) { + + username_ban_list.emplace_back(username); + } + } + + // Ban the member's IP as well + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) { + ip_ban_list.emplace_back(ip); + } + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberBanned, nickname, username); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string address; + packet >> address; + + bool unbanned = false; + { + std::lock_guard lock(ban_list_mutex); + + auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address); + if (it != username_ban_list.end()) { + unbanned = true; + username_ban_list.erase(it); + } + + it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address); + if (it != ip_ban_list.end()) { + unbanned = true; + ip_ban_list.erase(it); + } + } + + if (unbanned) { + SendStatusMessage(IdAddressUnbanned, address, ""); + } else { + SendModNoSuchUser(event->peer); + } +} + +void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + SendModBanListResponse(event->peer); +} + bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const { // A nickname is valid if it matches the regex and is not already taken by anybody else in the // room. @@ -336,6 +578,22 @@ bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const }); } +bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { + if (room_information.host_username.empty()) + return false; // This room does not support moderation + std::lock_guard lock(member_mutex); + const auto sending_member = + std::find_if(members.begin(), members.end(), + [client](const auto& member) { return member.peer == client; }); + if (sending_member == members.end()) { + return false; + } + if (sending_member->user_data.username != room_information.host_username) { + return false; + } + return true; +} + void Room::RoomImpl::SendNameCollision(ENetPeer* client) { Packet packet; packet << static_cast(IdNameCollision); @@ -407,6 +665,61 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { enet_host_flush(server); } +void Room::RoomImpl::SendUserKicked(ENetPeer* client) { + Packet packet; + packet << static_cast(IdHostKicked); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserBanned(ENetPeer* client) { + Packet packet; + packet << static_cast(IdHostBanned); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModPermissionDenied); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModNoSuchUser); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { + Packet packet; + packet << static_cast(IdModBanListResponse); + { + std::lock_guard lock(ban_list_mutex); + packet << username_ban_list; + packet << ip_ban_list; + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + void Room::RoomImpl::SendCloseMessage() { Packet packet; packet << static_cast(IdCloseRoom); @@ -450,6 +763,7 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << room_information.member_slots; packet << room_information.port; packet << room_information.preferred_game; + packet << room_information.host_username; packet << static_cast(members.size()); { @@ -625,8 +939,10 @@ Room::~Room() = default; bool Room::Create(const std::string& name, const std::string& description, const std::string& server_address, u16 server_port, const std::string& password, - const u32 max_connections, const std::string& preferred_game, - u64 preferred_game_id, std::unique_ptr verify_backend) { + const u32 max_connections, const std::string& host_username, + const std::string& preferred_game, u64 preferred_game_id, + std::unique_ptr verify_backend, + const Room::BanList& ban_list) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -648,8 +964,11 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.port = server_port; room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; + room_impl->room_information.host_username = host_username; room_impl->password = password; room_impl->verify_backend = std::move(verify_backend); + room_impl->username_ban_list = ban_list.first; + room_impl->ip_ban_list = ban_list.second; room_impl->StartLoop(); return true; @@ -668,6 +987,11 @@ std::string Room::GetVerifyUID() const { return room_impl->verify_UID; } +Room::BanList Room::GetBanList() const { + std::lock_guard lock(room_impl->ban_list_mutex); + return {room_impl->username_ban_list, room_impl->ip_ban_list}; +} + std::vector Room::GetRoomMemberList() const { std::vector member_list; std::lock_guard lock(room_impl->member_mutex); diff --git a/src/network/room.h b/src/network/room.h index a3d93eea9..3181e84d7 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -31,6 +31,7 @@ struct RoomInformation { u16 port; ///< The port of this room std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game + std::string host_username; ///< Forum username of the host }; struct GameInfo { @@ -62,12 +63,26 @@ enum RoomMessageTypes : u8 { IdRoomIsFull, IdConsoleIdCollision, IdStatusMessage, + IdHostKicked, + IdHostBanned, + /// Moderation requests + IdModKick, + IdModBan, + IdModUnban, + IdModGetBanList, + // Moderation responses + IdModBanListResponse, + IdModPermissionDenied, + IdModNoSuchUser, }; /// Types of system status messages enum StatusMessageTypes : u8 { - IdMemberJoin = 1, ///< Member joining - IdMemberLeave, ///< Member leaving + IdMemberJoin = 1, ///< Member joining + IdMemberLeave, ///< Member leaving + IdMemberKicked, ///< A member is kicked from the room + IdMemberBanned, ///< A member is banned from the room + IdAddressUnbanned, ///< A username / ip address is unbanned from the room }; /// This is what a server [person creating a server] would use. @@ -115,6 +130,11 @@ public: */ bool HasPassword() const; + using UsernameBanList = std::vector; + using IPBanList = std::vector; + + using BanList = std::pair; + /** * Creates the socket for this room. Will bind to default address if * server is empty string. @@ -123,14 +143,21 @@ public: const std::string& server = "", u16 server_port = DefaultRoomPort, const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, - const std::string& preferred_game = "", u64 preferred_game_id = 0, - std::unique_ptr verify_backend = nullptr); + const std::string& host_username = "", const std::string& preferred_game = "", + u64 preferred_game_id = 0, + std::unique_ptr verify_backend = nullptr, + const BanList& ban_list = {}); /** * Sets the verification GUID of the room. */ void SetVerifyUID(const std::string& uid); + /** + * Gets the ban list (including banned forum usernames and IPs) of the room. + */ + BanList GetBanList() const; + /** * Destroys the socket */ From 7acd2664dd046c231fbf5f6b94bbacb169061450 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:13:46 +0800 Subject: [PATCH 15/27] network/room_member: Add moderation functions To allow for passing moderation errors around without impacting the State, this commit also separates the previous State enum into two enums: State, and Error. The State enum now only contains generic states like disconnected or connected, and the Error enum describes the specific error happened. citra_qt/multiplayer/{state, message} is changed accordingly. --- src/citra_qt/multiplayer/message.cpp | 5 ++ src/citra_qt/multiplayer/message.h | 3 + src/citra_qt/multiplayer/state.cpp | 90 ++++++++++++-------- src/citra_qt/multiplayer/state.h | 3 + src/network/room_member.cpp | 122 ++++++++++++++++++++++++--- src/network/room_member.h | 84 +++++++++++++++--- 6 files changed, 251 insertions(+), 56 deletions(-) diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index e13463a21..36e465acb 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -36,11 +36,16 @@ const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); const ConnectionError GENERIC_ERROR( QT_TR_NOOP("An unknown error occured. If this error continues to occur, please open an issue")); const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect.")); +const ConnectionError HOST_KICKED(QT_TR_NOOP("You have been kicked by the room host.")); const ConnectionError MAC_COLLISION( QT_TR_NOOP("MAC address is already in use. Please choose another.")); const ConnectionError CONSOLE_ID_COLLISION(QT_TR_NOOP( "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " "> Configure > System to regenerate your Console ID.")); +const ConnectionError PERMISSION_DENIED( + QT_TR_NOOP("You do not have enough permission to perform this action.")); +const ConnectionError NO_SUCH_USER(QT_TR_NOOP( + "The user you are trying to kick/ban could not be found.\nThey may have left the room.")); static bool WarnMessage(const std::string& title, const std::string& text) { return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h index cc8e0f4a4..955b90847 100644 --- a/src/citra_qt/multiplayer/message.h +++ b/src/citra_qt/multiplayer/message.h @@ -36,8 +36,11 @@ extern const ConnectionError WRONG_VERSION; extern const ConnectionError WRONG_PASSWORD; extern const ConnectionError GENERIC_ERROR; extern const ConnectionError LOST_CONNECTION; +extern const ConnectionError HOST_KICKED; extern const ConnectionError MAC_COLLISION; extern const ConnectionError CONSOLE_ID_COLLISION; +extern const ConnectionError PERMISSION_DENIED; +extern const ConnectionError NO_SUCH_USER; /** * Shows a standard QMessageBox with a error message diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index d819a3d0f..14b2bd7ec 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -27,9 +27,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); connect(this, &MultiplayerState::NetworkStateChanged, this, &MultiplayerState::OnNetworkStateChanged); + error_callback_handle = member->BindOnError( + [this](const Network::RoomMember::Error& error) { emit NetworkError(error); }); + connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError); } qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); announce_multiplayer_session = std::make_shared(); announce_multiplayer_session->BindErrorCallback( @@ -52,6 +56,12 @@ MultiplayerState::~MultiplayerState() { member->Unbind(state_callback_handle); } } + + if (error_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(error_callback_handle); + } + } } void MultiplayerState::Close() { @@ -88,41 +98,8 @@ void MultiplayerState::retranslateUi() { void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); - bool is_connected = false; - switch (state) { - case Network::RoomMember::State::LostConnection: - NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); - break; - case Network::RoomMember::State::CouldNotConnect: - NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::NameCollision: - NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID_SERVER); - break; - case Network::RoomMember::State::MacCollision: - NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); - break; - case Network::RoomMember::State::ConsoleIdCollision: - NetworkMessage::ShowError(NetworkMessage::CONSOLE_ID_COLLISION); - break; - case Network::RoomMember::State::RoomIsFull: - NetworkMessage::ShowError(NetworkMessage::ROOM_IS_FULL); - break; - case Network::RoomMember::State::WrongPassword: - NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); - break; - case Network::RoomMember::State::WrongVersion: - NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); - break; - case Network::RoomMember::State::Error: - NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::Joined: - is_connected = true; + if (state == Network::RoomMember::State::Joined) { OnOpenNetworkRoom(); - break; - } - if (is_connected) { status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); status_text->setText(tr("Connected")); leave_room->setEnabled(true); @@ -137,6 +114,51 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s current_state = state; } +void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) { + LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error)); + switch (error) { + case Network::RoomMember::Error::LostConnection: + NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); + break; + case Network::RoomMember::Error::HostKicked: + NetworkMessage::ShowError(NetworkMessage::HOST_KICKED); + break; + case Network::RoomMember::Error::CouldNotConnect: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::NameCollision: + NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID_SERVER); + break; + case Network::RoomMember::Error::MacCollision: + NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + NetworkMessage::ShowError(NetworkMessage::CONSOLE_ID_COLLISION); + break; + case Network::RoomMember::Error::RoomIsFull: + NetworkMessage::ShowError(NetworkMessage::ROOM_IS_FULL); + break; + case Network::RoomMember::Error::WrongPassword: + NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); + break; + case Network::RoomMember::Error::WrongVersion: + NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); + break; + case Network::RoomMember::Error::HostBanned: + NetworkMessage::ShowError(NetworkMessage::HOST_BANNED); + break; + case Network::RoomMember::Error::UnknownError: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::PermissionDenied: + NetworkMessage::ShowError(NetworkMessage::PERMISSION_DENIED); + break; + case Network::RoomMember::Error::NoSuchUser: + NetworkMessage::ShowError(NetworkMessage::NO_SUCH_USER); + break; + } +} + void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { announce_multiplayer_session->Stop(); QMessageBox::warning( diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h index b375a81a6..78a36865b 100644 --- a/src/citra_qt/multiplayer/state.h +++ b/src/citra_qt/multiplayer/state.h @@ -40,6 +40,7 @@ public: public slots: void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnNetworkError(const Network::RoomMember::Error& error); void OnViewLobby(); void OnCreateRoom(); bool OnCloseRoom(); @@ -50,6 +51,7 @@ public slots: signals: void NetworkStateChanged(const Network::RoomMember::State&); + void NetworkError(const Network::RoomMember::Error&); void AnnounceFailed(const Common::WebResult&); private: @@ -65,6 +67,7 @@ private: std::shared_ptr announce_multiplayer_session; Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; Network::RoomMember::CallbackHandle state_callback_handle; + Network::RoomMember::CallbackHandle error_callback_handle; }; Q_DECLARE_METATYPE(Common::WebResult); diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 79ba71da8..40d7e7068 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -31,6 +31,7 @@ public: std::atomic state{State::Idle}; ///< Current state of the RoomMember. void SetState(const State new_state); + void SetError(const Error new_error); bool IsConnected() const; std::string nickname; ///< The nickname of this member. @@ -61,6 +62,8 @@ public: CallbackSet callback_set_status_messages; CallbackSet callback_set_room_information; CallbackSet callback_set_state; + CallbackSet callback_set_error; + CallbackSet callback_set_ban_list; }; Callbacks callbacks; ///< All CallbackSets to all events @@ -117,6 +120,12 @@ public: */ void HandleStatusMessagePacket(const ENetEvent* event); + /** + * Extracts a ban list request response from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleModBanListResponsePacket(const ENetEvent* event); + /** * Disconnects the RoomMember from the Room */ @@ -137,6 +146,10 @@ void RoomMember::RoomMemberImpl::SetState(const State new_state) { } } +void RoomMember::RoomMemberImpl::SetError(const Error new_error) { + Invoke(new_error); +} + bool RoomMember::RoomMemberImpl::IsConnected() const { return state == State::Joining || state == State::Joined; } @@ -170,32 +183,59 @@ void RoomMember::RoomMemberImpl::MemberLoop() { HandleJoinPacket(&event); // Get the MAC Address for the client SetState(State::Joined); break; + case IdModBanListResponse: + HandleModBanListResponsePacket(&event); + break; case IdRoomIsFull: - SetState(State::RoomIsFull); + SetState(State::Idle); + SetError(Error::RoomIsFull); break; case IdNameCollision: - SetState(State::NameCollision); + SetState(State::Idle); + SetError(Error::NameCollision); break; case IdMacCollision: - SetState(State::MacCollision); + SetState(State::Idle); + SetError(Error::MacCollision); break; case IdConsoleIdCollision: - SetState(State::ConsoleIdCollision); + SetState(State::Idle); + SetError(Error::ConsoleIdCollision); break; case IdVersionMismatch: - SetState(State::WrongVersion); + SetState(State::Idle); + SetError(Error::WrongVersion); break; case IdWrongPassword: - SetState(State::WrongPassword); + SetState(State::Idle); + SetError(Error::WrongPassword); break; case IdCloseRoom: - SetState(State::LostConnection); + SetState(State::Idle); + SetError(Error::LostConnection); + break; + case IdHostKicked: + SetState(State::Idle); + SetError(Error::HostKicked); + break; + case IdHostBanned: + SetState(State::Idle); + SetError(Error::HostBanned); + break; + case IdModPermissionDenied: + SetError(Error::PermissionDenied); + break; + case IdModNoSuchUser: + SetError(Error::NoSuchUser); break; } enet_packet_destroy(event.packet); break; case ENET_EVENT_TYPE_DISCONNECT: - SetState(State::LostConnection); + if (state == State::Joined) { + SetState(State::Idle); + SetError(Error::LostConnection); + } break; } } @@ -251,11 +291,13 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> info.member_slots; packet >> info.port; packet >> info.preferred_game; + packet >> info.host_username; room_information.name = info.name; room_information.description = info.description; room_information.member_slots = info.member_slots; room_information.port = info.port; room_information.preferred_game = info.preferred_game; + room_information.host_username = info.host_username; u32 num_members; packet >> num_members; @@ -344,6 +386,19 @@ void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* even Invoke(status_message_entry); } +void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + Room::BanList ban_list = {}; + packet >> ban_list.first; + packet >> ban_list.second; + Invoke(ban_list); +} + void RoomMember::RoomMemberImpl::Disconnect() { member_information.clear(); room_information.member_slots = 0; @@ -383,6 +438,12 @@ RoomMember::RoomMemberImpl::Callbacks::Get() { return callback_set_state; } +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_error; +} + template <> RoomMember::RoomMemberImpl::CallbackSet& RoomMember::RoomMemberImpl::Callbacks::Get() { @@ -400,6 +461,12 @@ RoomMember::RoomMemberImpl::Callbacks::Get() { return callback_set_status_messages; } +template <> +RoomMember::RoomMemberImpl::CallbackSet& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_ban_list; +} + template void RoomMember::RoomMemberImpl::Invoke(const T& data) { std::lock_guard lock(callback_mutex); @@ -481,7 +548,8 @@ void RoomMember::Join(const std::string& nick, const std::string& console_id_has enet_host_connect(room_member_impl->client, &address, NumChannels, 0); if (!room_member_impl->server) { - room_member_impl->SetState(State::Error); + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::UnknownError); return; } @@ -494,7 +562,8 @@ void RoomMember::Join(const std::string& nick, const std::string& console_id_has SendGameInfo(room_member_impl->current_game_info); } else { enet_peer_disconnect(room_member_impl->server, 0); - room_member_impl->SetState(State::CouldNotConnect); + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::CouldNotConnect); } } @@ -532,11 +601,37 @@ void RoomMember::SendGameInfo(const GameInfo& game_info) { room_member_impl->Send(std::move(packet)); } +void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) { + ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban, + "type is not a moderation request"); + if (!IsConnected()) + return; + + Packet packet; + packet << static_cast(type); + packet << nickname; + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::RequestBanList() { + if (!IsConnected()) + return; + + Packet packet; + packet << static_cast(IdModGetBanList); + room_member_impl->Send(std::move(packet)); +} + RoomMember::CallbackHandle RoomMember::BindOnStateChanged( std::function callback) { return room_member_impl->Bind(callback); } +RoomMember::CallbackHandle RoomMember::BindOnError( + std::function callback) { + return room_member_impl->Bind(callback); +} + RoomMember::CallbackHandle RoomMember::BindOnWifiPacketReceived( std::function callback) { return room_member_impl->Bind(callback); @@ -557,6 +652,11 @@ RoomMember::CallbackHandle RoomMember::BindOnStatusMessageRe return room_member_impl->Bind(callback); } +RoomMember::CallbackHandle RoomMember::BindOnBanListReceived( + std::function callback) { + return room_member_impl->Bind(callback); +} + template void RoomMember::Unbind(CallbackHandle handle) { std::lock_guard lock(room_member_impl->callback_mutex); @@ -574,8 +674,10 @@ void RoomMember::Leave() { template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); template void RoomMember::Unbind(CallbackHandle); +template void RoomMember::Unbind(CallbackHandle); } // namespace Network diff --git a/src/network/room_member.h b/src/network/room_member.h index 5062b225e..65d1c64eb 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -57,20 +57,30 @@ class RoomMember final { public: enum class State : u8 { Uninitialized, ///< Not initialized - Idle, ///< Default state - Error, ///< Some error [permissions to network device missing or something] + Idle, ///< Default state (i.e. not connected) Joining, ///< The client is attempting to join a room. Joined, ///< The client is connected to the room and is ready to send/receive packets. + }; + + enum class Error : u8 { + // Reasons why connection was closed LostConnection, ///< Connection closed + HostKicked, ///< Kicked by the host // Reasons why connection was rejected + UnknownError, ///< Some error [permissions to network device missing or something] NameCollision, ///< Somebody is already using this name MacCollision, ///< Somebody is already using that mac-address ConsoleIdCollision, ///< Somebody in the room has the same Console ID WrongVersion, ///< The room version is not the same as for this RoomMember WrongPassword, ///< The password doesn't match the one from the Room CouldNotConnect, ///< The room is not responding to a connection attempt - RoomIsFull ///< Room is already at the maximum number of players + RoomIsFull, ///< Room is already at the maximum number of players + HostBanned, ///< The user is banned by the host + + // Reasons why moderation request failed + PermissionDenied, ///< The user does not have mod permissions + NoSuchUser, ///< The nickname the user attempts to kick/ban does not exist }; struct MemberInformation { @@ -161,6 +171,19 @@ public: */ void SendGameInfo(const GameInfo& game_info); + /** + * Sends a moderation request to the room. + * @param type Moderation request type. + * @param nickname The subject of the request. (i.e. the user you want to kick/ban) + */ + void SendModerationRequest(RoomMessageTypes type, const std::string& nickname); + + /** + * Attempts to retrieve ban list from the room. + * If success, the ban list callback would be called. Otherwise an error would be emitted. + */ + void RequestBanList(); + /** * Binds a function to an event that will be triggered every time the State of the member * changed. The function wil be called every time the event is triggered. The callback function @@ -170,6 +193,15 @@ public: */ CallbackHandle BindOnStateChanged(std::function callback); + /** + * Binds a function to an event that will be triggered every time an error happened. The + * function wil be called every time the event is triggered. The callback function must not bind + * or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnError(std::function callback); + /** * Binds a function to an event that will be triggered every time a WifiPacket is received. * The function wil be called everytime the event is triggered. @@ -210,6 +242,16 @@ public: CallbackHandle BindOnStatusMessageReceived( std::function callback); + /** + * Binds a function to an event that will be triggered every time a requested ban list + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle BindOnBanListReceived( + std::function callback); + /** * Leaves the current room. */ @@ -224,24 +266,42 @@ static const char* GetStateStr(const RoomMember::State& s) { switch (s) { case RoomMember::State::Idle: return "Idle"; - case RoomMember::State::Error: - return "Error"; case RoomMember::State::Joining: return "Joining"; case RoomMember::State::Joined: return "Joined"; - case RoomMember::State::LostConnection: + } + return "Unknown"; +} + +static const char* GetErrorStr(const RoomMember::Error& e) { + switch (e) { + case RoomMember::Error::LostConnection: return "LostConnection"; - case RoomMember::State::NameCollision: + case RoomMember::Error::HostKicked: + return "HostKicked"; + case RoomMember::Error::UnknownError: + return "UnknownError"; + case RoomMember::Error::NameCollision: return "NameCollision"; - case RoomMember::State::MacCollision: - return "MacCollision"; - case RoomMember::State::WrongVersion: + case RoomMember::Error::MacCollision: + return "MaxCollision"; + case RoomMember::Error::ConsoleIdCollision: + return "ConsoleIdCollision"; + case RoomMember::Error::WrongVersion: return "WrongVersion"; - case RoomMember::State::WrongPassword: + case RoomMember::Error::WrongPassword: return "WrongPassword"; - case RoomMember::State::CouldNotConnect: + case RoomMember::Error::CouldNotConnect: return "CouldNotConnect"; + case RoomMember::Error::RoomIsFull: + return "RoomIsFull"; + case RoomMember::Error::HostBanned: + return "HostBanned"; + case RoomMember::Error::PermissionDenied: + return "PermissionDenied"; + case RoomMember::Error::NoSuchUser: + return "NoSuchUser"; } return "Unknown"; } From 6359b6094cbde9d5aa8afb4869af91c7c85dd168 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:22:14 +0800 Subject: [PATCH 16/27] citra_qt: Add a moderation dialog The dialog currently supports accessing the ban list and removing entries from it. --- src/citra_qt/CMakeLists.txt | 3 + src/citra_qt/multiplayer/client_room.cpp | 13 ++ src/citra_qt/multiplayer/client_room.h | 1 + src/citra_qt/multiplayer/client_room.ui | 10 ++ .../multiplayer/moderation_dialog.cpp | 113 ++++++++++++++++++ src/citra_qt/multiplayer/moderation_dialog.h | 42 +++++++ src/citra_qt/multiplayer/moderation_dialog.ui | 84 +++++++++++++ src/citra_qt/multiplayer/state.cpp | 6 + src/citra_qt/multiplayer/state.h | 1 + 9 files changed, 273 insertions(+) create mode 100644 src/citra_qt/multiplayer/moderation_dialog.cpp create mode 100644 src/citra_qt/multiplayer/moderation_dialog.h create mode 100644 src/citra_qt/multiplayer/moderation_dialog.ui diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 12affbd85..9eab29b3e 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -99,6 +99,8 @@ add_executable(citra-qt multiplayer/lobby.cpp multiplayer/message.h multiplayer/message.cpp + multiplayer/moderation_dialog.cpp + multiplayer/moderation_dialog.h multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h @@ -135,6 +137,7 @@ set(UIS multiplayer/chat_room.ui multiplayer/client_room.ui multiplayer/host_room.ui + multiplayer/moderation_dialog.ui aboutdialog.ui cheats.ui hotkeys.ui diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 16f9a58d7..54b4dc55e 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -13,6 +13,7 @@ #include "citra_qt/game_list_p.h" #include "citra_qt/multiplayer/client_room.h" #include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/moderation_dialog.h" #include "citra_qt/multiplayer/state.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" @@ -42,11 +43,23 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); }); ui->disconnect->setDefault(false); ui->disconnect->setAutoDefault(false); + connect(ui->moderation, &QPushButton::clicked, [this] { + ModerationDialog dialog(this); + dialog.exec(); + }); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); UpdateView(); } ClientRoomWindow::~ClientRoomWindow() = default; +void ClientRoomWindow::SetModPerms(bool is_mod) { + ui->moderation->setVisible(is_mod); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); +} + void ClientRoomWindow::RetranslateUi() { ui->retranslateUi(this); ui->chat->RetranslateUi(); diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index 47add6f51..7d4f2b238 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -18,6 +18,7 @@ public: ~ClientRoomWindow(); void RetranslateUi(); + void SetModPerms(bool is_mod); public slots: void OnRoomUpdate(const Network::RoomInformation&); diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui index 22b969d3b..97e88b502 100644 --- a/src/citra_qt/multiplayer/client_room.ui +++ b/src/citra_qt/multiplayer/client_room.ui @@ -41,6 +41,16 @@ + + + + Moderation... + + + false + + + diff --git a/src/citra_qt/multiplayer/moderation_dialog.cpp b/src/citra_qt/multiplayer/moderation_dialog.cpp new file mode 100644 index 000000000..def084666 --- /dev/null +++ b/src/citra_qt/multiplayer/moderation_dialog.cpp @@ -0,0 +1,113 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/multiplayer/moderation_dialog.h" +#include "network/network.h" +#include "network/room_member.h" +#include "ui_moderation_dialog.h" + +namespace Column { +enum { + SUBJECT, + TYPE, + COUNT, +}; +} + +ModerationDialog::ModerationDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + + qRegisterMetaType(); + + if (auto member = Network::GetRoomMember().lock()) { + callback_handle_status_message = member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ModerationDialog::StatusMessageReceived, this, + &ModerationDialog::OnStatusMessageReceived); + callback_handle_ban_list = member->BindOnBanListReceived( + [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); }); + connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList); + } + + // Initialize the UI + model = new QStandardItemModel(ui->ban_list_view); + model->insertColumns(0, Column::COUNT); + model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject")); + model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type")); + + ui->ban_list_view->setModel(model); + + // Load the ban list in background + LoadBanList(); + + connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); }); + connect(ui->unban, &QPushButton::clicked, this, [this] { + auto index = ui->ban_list_view->currentIndex(); + SendUnbanRequest(model->item(index.row(), 0)->text()); + }); + connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); }); +} + +ModerationDialog::~ModerationDialog() { + if (callback_handle_status_message) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_status_message); + } + } + + if (callback_handle_ban_list) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_ban_list); + } + } +} + +void ModerationDialog::LoadBanList() { + if (auto room = Network::GetRoomMember().lock()) { + ui->refresh->setEnabled(false); + ui->refresh->setText(tr("Refreshing")); + ui->unban->setEnabled(false); + room->RequestBanList(); + } +} + +void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { + model->removeRows(0, model->rowCount()); + for (const auto& username : ban_list.first) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username)); + QStandardItem* type_item = new QStandardItem(tr("Forum Username")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (const auto& ip : ban_list.second) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip)); + QStandardItem* type_item = new QStandardItem(tr("IP Address")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (int i = 0; i < Column::COUNT - 1; ++i) { + ui->ban_list_view->resizeColumnToContents(i); + } + ui->refresh->setEnabled(true); + ui->refresh->setText(tr("Refresh")); + ui->unban->setEnabled(false); +} + +void ModerationDialog::SendUnbanRequest(const QString& subject) { + if (auto room = Network::GetRoomMember().lock()) { + room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); + } +} + +void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) { + if (status_message.type != Network::IdMemberBanned && + status_message.type != Network::IdAddressUnbanned) + return; + + // Update the ban list for ban/unban + LoadBanList(); +} diff --git a/src/citra_qt/multiplayer/moderation_dialog.h b/src/citra_qt/multiplayer/moderation_dialog.h new file mode 100644 index 000000000..d10083d5b --- /dev/null +++ b/src/citra_qt/multiplayer/moderation_dialog.h @@ -0,0 +1,42 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "network/room.h" +#include "network/room_member.h" + +namespace Ui { +class ModerationDialog; +} + +class QStandardItemModel; + +class ModerationDialog : public QDialog { + Q_OBJECT + +public: + explicit ModerationDialog(QWidget* parent = nullptr); + ~ModerationDialog(); + +signals: + void StatusMessageReceived(const Network::StatusMessageEntry&); + void BanListReceived(const Network::Room::BanList&); + +private: + void LoadBanList(); + void PopulateBanList(const Network::Room::BanList& ban_list); + void SendUnbanRequest(const QString& subject); + void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message); + + std::unique_ptr ui; + QStandardItemModel* model; + Network::RoomMember::CallbackHandle callback_handle_status_message; + Network::RoomMember::CallbackHandle callback_handle_ban_list; +}; + +Q_DECLARE_METATYPE(Network::Room::BanList); diff --git a/src/citra_qt/multiplayer/moderation_dialog.ui b/src/citra_qt/multiplayer/moderation_dialog.ui new file mode 100644 index 000000000..808d99414 --- /dev/null +++ b/src/citra_qt/multiplayer/moderation_dialog.ui @@ -0,0 +1,84 @@ + + + ModerationDialog + + + Moderation + + + + 0 + 0 + 500 + 300 + + + + + + + Ban List + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refreshing + + + false + + + + + + + Unban + + + false + + + + + + + + + + + + + + + QDialogButtonBox::Ok + + + + + + + + buttonBox + accepted() + ModerationDialog + accept() + + + + diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 14b2bd7ec..996af55b3 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -226,6 +226,12 @@ void MultiplayerState::OnOpenNetworkRoom() { if (client_room == nullptr) { client_room = new ClientRoomWindow(this); } + const std::string host_username = member->GetRoomInformation().host_username; + if (host_username.empty()) { + client_room->SetModPerms(false); + } else { + client_room->SetModPerms(member->GetUsername() == host_username); + } BringWidgetToFront(client_room); return; } diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h index 78a36865b..a49288e83 100644 --- a/src/citra_qt/multiplayer/state.h +++ b/src/citra_qt/multiplayer/state.h @@ -66,6 +66,7 @@ private: QAction* show_room; std::shared_ptr announce_multiplayer_session; Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; + bool has_mod_perms = false; Network::RoomMember::CallbackHandle state_callback_handle; Network::RoomMember::CallbackHandle error_callback_handle; }; From 15540df14095dea37647def844a7a07e96accefa Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:26:12 +0800 Subject: [PATCH 17/27] citra_qt/multiplayer/chat_room: Add moderation to context menu --- src/citra_qt/multiplayer/chat_room.cpp | 57 +++++++++++++++++++++++- src/citra_qt/multiplayer/chat_room.h | 5 +++ src/citra_qt/multiplayer/client_room.cpp | 1 + 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index ff12b3f1a..94dc6c357 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -160,6 +160,10 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_uniqueretranslateUi(this); } @@ -177,6 +181,21 @@ void ChatRoom::AppendChatMessage(const QString& msg) { ui->chat_history->append(msg); } +void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { + if (auto room = Network::GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&nickname](const Network::RoomMember::MemberInformation& member) { + return member.nickname == nickname; + }); + if (it == members.end()) { + NetworkMessage::ShowError(NetworkMessage::NO_SUCH_USER); + return; + } + room->SendModerationRequest(type, nickname); + } +} + bool ChatRoom::ValidateMessage(const std::string& msg) { return !msg.empty(); } @@ -241,6 +260,15 @@ void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_ case Network::IdMemberLeave: message = tr("%1 has left").arg(name); break; + case Network::IdMemberKicked: + message = tr("%1 has been kicked").arg(name); + break; + case Network::IdMemberBanned: + message = tr("%1 has been banned").arg(name); + break; + case Network::IdAddressUnbanned: + message = tr("%1 has been unbanned").arg(name); + break; } if (!message.isEmpty()) AppendStatusMessage(message); @@ -351,7 +379,7 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { std::string nickname = player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); if (auto room = Network::GetRoomMember().lock()) { - // You can't block yourself + // You can't block, kick or ban yourself if (nickname == room->GetNickname()) return; } @@ -377,5 +405,32 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { } }); + if (has_mod_perms) { + context_menu.addSeparator(); + + QAction* kick_action = context_menu.addAction(tr("Kick")); + QAction* ban_action = context_menu.addAction(tr("Ban")); + + connect(kick_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = + QMessageBox::question(this, tr("Kick Player"), + tr("Are you sure you would like to kick %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModKick, nickname); + }); + connect(ban_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Ban Player"), + tr("Are you sure you would like to kick and ban %1?\n\nThis would " + "ban both their forum username and their IP address.") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModBan, nickname); + }); + } + context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); } diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index 7a7c84b48..58cb25e83 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -36,6 +36,8 @@ public: void AppendStatusMessage(const QString& msg); ~ChatRoom(); + void SetModPerms(bool is_mod); + public slots: void OnRoomUpdate(const Network::RoomInformation& info); void OnChatReceive(const Network::ChatEntry&); @@ -54,8 +56,10 @@ private: static constexpr u32 max_chat_lines = 1000; void AppendChatMessage(const QString&); bool ValidateMessage(const std::string&); + void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); void UpdateIconDisplay(); + bool has_mod_perms = false; QStandardItemModel* player_list; std::unique_ptr ui; std::unordered_set block_list; @@ -66,3 +70,4 @@ Q_DECLARE_METATYPE(Network::ChatEntry); Q_DECLARE_METATYPE(Network::StatusMessageEntry); Q_DECLARE_METATYPE(Network::RoomInformation); Q_DECLARE_METATYPE(Network::RoomMember::State); +Q_DECLARE_METATYPE(Network::RoomMember::Error); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 54b4dc55e..84a425189 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -55,6 +55,7 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) ClientRoomWindow::~ClientRoomWindow() = default; void ClientRoomWindow::SetModPerms(bool is_mod) { + ui->chat->SetModPerms(is_mod); ui->moderation->setVisible(is_mod); ui->moderation->setDefault(false); ui->moderation->setAutoDefault(false); From deb398d190459b1fcdb238c12b9a7a9b9df6b344 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:22:49 +0800 Subject: [PATCH 18/27] citra_qt: Save ban list for room hosting --- src/citra_qt/configuration/config.cpp | 28 ++++++++++++++++++++++++++ src/citra_qt/multiplayer/host_room.cpp | 9 +++++++-- src/citra_qt/multiplayer/host_room.ui | 14 +++++++++++++ src/citra_qt/multiplayer/state.cpp | 5 +++++ src/citra_qt/ui_settings.h | 3 +++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 5a69a2d30..dcbe18122 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -330,6 +330,21 @@ void Config::ReadValues() { UISettings::values.max_player = ReadSetting("max_player", 8).toUInt(); UISettings::values.game_id = ReadSetting("game_id", 0).toULongLong(); UISettings::values.room_description = ReadSetting("room_description", "").toString(); + // Read ban list back + size = qt_config->beginReadArray("username_ban_list"); + UISettings::values.ban_list.first.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.first[i] = ReadSetting("username").toString().toStdString(); + } + qt_config->endArray(); + size = qt_config->beginReadArray("ip_ban_list"); + UISettings::values.ban_list.second.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.second[i] = ReadSetting("ip").toString().toStdString(); + } + qt_config->endArray(); qt_config->endGroup(); qt_config->endGroup(); @@ -535,6 +550,19 @@ void Config::SaveValues() { WriteSetting("max_player", UISettings::values.max_player, 8); WriteSetting("game_id", UISettings::values.game_id, 0); WriteSetting("room_description", UISettings::values.room_description, ""); + // Write ban list + qt_config->beginWriteArray("username_ban_list"); + for (std::size_t i = 0; i < UISettings::values.ban_list.first.size(); ++i) { + qt_config->setArrayIndex(i); + WriteSetting("username", QString::fromStdString(UISettings::values.ban_list.first[i])); + } + qt_config->endArray(); + qt_config->beginWriteArray("ip_ban_list"); + for (std::size_t i = 0; i < UISettings::values.ban_list.second.size(); ++i) { + qt_config->setArrayIndex(i); + WriteSetting("ip", QString::fromStdString(UISettings::values.ban_list.second[i])); + } + qt_config->endArray(); qt_config->endGroup(); qt_config->endGroup(); diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 27ab37f46..d5f093e48 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -127,11 +127,16 @@ void HostRoomWindow::Host() { 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; + Network::Room::BanList ban_list{}; + if (ui->load_ban_list->isChecked()) { + ban_list = UISettings::values.ban_list; + } 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, CreateVerifyBackend(is_public)); + password, ui->max_player->value(), + Settings::values.citra_username, game_name.toStdString(), + game_id, CreateVerifyBackend(is_public), ban_list); if (!created) { NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); LOG_ERROR(Network, "Could not create room!"); diff --git a/src/citra_qt/multiplayer/host_room.ui b/src/citra_qt/multiplayer/host_room.ui index 5084ef7f4..d54cf49c6 100644 --- a/src/citra_qt/multiplayer/host_room.ui +++ b/src/citra_qt/multiplayer/host_room.ui @@ -145,6 +145,20 @@ + + + + + + Load Previous Ban List + + + true + + + + + diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 996af55b3..2970c0691 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -13,6 +13,7 @@ #include "citra_qt/multiplayer/lobby.h" #include "citra_qt/multiplayer/message.h" #include "citra_qt/multiplayer/state.h" +#include "citra_qt/ui_settings.h" #include "citra_qt/util/clickable_label.h" #include "common/announce_multiplayer_room.h" #include "common/logging/log.h" @@ -213,6 +214,10 @@ bool MultiplayerState::OnCloseRoom() { if (room->GetState() != Network::Room::State::Open) { return true; } + // Save ban list + if (auto room = Network::GetRoom().lock()) { + UISettings::values.ban_list = std::move(room->GetBanList()); + } room->Destroy(); announce_multiplayer_session->Stop(); LOG_DEBUG(Frontend, "Closed the room (as a server)"); diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index b26d69364..d623b14da 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -5,6 +5,8 @@ #pragma once #include +#include +#include #include #include #include @@ -110,6 +112,7 @@ struct Values { uint host_type; qulonglong game_id; QString room_description; + std::pair, std::vector> ban_list; // logging bool show_console; From bd29f1facbc9ea561797cf561fd4e015ee9ed5d3 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:26:52 +0800 Subject: [PATCH 19/27] dedicated_room: load and save ban list The ban list is stored in a format so-called CitraRoom-BanList-1 and just first stores username ban list, one entry per line, then an empty line and then store the ip ban list. --- src/dedicated_room/citra-room.cpp | 96 ++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/src/dedicated_room/citra-room.cpp b/src/dedicated_room/citra-room.cpp index 9246b5797..8e2938006 100644 --- a/src/dedicated_room/citra-room.cpp +++ b/src/dedicated_room/citra-room.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -27,10 +28,12 @@ #include "common/common_types.h" #include "common/detached_tasks.h" #include "common/scm_rev.h" +#include "common/string_util.h" #include "core/announce_multiplayer_session.h" #include "core/core.h" #include "core/settings.h" #include "network/network.h" +#include "network/room.h" #include "network/verify_user.h" #ifdef ENABLE_WEB_SERVICE @@ -50,6 +53,7 @@ static void PrintHelp(const char* argv0) { "--username The username used for announce\n" "--token The token used for announce\n" "--web-api-url Citra Web API url\n" + "--ban-list-file The file for storing the room ban list\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n"; } @@ -59,6 +63,71 @@ static void PrintVersion() { << " Libnetwork: " << Network::network_version << std::endl; } +/// The magic text at the beginning of a citra-room ban list file. +static constexpr char BanListMagic[] = "CitraRoom-BanList-1"; + +static Network::Room::BanList LoadBanList(const std::string& path) { + std::ifstream file; + OpenFStream(file, path, std::ios_base::in); + if (!file || file.eof()) { + std::cout << "Could not open ban list!\n\n"; + return {}; + } + std::string magic; + std::getline(file, magic); + if (magic != BanListMagic) { + std::cout << "Ban list is not valid!\n\n"; + return {}; + } + + // false = username ban list, true = ip ban list + bool ban_list_type = false; + Network::Room::UsernameBanList username_ban_list; + Network::Room::IPBanList ip_ban_list; + while (!file.eof()) { + std::string line; + std::getline(file, line); + line.erase(std::remove(line.begin(), line.end(), '\0'), line.end()); + line = Common::StripSpaces(line); + if (line.empty()) { + // An empty line marks start of the IP ban list + ban_list_type = true; + continue; + } + if (ban_list_type) { + ip_ban_list.emplace_back(line); + } else { + username_ban_list.emplace_back(line); + } + } + + return {username_ban_list, ip_ban_list}; +} + +static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) { + std::ofstream file; + OpenFStream(file, path, std::ios_base::out); + if (!file) { + std::cout << "Could not save ban list!\n\n"; + return; + } + + file << BanListMagic << "\n"; + + // Username ban list + for (const auto& username : ban_list.first) { + file << username << "\n"; + } + file << "\n"; + + // IP ban list + for (const auto& ip : ban_list.second) { + file << ip << "\n"; + } + + file.flush(); +} + /// Application entry point int main(int argc, char** argv) { Common::DetachedTasks detached_tasks; @@ -75,6 +144,7 @@ int main(int argc, char** argv) { std::string username; std::string token; std::string web_api_url; + std::string ban_list_file; u64 preferred_game_id = 0; u32 port = Network::DefaultRoomPort; u32 max_members = 16; @@ -90,6 +160,7 @@ int main(int argc, char** argv) { {"username", required_argument, 0, 'u'}, {"token", required_argument, 0, 't'}, {"web-api-url", required_argument, 0, 'a'}, + {"ban-list-file", required_argument, 0, 'b'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {0, 0, 0, 0}, @@ -129,6 +200,9 @@ int main(int argc, char** argv) { case 'a': web_api_url.assign(optarg); break; + case 'b': + ban_list_file.assign(optarg); + break; case 'h': PrintHelp(argv[0]); return 0; @@ -164,6 +238,10 @@ int main(int argc, char** argv) { PrintHelp(argv[0]); return -1; } + if (ban_list_file.empty()) { + std::cout << "Ban list file not set!\nThis should get set to load and save room ban " + "list.\nSet with --ban-list-file \n\n"; + } bool announce = true; if (username.empty()) { announce = false; @@ -184,6 +262,12 @@ int main(int argc, char** argv) { Settings::values.citra_token = token; } + // Load the ban list + Network::Room::BanList ban_list; + if (!ban_list_file.empty()) { + ban_list = LoadBanList(ban_list_file); + } + std::unique_ptr verify_backend; if (announce) { #ifdef ENABLE_WEB_SERVICE @@ -199,8 +283,8 @@ int main(int argc, char** argv) { Network::Init(); if (std::shared_ptr room = Network::GetRoom().lock()) { - if (!room->Create(room_name, room_description, "", port, password, max_members, - preferred_game, preferred_game_id, std::move(verify_backend))) { + if (!room->Create(room_name, room_description, "", port, password, max_members, username, + preferred_game, preferred_game_id, std::move(verify_backend), ban_list)) { std::cout << "Failed to create room: \n\n"; return -1; } @@ -217,6 +301,10 @@ int main(int argc, char** argv) { announce_session->Stop(); } announce_session.reset(); + // Save the ban list + if (!ban_list_file.empty()) { + SaveBanList(room->GetBanList(), ban_list_file); + } room->Destroy(); Network::Shutdown(); return 0; @@ -227,6 +315,10 @@ int main(int argc, char** argv) { announce_session->Stop(); } announce_session.reset(); + // Save the ban list + if (!ban_list_file.empty()) { + SaveBanList(room->GetBanList(), ban_list_file); + } room->Destroy(); } Network::Shutdown(); From 6feeaed77ef31623749e0fd5208ea73aba9b0925 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 24 Nov 2018 16:32:29 +0800 Subject: [PATCH 20/27] citra: add errors callback and add status message types --- src/citra/citra.cpp | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index 631e4d766..a0a661887 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -81,42 +81,53 @@ static void OnStateChanged(const Network::RoomMember::State& state) { case Network::RoomMember::State::Joined: LOG_DEBUG(Network, "Successfully joined to the room"); break; - case Network::RoomMember::State::LostConnection: + default: + break; + } +} + +static void OnNetworkError(const Network::RoomMember::Error& error) { + switch (error) { + case Network::RoomMember::Error::LostConnection: LOG_DEBUG(Network, "Lost connection to the room"); break; - case Network::RoomMember::State::CouldNotConnect: - LOG_ERROR(Network, "State: CouldNotConnect"); + case Network::RoomMember::Error::CouldNotConnect: + LOG_ERROR(Network, "Error: Could not connect"); exit(1); break; - case Network::RoomMember::State::NameCollision: + case Network::RoomMember::Error::NameCollision: LOG_ERROR( Network, "You tried to use the same nickname as another user that is connected to the Room"); exit(1); break; - case Network::RoomMember::State::MacCollision: + case Network::RoomMember::Error::MacCollision: LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " "connected to the Room"); exit(1); break; - case Network::RoomMember::State::ConsoleIdCollision: + case Network::RoomMember::Error::ConsoleIdCollision: LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); exit(1); break; - case Network::RoomMember::State::WrongPassword: + case Network::RoomMember::Error::WrongPassword: LOG_ERROR(Network, "Room replied with: Wrong password"); exit(1); break; - case Network::RoomMember::State::WrongVersion: + case Network::RoomMember::Error::WrongVersion: LOG_ERROR(Network, "You are using a different version than the room you are trying to connect to"); exit(1); break; - case Network::RoomMember::State::RoomIsFull: + case Network::RoomMember::Error::RoomIsFull: LOG_ERROR(Network, "The room is full"); exit(1); break; - default: + case Network::RoomMember::Error::HostKicked: + LOG_ERROR(Network, "You have been kicked by the host"); + break; + case Network::RoomMember::Error::HostBanned: + LOG_ERROR(Network, "You have been banned by the host"); break; } } @@ -134,6 +145,15 @@ static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { case Network::IdMemberLeave: message = fmt::format("{} has left", msg.nickname); break; + case Network::IdMemberKicked: + message = fmt::format("{} has been kicked", msg.nickname); + break; + case Network::IdMemberBanned: + message = fmt::format("{} has been banned", msg.nickname); + break; + case Network::IdAddressUnbanned: + message = fmt::format("{} has been unbanned", msg.nickname); + break; } if (!message.empty()) std::cout << std::endl << "* " << message << std::endl << std::endl; @@ -358,6 +378,7 @@ int main(int argc, char** argv) { member->BindOnChatMessageRecieved(OnMessageReceived); member->BindOnStatusMessageReceived(OnStatusMessageReceived); member->BindOnStateChanged(OnStateChanged); + member->BindOnError(OnNetworkError); LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname); member->Join(nickname, Service::CFG::GetConsoleIdHash(system), address.c_str(), port, 0, From 8b8b39ec0e3a5322a85fdd7c1bb5f0d22d2c433e Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 1 Dec 2018 09:28:55 +0800 Subject: [PATCH 21/27] citra_qt/multiplayer: Add user ping support The user would be notified if the message contains "@" followed by the user's nickname or forum username. An alert would be shown, and the icon and message in the status bar would be changed. All notification is only shown if the chat window currently does not have focus. Also added a connected_notification icon for showing in the status bar. --- dist/license.md | 3 ++ .../icons/16x16/connected_notification.png | Bin 0 -> 607 bytes dist/qt_themes/colorful/style.qrc | 1 + dist/qt_themes/colorful_dark/style.qrc | 1 + dist/qt_themes/default/default.qrc | 2 ++ .../icons/16x16/connected_notification.png | Bin 0 -> 517 bytes .../icons/16x16/connected_notification.png | Bin 0 -> 526 bytes dist/qt_themes/qdarkstyle/style.qrc | 1 + license.txt | 1 + src/citra_qt/multiplayer/chat_room.cpp | 34 ++++++++++++++++-- src/citra_qt/multiplayer/chat_room.h | 1 + src/citra_qt/multiplayer/client_room.cpp | 1 + src/citra_qt/multiplayer/client_room.h | 1 + src/citra_qt/multiplayer/state.cpp | 29 ++++++++++++++- src/citra_qt/multiplayer/state.h | 4 +++ 15 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 dist/qt_themes/colorful/icons/16x16/connected_notification.png create mode 100644 dist/qt_themes/default/icons/16x16/connected_notification.png create mode 100644 dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png diff --git a/dist/license.md b/dist/license.md index f7f74ab5a..3300388a7 100644 --- a/dist/license.md +++ b/dist/license.md @@ -4,6 +4,7 @@ Icon Name | License | Origin/Author --- | --- | --- qt_themes/default/icons/16x16/checked.png | Free for non-commercial use qt_themes/default/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/16x16/failed.png | Free for non-commercial use qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com @@ -16,6 +17,7 @@ qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from 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 qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/failed.png | Free for non-commercial use qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com @@ -27,6 +29,7 @@ qt_themes/qdarkstyle/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.c 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 +qt_themes/colorful/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com diff --git a/dist/qt_themes/colorful/icons/16x16/connected_notification.png b/dist/qt_themes/colorful/icons/16x16/connected_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..0dfe032d58160f1a24f54040964b5b5f2e97a724 GIT binary patch literal 607 zcmV-l0-*hgP)Al}+;~l5Q zvTbMg9^UuieCIpx-&Ifi=FyAs#93blXAAVk6T1cLkfNhvWKhv_s&Zmhz^+*5hT57% zOF_;n8U!$wS#BAtE4F_}x9_uN3h#||R;qIF^}7#eT+d|uz94mKnfhXH31H9huIfo8 zi^zay+&fR64K1E@-JxlR%=;^#xv6PIV?+G_s+UCMg=h4^)8}bd)f^GE)~e{qV!srN z3)gS0wp1Shp|S7zWIB^sFUGWeiInyCIgX))T9mw`_&DPPf>r?`06RRtwXvb@^0G)- zvhTjLk|#o z6b%F40B icons/index.theme icons/16x16/connected.png + icons/16x16/connected_notification.png icons/16x16/disconnected.png icons/16x16/lock.png icons/48x48/bad_folder.png diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc index 00a7598fe..9c531fe1b 100644 --- a/dist/qt_themes/colorful_dark/style.qrc +++ b/dist/qt_themes/colorful_dark/style.qrc @@ -2,6 +2,7 @@ icons/index.theme ../colorful/icons/16x16/connected.png + ../colorful/icons/16x16/connected_notification.png ../colorful/icons/16x16/disconnected.png icons/16x16/lock.png ../colorful/icons/48x48/bad_folder.png diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index 4840532a2..cf011680f 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -10,6 +10,8 @@ icons/16x16/disconnected.png + icons/16x16/connected_notification.png + icons/16x16/lock.png icons/48x48/bad_folder.png diff --git a/dist/qt_themes/default/icons/16x16/connected_notification.png b/dist/qt_themes/default/icons/16x16/connected_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..e64901378b000db1871d03db88af5f06c454cb01 GIT binary patch literal 517 zcmV+g0{Z=lP)=oH5kv??W6KBf~W^uD5aJbuU z?1_l^zTebLSsabzI+llOtNV%$o|VX}coVWS{*HQ<)uxI9Ck6S4=9S_D*7>#60 zKOeWZFLYif{X3q+e8sGmn$7ygk(FzSW2Em%_GsHe=P~>O_ZNK92NF;*00000NkvXX Hu0mjf^1s=j literal 0 HcmV?d00001 diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png b/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd8b9d2930d99340778360c68bf28923a66609c GIT binary patch literal 526 zcmV+p0`dKcP)2WpUc5F_z--H^xh8V++PPa z33XEKY?gdgT~jBj|8aa2I1da0uYrTWBVZ~bo|TbKsi966PC1e~wUOg4^}G7fa*Xuu zD}ChCz!%^SaA1RG7s}j2{h+R>H`N~{31-R!rn93J^+qAGq+VB#t4(!Zk5kRaL%_XE zZwD|7j010%$4)l2G8(}uq%QEa{|BSMKwqd&%+I&Gt@=}pSAmByUBvjj+&Xa?AhHBA zg=3C-cdUL$odY1E5y>+Kw}I7x%KDK!!OK4KI`Cf7rGGvQYAIF5bAb`SY)*g}wJhau z3ikh)8lzs_F#r7`PS@PYUsN&*fWd_-?rE zoG=u!$6)85fB0bU!s5G7mw@{bF;~)+PNPxVI=FT#;4H+?AU)r`u=oPtH#>3~=2X*b QzyJUM07*qoM6N<$g7iq=m;e9( literal 0 HcmV?d00001 diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc index c151238e1..0a4424b7f 100644 --- a/dist/qt_themes/qdarkstyle/style.qrc +++ b/dist/qt_themes/qdarkstyle/style.qrc @@ -3,6 +3,7 @@ icons/index.theme icons/16x16/connected.png icons/16x16/disconnected.png + icons/16x16/connected_notification.png icons/16x16/lock.png icons/48x48/bad_folder.png icons/48x48/chip.png diff --git a/license.txt b/license.txt index fe47594db..93078088c 100644 --- a/license.txt +++ b/license.txt @@ -345,6 +345,7 @@ Icon Name | License | Origin/Author --- | --- | --- checked.png | Free for non-commercial use connected.png | CC BY-ND 3.0 | https://icons8.com +connected_notification.png | CC BY-ND 3.0 | https://icons8.com disconnected.png | CC BY-ND 3.0 | https://icons8.com failed.png | Free for non-commercial use lock.png | CC BY-ND 3.0 | https://icons8.com diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 94dc6c357..4c4150d6a 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -34,6 +34,24 @@ public: nickname = QString::fromStdString(chat.nickname); username = QString::fromStdString(chat.username); message = QString::fromStdString(chat.message); + + // Check for user pings + QString cur_nickname, cur_username; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = QString::fromStdString(room->GetNickname()); + cur_username = QString::fromStdString(room->GetUsername()); + } + if (message.contains(QString("@").append(cur_nickname)) || + (!cur_username.isEmpty() && message.contains(QString("@").append(cur_username)))) { + + contains_ping = true; + } else { + contains_ping = false; + } + } + + bool ContainsPing() const { + return contains_ping; } /// Format the message using the players color @@ -45,19 +63,28 @@ public: } else { name = QString("%1 (%2)").arg(nickname, username); } - return QString("[%1] <%3> %4") - .arg(timestamp, color, name.toHtmlEscaped(), message.toHtmlEscaped()); + + QString style; + if (ContainsPing()) { + // Add a background color to these messages + style = QString("background-color: %1").arg(ping_color); + } + + return QString("[%1] <%3> %5") + .arg(timestamp, color, name.toHtmlEscaped(), style, message.toHtmlEscaped()); } private: static constexpr std::array player_color = { {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + static constexpr char ping_color[] = "#FFFF00"; QString timestamp; QString nickname; QString username; QString message; + bool contains_ping; }; class StatusMessage { @@ -240,6 +267,9 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { } auto player = std::distance(members.begin(), it); ChatMessage m(chat); + if (m.ContainsPing()) { + emit UserPinged(); + } AppendChatMessage(m.GetPlayerChatMessage(player)); } } diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index 58cb25e83..70f786dbe 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -51,6 +51,7 @@ public slots: signals: void ChatReceived(const Network::ChatEntry&); void StatusMessageReceived(const Network::StatusMessageEntry&); + void UserPinged(); private: static constexpr u32 max_chat_lines = 1000; diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 84a425189..d87a3e6e1 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -49,6 +49,7 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) }); ui->moderation->setDefault(false); ui->moderation->setAutoDefault(false); + connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); UpdateView(); } diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index 7d4f2b238..c40d324c3 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -27,6 +27,7 @@ public slots: signals: void RoomInformationChanged(const Network::RoomInformation&); void StateChanged(const Network::RoomMember::State&); + void ShowNotification(); private: void Disconnect(); diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 2970c0691..b14e79f9b 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -49,6 +50,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + + connect(static_cast(QApplication::instance()), &QApplication::focusChanged, this, + [this](QWidget* /*old*/, QWidget* now) { + if (client_room && client_room->isAncestorOf(now)) { + HideNotification(); + } + }); } MultiplayerState::~MultiplayerState() { @@ -173,7 +181,9 @@ void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { } void MultiplayerState::UpdateThemedIcons() { - if (current_state == Network::RoomMember::State::Joined) { + if (show_notification) { + status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16)); + } else if (current_state == Network::RoomMember::State::Joined) { status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); } else { status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); @@ -225,11 +235,28 @@ bool MultiplayerState::OnCloseRoom() { return true; } +void MultiplayerState::ShowNotification() { + if (client_room && client_room->isAncestorOf(QApplication::focusWidget())) + return; // Do not show notification if the chat window currently has focus + show_notification = true; + QApplication::alert(nullptr); + status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16)); + status_text->setText(tr("New Messages Received")); +} + +void MultiplayerState::HideNotification() { + show_notification = false; + status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); + status_text->setText(tr("Connected")); +} + void MultiplayerState::OnOpenNetworkRoom() { if (auto member = Network::GetRoomMember().lock()) { if (member->IsConnected()) { if (client_room == nullptr) { client_room = new ClientRoomWindow(this); + connect(client_room, &ClientRoomWindow::ShowNotification, this, + &MultiplayerState::ShowNotification); } const std::string host_username = member->GetRoomInformation().host_username; if (host_username.empty()) { diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h index a49288e83..8061d18cf 100644 --- a/src/citra_qt/multiplayer/state.h +++ b/src/citra_qt/multiplayer/state.h @@ -48,6 +48,8 @@ public slots: void OnDirectConnectToRoom(); void OnAnnounceFailed(const Common::WebResult&); void UpdateThemedIcons(); + void ShowNotification(); + void HideNotification(); signals: void NetworkStateChanged(const Network::RoomMember::State&); @@ -69,6 +71,8 @@ private: bool has_mod_perms = false; Network::RoomMember::CallbackHandle state_callback_handle; Network::RoomMember::CallbackHandle error_callback_handle; + + bool show_notification = false; }; Q_DECLARE_METATYPE(Common::WebResult); From 94be4050bc5f53df05b301719ffb269b999b8c99 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 15 Dec 2018 14:37:23 +0800 Subject: [PATCH 22/27] network/packet: Fix reading vectors/arrays of strings Previously would break here, as it is trying to initialize a string with 0, which is then considered NULL. --- src/network/packet.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/packet.h b/src/network/packet.h index 5a2e58dc2..7bdc3da95 100644 --- a/src/network/packet.h +++ b/src/network/packet.h @@ -126,7 +126,7 @@ Packet& Packet::operator>>(std::vector& out_data) { // Then extract the data for (std::size_t i = 0; i < out_data.size(); ++i) { - T character = 0; + T character; *this >> character; out_data[i] = character; } @@ -136,7 +136,7 @@ Packet& Packet::operator>>(std::vector& out_data) { template Packet& Packet::operator>>(std::array& out_data) { for (std::size_t i = 0; i < out_data.size(); ++i) { - T character = 0; + T character; *this >> character; out_data[i] = character; } From 9d062d63da522792e008a8c3d13ce9b459407dc7 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 15 Dec 2018 17:13:46 +0800 Subject: [PATCH 23/27] network, citra_qt: Give moderation permission to community mods Based on the `roles` payload in the JWT, the rooms will now give mod permission to Citra Community Moderators. To notify the client of its permissions, a new response, IdJoinSuccessAsMod is added, and there's now a new RoomMember::State called Moderator. --- src/citra/citra.cpp | 3 ++ src/citra_qt/multiplayer/chat_room.cpp | 4 ++- src/citra_qt/multiplayer/client_room.cpp | 5 +++- src/citra_qt/multiplayer/client_room.h | 2 +- src/citra_qt/multiplayer/direct_connect.cpp | 6 ++-- src/citra_qt/multiplayer/host_room.cpp | 2 +- src/citra_qt/multiplayer/lobby.cpp | 2 +- src/citra_qt/multiplayer/state.cpp | 18 ++++++------ src/core/hle/service/nwm/nwm_uds.cpp | 4 ++- src/network/room.cpp | 31 +++++++++++++++++---- src/network/room.h | 1 + src/network/room_member.cpp | 12 +++++--- src/network/room_member.h | 5 +++- src/network/verify_user.h | 1 + src/web_service/verify_user_jwt.cpp | 4 +++ 15 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index a0a661887..2fabfbfc7 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -81,6 +81,9 @@ static void OnStateChanged(const Network::RoomMember::State& state) { case Network::RoomMember::State::Joined: LOG_DEBUG(Network, "Successfully joined to the room"); break; + case Network::RoomMember::State::Moderator: + LOG_DEBUG(Network, "Successfully joined the room as a moderator"); + break; default: break; } diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 4c4150d6a..85ac53987 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -306,7 +306,9 @@ void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_ void ChatRoom::OnSendChat() { if (auto room = Network::GetRoomMember().lock()) { - if (room->GetState() != Network::RoomMember::State::Joined) { + if (room->GetState() != Network::RoomMember::State::Joined && + room->GetState() != Network::RoomMember::State::Moderator) { + return; } auto message = ui->chat_message->text().toStdString(); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index d87a3e6e1..458f8c864 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -72,9 +72,12 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { } void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { - if (state == Network::RoomMember::State::Joined) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + ui->chat->Clear(); ui->chat->AppendStatusMessage(tr("Connected")); + SetModPerms(state == Network::RoomMember::State::Moderator); } UpdateView(); } diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index c40d324c3..584a51642 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -18,7 +18,6 @@ public: ~ClientRoomWindow(); void RetranslateUi(); - void SetModPerms(bool is_mod); public slots: void OnRoomUpdate(const Network::RoomInformation&); @@ -32,6 +31,7 @@ signals: private: void Disconnect(); void UpdateView(); + void SetModPerms(bool is_mod); QStandardItemModel* player_list; std::unique_ptr ui; diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 3870c1e25..c14edde76 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -63,7 +63,7 @@ void DirectConnectWindow::Connect() { // Prevent the user from trying to join a room while they are already joining. if (member->GetState() == Network::RoomMember::State::Joining) { return; - } else if (member->GetState() == Network::RoomMember::State::Joined) { + } else if (member->IsConnected()) { // And ask if they want to leave the room if they are already in one. if (!NetworkMessage::WarnDisconnect()) { return; @@ -122,7 +122,9 @@ void DirectConnectWindow::OnConnection() { EndConnecting(); if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->GetState() == Network::RoomMember::State::Joined) { + if (room_member->GetState() == Network::RoomMember::State::Joined || + room_member->GetState() == Network::RoomMember::State::Moderator) { + close(); } } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index d5f093e48..2dbc74d6c 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -113,7 +113,7 @@ void HostRoomWindow::Host() { if (auto member = Network::GetRoomMember().lock()) { if (member->GetState() == Network::RoomMember::State::Joining) { return; - } else if (member->GetState() == Network::RoomMember::State::Joined) { + } else if (member->IsConnected()) { auto parent = static_cast(parentWidget()); if (!parent->OnCloseRoom()) { close(); diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index bb1807776..3b48b3a95 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -109,7 +109,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { // Prevent the user from trying to join a room while they are already joining. if (member->GetState() == Network::RoomMember::State::Joining) { return; - } else if (member->GetState() == Network::RoomMember::State::Joined) { + } else if (member->IsConnected()) { // And ask if they want to leave the room if they are already in one. if (!NetworkMessage::WarnDisconnect()) { return; diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index b14e79f9b..75d74e189 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -89,7 +89,9 @@ void MultiplayerState::retranslateUi() { if (current_state == Network::RoomMember::State::Uninitialized) { status_text->setText(tr("Not Connected. Click here to find a room!")); - } else if (current_state == Network::RoomMember::State::Joined) { + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + status_text->setText(tr("Connected")); } else { status_text->setText(tr("Not Connected")); @@ -107,7 +109,9 @@ void MultiplayerState::retranslateUi() { void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); - if (state == Network::RoomMember::State::Joined) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + OnOpenNetworkRoom(); status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); status_text->setText(tr("Connected")); @@ -183,7 +187,9 @@ void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { void MultiplayerState::UpdateThemedIcons() { if (show_notification) { status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16)); - } else if (current_state == Network::RoomMember::State::Joined) { + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); } else { status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); @@ -258,12 +264,6 @@ void MultiplayerState::OnOpenNetworkRoom() { connect(client_room, &ClientRoomWindow::ShowNotification, this, &MultiplayerState::ShowNotification); } - const std::string host_username = member->GetRoomInformation().host_username; - if (host_username.empty()) { - client_room->SetModPerms(false); - } else { - client_room->SetModPerms(member->GetUsername() == host_username); - } BringWidgetToFront(client_room); return; } diff --git a/src/core/hle/service/nwm/nwm_uds.cpp b/src/core/hle/service/nwm/nwm_uds.cpp index 755da4f1d..bf2d408c1 100644 --- a/src/core/hle/service/nwm/nwm_uds.cpp +++ b/src/core/hle/service/nwm/nwm_uds.cpp @@ -140,7 +140,9 @@ std::list GetReceivedBeacons(const MacAddress& sender) { /// Sends a WifiPacket to the room we're currently connected to. void SendPacket(Network::WifiPacket& packet) { if (auto room_member = Network::GetRoomMember().lock()) { - if (room_member->GetState() == Network::RoomMember::State::Joined) { + if (room_member->GetState() == Network::RoomMember::State::Joined || + room_member->GetState() == Network::RoomMember::State::Moderator) { + packet.transmitter_address = room_member->GetMacAddress(); room_member->SendWifiPacket(packet); } diff --git a/src/network/room.cpp b/src/network/room.cpp index 394e8644f..5386977e5 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -155,6 +155,12 @@ public: */ void SendJoinSuccess(ENetPeer* client, MacAddress mac_address); + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room, and it has been granted mod permissions. + */ + void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address); + /** * Sends a IdHostKicked message telling the client that they have been kicked. */ @@ -401,7 +407,11 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { // Notify everyone that the room information has changed. BroadcastRoomInformation(); - SendJoinSuccess(event->peer, preferred_mac); + if (HasModPermission(event->peer)) { + SendJoinSuccessAsMod(event->peer, preferred_mac); + } else { + SendJoinSuccess(event->peer, preferred_mac); + } } void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { @@ -588,10 +598,11 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { if (sending_member == members.end()) { return false; } - if (sending_member->user_data.username != room_information.host_username) { - return false; - } - return true; + if (sending_member->user_data.moderator) // Community moderator + return true; + if (sending_member->user_data.username == room_information.host_username) // Room host + return true; + return false; } void Room::RoomImpl::SendNameCollision(ENetPeer* client) { @@ -665,6 +676,16 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { enet_host_flush(server); } +void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet << static_cast(IdJoinSuccessAsMod); + packet << mac_address; + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + void Room::RoomImpl::SendUserKicked(ENetPeer* client) { Packet packet; packet << static_cast(IdHostKicked); diff --git a/src/network/room.h b/src/network/room.h index 3181e84d7..5781631d7 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -74,6 +74,7 @@ enum RoomMessageTypes : u8 { IdModBanListResponse, IdModPermissionDenied, IdModNoSuchUser, + IdJoinSuccessAsMod, }; /// Types of system status messages diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 40d7e7068..8a846ee04 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -151,7 +151,7 @@ void RoomMember::RoomMemberImpl::SetError(const Error new_error) { } bool RoomMember::RoomMemberImpl::IsConnected() const { - return state == State::Joining || state == State::Joined; + return state == State::Joining || state == State::Joined || state == State::Moderator; } void RoomMember::RoomMemberImpl::MemberLoop() { @@ -176,12 +176,17 @@ void RoomMember::RoomMemberImpl::MemberLoop() { HandleRoomInformationPacket(&event); break; case IdJoinSuccess: + case IdJoinSuccessAsMod: // The join request was successful, we are now in the room. // If we joined successfully, there must be at least one client in the room: us. ASSERT_MSG(member_information.size() > 0, "We have not yet received member information."); HandleJoinPacket(&event); // Get the MAC Address for the client - SetState(State::Joined); + if (event.packet->data[0] == IdJoinSuccessAsMod) { + SetState(State::Moderator); + } else { + SetState(State::Joined); + } break; case IdModBanListResponse: HandleModBanListResponsePacket(&event); @@ -232,7 +237,7 @@ void RoomMember::RoomMemberImpl::MemberLoop() { enet_packet_destroy(event.packet); break; case ENET_EVENT_TYPE_DISCONNECT: - if (state == State::Joined) { + if (state == State::Joined || state == State::Moderator) { SetState(State::Idle); SetError(Error::LostConnection); } @@ -331,7 +336,6 @@ void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { // Parse the MAC Address from the packet packet >> mac_address; - SetState(State::Joined); } void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { diff --git a/src/network/room_member.h b/src/network/room_member.h index 65d1c64eb..3410abac1 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -59,7 +59,8 @@ public: Uninitialized, ///< Not initialized Idle, ///< Default state (i.e. not connected) Joining, ///< The client is attempting to join a room. - Joined, ///< The client is connected to the room and is ready to send/receive packets. + Joined, ///< The client is connected to the room and is ready to send/receive packets. + Moderator, ///< The client is connnected to the room and is granted mod permissions. }; enum class Error : u8 { @@ -270,6 +271,8 @@ static const char* GetStateStr(const RoomMember::State& s) { return "Joining"; case RoomMember::State::Joined: return "Joined"; + case RoomMember::State::Moderator: + return "Moderator"; } return "Unknown"; } diff --git a/src/network/verify_user.h b/src/network/verify_user.h index 74e154331..01b9877c8 100644 --- a/src/network/verify_user.h +++ b/src/network/verify_user.h @@ -13,6 +13,7 @@ struct UserData { std::string username; std::string display_name; std::string avatar_url; + bool moderator = false; ///< Whether the user is a Citra Moderator. }; /** diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index fb7b7281d..27e08db9e 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -50,6 +50,10 @@ Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& ver if (decoded.payload().has_claim("avatarUrl")) { user_data.avatar_url = decoded.payload().get_claim_value("avatarUrl"); } + if (decoded.payload().has_claim("roles")) { + auto roles = decoded.payload().get_claim_value>("roles"); + user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end(); + } return user_data; } From 13ec2abbf6758a2817c4ad1524b01c8e9a583056 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 15 Dec 2018 22:47:07 +0800 Subject: [PATCH 24/27] network: Make citra mods optional and disabled by default To avoid extra legal responsibility, this should actually only be used on our self-hosted rooms. --- src/dedicated_room/citra-room.cpp | 13 ++++++++++++- src/network/room.cpp | 15 ++++++++++----- src/network/room.h | 3 ++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/dedicated_room/citra-room.cpp b/src/dedicated_room/citra-room.cpp index 8e2938006..8a8c2ae0f 100644 --- a/src/dedicated_room/citra-room.cpp +++ b/src/dedicated_room/citra-room.cpp @@ -54,6 +54,7 @@ static void PrintHelp(const char* argv0) { "--token The token used for announce\n" "--web-api-url Citra Web API url\n" "--ban-list-file The file for storing the room ban list\n" + "--enable-citra-mods Allow Citra Community Moderators to moderate on your room\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n"; } @@ -148,6 +149,7 @@ int main(int argc, char** argv) { u64 preferred_game_id = 0; u32 port = Network::DefaultRoomPort; u32 max_members = 16; + bool enable_citra_mods = false; static struct option long_options[] = { {"room-name", required_argument, 0, 'n'}, @@ -161,6 +163,7 @@ int main(int argc, char** argv) { {"token", required_argument, 0, 't'}, {"web-api-url", required_argument, 0, 'a'}, {"ban-list-file", required_argument, 0, 'b'}, + {"enable-citra-mods", no_argument, 0, 'e'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {0, 0, 0, 0}, @@ -203,6 +206,9 @@ int main(int argc, char** argv) { case 'b': ban_list_file.assign(optarg); break; + case 'e': + enable_citra_mods = true; + break; case 'h': PrintHelp(argv[0]); return 0; @@ -261,6 +267,10 @@ int main(int argc, char** argv) { Settings::values.citra_username = username; Settings::values.citra_token = token; } + if (!announce && enable_citra_mods) { + enable_citra_mods = false; + std::cout << "Can not enable Citra Moderators for private rooms\n\n"; + } // Load the ban list Network::Room::BanList ban_list; @@ -284,7 +294,8 @@ int main(int argc, char** argv) { Network::Init(); if (std::shared_ptr room = Network::GetRoom().lock()) { if (!room->Create(room_name, room_description, "", port, password, max_members, username, - preferred_game, preferred_game_id, std::move(verify_backend), ban_list)) { + preferred_game, preferred_game_id, std::move(verify_backend), ban_list, + enable_citra_mods)) { std::cout << "Failed to create room: \n\n"; return -1; } diff --git a/src/network/room.cpp b/src/network/room.cpp index 5386977e5..6014b4360 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -589,8 +589,6 @@ bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const } bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { - if (room_information.host_username.empty()) - return false; // This room does not support moderation std::lock_guard lock(member_mutex); const auto sending_member = std::find_if(members.begin(), members.end(), @@ -598,10 +596,16 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { if (sending_member == members.end()) { return false; } - if (sending_member->user_data.moderator) // Community moderator + if (room_information.enable_citra_mods && + sending_member->user_data.moderator) { // Community moderator + return true; - if (sending_member->user_data.username == room_information.host_username) // Room host + } + if (!room_information.host_username.empty() && + sending_member->user_data.username == room_information.host_username) { // Room host + return true; + } return false; } @@ -963,7 +967,7 @@ bool Room::Create(const std::string& name, const std::string& description, const u32 max_connections, const std::string& host_username, const std::string& preferred_game, u64 preferred_game_id, std::unique_ptr verify_backend, - const Room::BanList& ban_list) { + const Room::BanList& ban_list, bool enable_citra_mods) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -986,6 +990,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->room_information.host_username = host_username; + room_impl->room_information.enable_citra_mods = enable_citra_mods; room_impl->password = password; room_impl->verify_backend = std::move(verify_backend); room_impl->username_ban_list = ban_list.first; diff --git a/src/network/room.h b/src/network/room.h index 5781631d7..a67984837 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -32,6 +32,7 @@ struct RoomInformation { std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game std::string host_username; ///< Forum username of the host + bool enable_citra_mods; ///< Allow Citra Moderators to moderate on this room }; struct GameInfo { @@ -147,7 +148,7 @@ public: const std::string& host_username = "", const std::string& preferred_game = "", u64 preferred_game_id = 0, std::unique_ptr verify_backend = nullptr, - const BanList& ban_list = {}); + const BanList& ban_list = {}, bool enable_citra_mods = false); /** * Sets the verification GUID of the room. From 4574bd1e5c5bf34ca6aee6f2a5398dc301d7513e Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 16 Dec 2018 10:48:15 +0800 Subject: [PATCH 25/27] web_service: Change endpoint to `/lobby`. Preparation for shipping. --- src/web_service/announce_room_json.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index f1a690443..2d2601256 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -114,12 +114,12 @@ Common::WebResult RoomJson::Update() { return Common::WebResult{Common::WebResult::Code::LibError, "Room is not registered"}; } nlohmann::json json{{"players", room.members}}; - return client.PostJson(fmt::format("/lobby2/{}", room_id), json.dump(), false); + return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); } std::string RoomJson::Register() { nlohmann::json json = room; - auto reply = client.PostJson("/lobby2", json.dump(), false).returned_data; + auto reply = client.PostJson("/lobby", json.dump(), false).returned_data; if (reply.empty()) { return ""; } @@ -134,7 +134,7 @@ void RoomJson::ClearPlayers() { } AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { - auto reply = client.GetJson("/lobby2", true).returned_data; + auto reply = client.GetJson("/lobby", true).returned_data; if (reply.empty()) { return {}; } @@ -149,7 +149,7 @@ void RoomJson::Delete() { Common::DetachedTasks::AddTask( [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() { // create a new client here because the this->client might be destroyed. - Client{host, username, token}.DeleteJson(fmt::format("/lobby2/{}", room_id), "", false); + Client{host, username, token}.DeleteJson(fmt::format("/lobby/{}", room_id), "", false); }); } From 4df4b90795a8bf1ca9f4ee7a60145500bd88ab3b Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 16 Dec 2018 23:08:47 +0800 Subject: [PATCH 26/27] citra_qt/multiplayer: Change style for pinged messages a bit To allow it to be seen more clearly in dark themes --- src/citra_qt/multiplayer/chat_room.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 85ac53987..60a41c6b1 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -70,7 +70,8 @@ public: style = QString("background-color: %1").arg(ping_color); } - return QString("[%1] <%3> %5") + return QString("[%1] <%3> %5") .arg(timestamp, color, name.toHtmlEscaped(), style, message.toHtmlEscaped()); } From 7a379ee03a905b3623dc841b5b752a83223d8822 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 16 Dec 2018 23:10:48 +0800 Subject: [PATCH 27/27] citra_qt/multiplayer: Add `View Profile` option Adds an UI action to navigate to the user's profile located in Citra Community. --- src/citra_qt/multiplayer/chat_room.cpp | 59 ++++++++++++++++---------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 60a41c6b1..47eb2da2c 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 @@ -411,34 +412,46 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { std::string nickname = player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); - if (auto room = Network::GetRoomMember().lock()) { - // You can't block, kick or ban yourself - if (nickname == room->GetNickname()) - return; - } QMenu context_menu; - QAction* block_action = context_menu.addAction(tr("Block Player")); - block_action->setCheckable(true); - block_action->setChecked(block_list.count(nickname) > 0); + QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString(); + if (!username.isEmpty()) { + QAction* view_profile_action = context_menu.addAction(tr("View Profile")); + connect(view_profile_action, &QAction::triggered, [username] { + QDesktopServices::openUrl( + QString("https://community.citra-emu.org/u/%1").arg(username)); + }); + } - connect(block_action, &QAction::triggered, [this, nickname] { - if (block_list.count(nickname)) { - block_list.erase(nickname); - } else { - QMessageBox::StandardButton result = QMessageBox::question( - this, tr("Block Player"), - tr("When you block a player, you will no longer receive chat messages from " - "them.

Are you sure you would like to block %1?") - .arg(QString::fromStdString(nickname)), - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::Yes) - block_list.emplace(nickname); - } - }); + std::string cur_nickname; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = room->GetNickname(); + } - if (has_mod_perms) { + if (nickname != cur_nickname) { // You can't block yourself + QAction* block_action = context_menu.addAction(tr("Block Player")); + + block_action->setCheckable(true); + block_action->setChecked(block_list.count(nickname) > 0); + + connect(block_action, &QAction::triggered, [this, nickname] { + if (block_list.count(nickname)) { + block_list.erase(nickname); + } else { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Block Player"), + tr("When you block a player, you will no longer receive chat messages from " + "them.

Are you sure you would like to block %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + block_list.emplace(nickname); + } + }); + } + + if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself context_menu.addSeparator(); QAction* kick_action = context_menu.addAction(tr("Kick"));