mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Merge pull request #4468 from citra-emu/multiplayer-v4/main
Multiplayer version 4
This commit is contained in:
		
						commit
						eabc9727d8
					
				
					 60 changed files with 2395 additions and 308 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							|  | @ -43,3 +43,6 @@ | |||
| [submodule "externals/cppzmq"] | ||||
| 	path = externals/cppzmq | ||||
| 	url = https://github.com/zeromq/cppzmq | ||||
| [submodule "cpp-jwt"] | ||||
| 	path = externals/cpp-jwt | ||||
| 	url = https://github.com/arun11299/cpp-jwt.git | ||||
|  |  | |||
							
								
								
									
										5
									
								
								dist/license.md
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								dist/license.md
									
										
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
|  | @ -11,10 +12,12 @@ 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 | ||||
| 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 | ||||
|  | @ -22,9 +25,11 @@ 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 | ||||
| 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 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								dist/qt_themes/colorful/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/qt_themes/colorful/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 607 B | 
							
								
								
									
										1
									
								
								dist/qt_themes/colorful/style.qrc
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								dist/qt_themes/colorful/style.qrc
									
										
									
									
										vendored
									
									
								
							|  | @ -2,6 +2,7 @@ | |||
|     <qresource prefix="icons/colorful"> | ||||
|         <file alias="index.theme">icons/index.theme</file> | ||||
|         <file alias="16x16/connected.png">icons/16x16/connected.png</file> | ||||
|         <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file> | ||||
|         <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> | ||||
|         <file alias="16x16/lock.png">icons/16x16/lock.png</file> | ||||
|         <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> | ||||
|  |  | |||
							
								
								
									
										2
									
								
								dist/qt_themes/colorful_dark/style.qrc
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/qt_themes/colorful_dark/style.qrc
									
										
									
									
										vendored
									
									
								
							|  | @ -2,11 +2,13 @@ | |||
|     <qresource prefix="icons/colorful_dark"> | ||||
|         <file alias="index.theme">icons/index.theme</file> | ||||
|         <file alias="16x16/connected.png">../colorful/icons/16x16/connected.png</file> | ||||
|         <file alias="16x16/connected_notification.png">../colorful/icons/16x16/connected_notification.png</file> | ||||
|         <file alias="16x16/disconnected.png">../colorful/icons/16x16/disconnected.png</file> | ||||
|         <file alias="16x16/lock.png">icons/16x16/lock.png</file> | ||||
|         <file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file> | ||||
|         <file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file> | ||||
|         <file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file> | ||||
|         <file alias="48x48/no_avatar.png">../qdarkstyle/icons/48x48/no_avatar.png</file> | ||||
|         <file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file> | ||||
|         <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file> | ||||
|         <file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file> | ||||
|  |  | |||
							
								
								
									
										4
									
								
								dist/qt_themes/default/default.qrc
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								dist/qt_themes/default/default.qrc
									
										
									
									
										vendored
									
									
								
							|  | @ -10,6 +10,8 @@ | |||
| 
 | ||||
|         <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> | ||||
| 
 | ||||
|         <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file> | ||||
| 
 | ||||
|         <file alias="16x16/lock.png">icons/16x16/lock.png</file> | ||||
| 
 | ||||
|         <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> | ||||
|  | @ -18,6 +20,8 @@ | |||
| 
 | ||||
|         <file alias="48x48/folder.png">icons/48x48/folder.png</file> | ||||
| 
 | ||||
|         <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file> | ||||
| 
 | ||||
|         <file alias="48x48/plus.png">icons/48x48/plus.png</file> | ||||
|        | ||||
|         <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								dist/qt_themes/default/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/qt_themes/default/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 517 B | 
							
								
								
									
										
											BIN
										
									
								
								dist/qt_themes/default/icons/48x48/no_avatar.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/qt_themes/default/icons/48x48/no_avatar.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 588 B | 
							
								
								
									
										
											BIN
										
									
								
								dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 526 B | 
							
								
								
									
										
											BIN
										
									
								
								dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 708 B | 
							
								
								
									
										2
									
								
								dist/qt_themes/qdarkstyle/style.qrc
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/qt_themes/qdarkstyle/style.qrc
									
										
									
									
										vendored
									
									
								
							|  | @ -3,10 +3,12 @@ | |||
|     <file alias="index.theme">icons/index.theme</file> | ||||
|     <file alias="16x16/connected.png">icons/16x16/connected.png</file> | ||||
|     <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> | ||||
|     <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file> | ||||
|     <file alias="16x16/lock.png">icons/16x16/lock.png</file> | ||||
|     <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> | ||||
|     <file alias="48x48/chip.png">icons/48x48/chip.png</file> | ||||
|     <file alias="48x48/folder.png">icons/48x48/folder.png</file> | ||||
|     <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file> | ||||
|     <file alias="48x48/plus.png">icons/48x48/plus.png</file> | ||||
|     <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> | ||||
|     <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> | ||||
|  |  | |||
							
								
								
									
										10
									
								
								externals/CMakeLists.txt
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								externals/CMakeLists.txt
									
										
									
									
										vendored
									
									
								
							|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										1
									
								
								externals/cpp-jwt
									
										
									
									
										vendored
									
									
										Submodule
									
								
							
							
						
						
									
										1
									
								
								externals/cpp-jwt
									
										
									
									
										vendored
									
									
										Submodule
									
								
							|  | @ -0,0 +1 @@ | |||
| Subproject commit 6e27aa4c8671e183f11e327a2e1f556c64fdc4a9 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" | ||||
|  | @ -80,42 +81,87 @@ 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: | ||||
|         LOG_DEBUG(Network, "Lost connection to the room"); | ||||
|         break; | ||||
|     case Network::RoomMember::State::CouldNotConnect: | ||||
|         LOG_ERROR(Network, "State: CouldNotConnect"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     case Network::RoomMember::State::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: | ||||
|         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::WrongPassword: | ||||
|         LOG_ERROR(Network, "Room replied with: Wrong password"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     case Network::RoomMember::State::WrongVersion: | ||||
|         LOG_ERROR(Network, | ||||
|                   "You are using a different version than the room you are trying to connect to"); | ||||
|         exit(1); | ||||
|     case Network::RoomMember::State::Moderator: | ||||
|         LOG_DEBUG(Network, "Successfully joined the room as a moderator"); | ||||
|         break; | ||||
|     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::Error::CouldNotConnect: | ||||
|         LOG_ERROR(Network, "Error: Could not connect"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     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::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::Error::ConsoleIdCollision: | ||||
|         LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::WrongPassword: | ||||
|         LOG_ERROR(Network, "Room replied with: Wrong password"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     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::Error::RoomIsFull: | ||||
|         LOG_ERROR(Network, "The room is full"); | ||||
|         exit(1); | ||||
|         break; | ||||
|     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; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|     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; | ||||
| } | ||||
| 
 | ||||
| static void InitializeLogging() { | ||||
|     Log::Filter log_filter(Log::Level::Debug); | ||||
|     log_filter.ParseFilterString(Settings::values.log_filter); | ||||
|  | @ -333,10 +379,13 @@ int main(int argc, char** argv) { | |||
|     if (use_multiplayer) { | ||||
|         if (auto member = Network::GetRoomMember().lock()) { | ||||
|             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, 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; | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -228,6 +231,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() | ||||
|  |  | |||
|  | @ -329,6 +329,22 @@ 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(); | ||||
|  | @ -533,6 +549,20 @@ 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, ""); | ||||
|     // 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(); | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| #include <array> | ||||
| #include <future> | ||||
| #include <QColor> | ||||
| #include <QDesktopServices> | ||||
| #include <QFutureWatcher> | ||||
| #include <QImage> | ||||
| #include <QList> | ||||
| #include <QLocale> | ||||
|  | @ -12,6 +14,7 @@ | |||
| #include <QMessageBox> | ||||
| #include <QMetaType> | ||||
| #include <QTime> | ||||
| #include <QUrl> | ||||
| #include <QtConcurrent/QtConcurrentRun> | ||||
| #include "citra_qt/game_list_p.h" | ||||
| #include "citra_qt/multiplayer/chat_room.h" | ||||
|  | @ -19,6 +22,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,24 +33,60 @@ 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); | ||||
| 
 | ||||
|         // 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
 | ||||
|     QString GetPlayerChatMessage(u16 player) const { | ||||
|         auto color = player_color[player % 16]; | ||||
|         return QString("[%1] <font color='%2'><%3></font> %4") | ||||
|             .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped()); | ||||
|         QString name; | ||||
|         if (username.isEmpty() || username == nickname) { | ||||
|             name = nickname; | ||||
|         } else { | ||||
|             name = QString("%1 (%2)").arg(nickname, username); | ||||
|         } | ||||
| 
 | ||||
|         QString style; | ||||
|         if (ContainsPing()) { | ||||
|             // Add a background color to these messages
 | ||||
|             style = QString("background-color: %1").arg(ping_color); | ||||
|         } | ||||
| 
 | ||||
|         return QString("[%1] <font color='%2'><%3></font> <font style='%4' " | ||||
|                        "color='#000000'>%5</font>") | ||||
|             .arg(timestamp, color, name.toHtmlEscaped(), style, message.toHtmlEscaped()); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     static constexpr std::array<const char*, 16> 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 { | ||||
|  | @ -57,37 +99,69 @@ public: | |||
|     } | ||||
| 
 | ||||
|     QString GetSystemChatMessage() const { | ||||
|         return QString("[%1] <font color='%2'><i>%3</i></font>") | ||||
|             .arg(timestamp, system_color, message); | ||||
|         return QString("[%1] <font color='%2'>* %3</font>").arg(timestamp, system_color, message); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     static constexpr const char system_color[] = "#888888"; | ||||
|     static constexpr const char system_color[] = "#FF8C00"; | ||||
|     QString timestamp; | ||||
|     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::ChatRoom>()) { | ||||
|     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); | ||||
| 
 | ||||
|     // register the network structs to use in slots and signals
 | ||||
|     qRegisterMetaType<Network::ChatEntry>(); | ||||
|     qRegisterMetaType<Network::StatusMessageEntry>(); | ||||
|     qRegisterMetaType<Network::RoomInformation>(); | ||||
|     qRegisterMetaType<Network::RoomMember::State>(); | ||||
| 
 | ||||
|  | @ -95,7 +169,12 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::C | |||
|     if (auto member = Network::GetRoomMember().lock()) { | ||||
|         member->BindOnChatMessageRecieved( | ||||
|             [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?
 | ||||
|     } | ||||
|  | @ -110,6 +189,10 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::C | |||
| 
 | ||||
| ChatRoom::~ChatRoom() = default; | ||||
| 
 | ||||
| void ChatRoom::SetModPerms(bool is_mod) { | ||||
|     has_mod_perms = is_mod; | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::RetranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
| } | ||||
|  | @ -127,6 +210,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(); | ||||
| } | ||||
|  | @ -157,7 +255,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."); | ||||
|  | @ -170,13 +269,48 @@ 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)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| 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(); | ||||
|  | @ -184,12 +318,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 +338,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<QStandardItem*> l; | ||||
|         std::vector<std::string> 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<std::string> 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"); | ||||
|                     } | ||||
|         player_list->invisibleRootItem()->appendRow(l); | ||||
|                     return result.returned_data; | ||||
|                 }); | ||||
|                 auto* future_watcher = new QFutureWatcher<std::string>(this); | ||||
|                 connect(future_watcher, &QFutureWatcher<std::string>::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<const u8*>(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 | ||||
|             } | ||||
|         } | ||||
|         name_item->setData(icon_cache.at(member.avatar_url), Qt::DecorationRole); | ||||
| 
 | ||||
|         player_list->invisibleRootItem()->appendRow(name_item); | ||||
|     } | ||||
|     // TODO(B3N30): Restore row selection
 | ||||
| } | ||||
|  | @ -230,14 +410,26 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { | |||
|     if (!item.isValid()) | ||||
|         return; | ||||
| 
 | ||||
|     std::string nickname = player_list->item(item.row())->text().toStdString(); | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         // You can't block yourself
 | ||||
|         if (nickname == room->GetNickname()) | ||||
|             return; | ||||
|     } | ||||
|     std::string nickname = | ||||
|         player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); | ||||
| 
 | ||||
|     QMenu context_menu; | ||||
| 
 | ||||
|     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)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     std::string cur_nickname; | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         cur_nickname = room->GetNickname(); | ||||
|     } | ||||
| 
 | ||||
|     if (nickname != cur_nickname) { // You can't block yourself
 | ||||
|         QAction* block_action = context_menu.addAction(tr("Block Player")); | ||||
| 
 | ||||
|         block_action->setCheckable(true); | ||||
|  | @ -257,6 +449,34 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { | |||
|                     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")); | ||||
|         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 <b>kick</b> %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 <b>kick and ban</b> %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)); | ||||
| } | ||||
|  |  | |||
|  | @ -36,9 +36,12 @@ 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&); | ||||
|     void OnStatusMessageReceive(const Network::StatusMessageEntry&); | ||||
|     void OnSendChat(); | ||||
|     void OnChatTextChanged(); | ||||
|     void PopupContextMenu(const QPoint& menu_location); | ||||
|  | @ -47,16 +50,25 @@ public slots: | |||
| 
 | ||||
| signals: | ||||
|     void ChatReceived(const Network::ChatEntry&); | ||||
|     void StatusMessageReceived(const Network::StatusMessageEntry&); | ||||
|     void UserPinged(); | ||||
| 
 | ||||
| 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::ChatRoom> ui; | ||||
|     std::unordered_set<std::string> block_list; | ||||
|     std::unordered_map<std::string, QPixmap> icon_cache; | ||||
| }; | ||||
| 
 | ||||
| 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); | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <width>807</width> | ||||
|     <height>432</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|  |  | |||
|  | @ -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" | ||||
|  | @ -33,6 +34,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?
 | ||||
|     } | ||||
|  | @ -40,11 +43,25 @@ 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); | ||||
|     connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); | ||||
|     UpdateView(); | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
| } | ||||
| 
 | ||||
| void ClientRoomWindow::RetranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
|     ui->chat->RetranslateUi(); | ||||
|  | @ -55,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(); | ||||
| } | ||||
|  | @ -82,6 +102,7 @@ void ClientRoomWindow::UpdateView() { | |||
|                                .arg(QString::fromStdString(information.name)) | ||||
|                                .arg(memberlist.size()) | ||||
|                                .arg(information.member_slots)); | ||||
|             ui->description->setText(QString::fromStdString(information.description)); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -26,10 +26,12 @@ public slots: | |||
| signals: | ||||
|     void RoomInformationChanged(const Network::RoomInformation&); | ||||
|     void StateChanged(const Network::RoomMember::State&); | ||||
|     void ShowNotification(); | ||||
| 
 | ||||
| private: | ||||
|     void Disconnect(); | ||||
|     void UpdateView(); | ||||
|     void SetModPerms(bool is_mod); | ||||
| 
 | ||||
|     QStandardItemModel* player_list; | ||||
|     std::unique_ptr<Ui::ClientRoom> ui; | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <width>807</width> | ||||
|     <height>432</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|  | @ -21,6 +21,13 @@ | |||
|        <property name="rightMargin"> | ||||
|         <number>0</number> | ||||
|        </property> | ||||
|        <item> | ||||
|         <widget class="QLabel" name="description"> | ||||
|          <property name="text"> | ||||
|           <string>Room Description</string> | ||||
|          </property> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item> | ||||
|         <spacer name="horizontalSpacer"> | ||||
|          <property name="orientation"> | ||||
|  | @ -34,6 +41,16 @@ | |||
|          </property> | ||||
|         </spacer> | ||||
|        </item> | ||||
|        <item> | ||||
|         <widget class="QPushButton" name="moderation"> | ||||
|          <property name="text"> | ||||
|           <string>Moderation...</string> | ||||
|          </property> | ||||
|          <property name="visible"> | ||||
|           <bool>false</bool> | ||||
|          </property> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item> | ||||
|         <widget class="QPushButton" name="disconnect"> | ||||
|          <property name="text"> | ||||
|  |  | |||
|  | @ -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" | ||||
|  | @ -62,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; | ||||
|  | @ -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()); | ||||
|         } | ||||
|  | @ -120,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(); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -19,8 +19,12 @@ | |||
| #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" | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/verify_user_jwt.h" | ||||
| #endif | ||||
| 
 | ||||
| HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | ||||
|                                std::shared_ptr<Core::AnnounceMultiplayerSession> session) | ||||
|  | @ -69,6 +73,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; | ||||
|  | @ -77,6 +82,21 @@ void HostRoomWindow::RetranslateUi() { | |||
|     ui->retranslateUi(this); | ||||
| } | ||||
| 
 | ||||
| std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBackend( | ||||
|     bool use_validation) const { | ||||
|     std::unique_ptr<Network::VerifyUser::Backend> verify_backend; | ||||
|     if (use_validation) { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|         verify_backend = std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url); | ||||
| #else | ||||
|         verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); | ||||
| #endif | ||||
|     } else { | ||||
|         verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); | ||||
|     } | ||||
|     return verify_backend; | ||||
| } | ||||
| 
 | ||||
| void HostRoomWindow::Host() { | ||||
|     if (!ui->username->hasAcceptableInput()) { | ||||
|         NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); | ||||
|  | @ -93,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<MultiplayerState*>(parentWidget()); | ||||
|             if (!parent->OnCloseRoom()) { | ||||
|                 close(); | ||||
|  | @ -106,9 +126,17 @@ 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; | ||||
|         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(), "", 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(), | ||||
|                                         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!"); | ||||
|  | @ -116,8 +144,34 @@ void HostRoomWindow::Host() { | |||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, | ||||
|                      Network::NoPreferredMac, password); | ||||
|         // 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, token); | ||||
| 
 | ||||
|         // Store settings
 | ||||
|         UISettings::values.room_nickname = ui->username->text(); | ||||
|  | @ -130,26 +184,11 @@ 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(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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(); | ||||
|     } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { | ||||
|  |  | |||
|  | @ -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<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const; | ||||
| 
 | ||||
|     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||
|     QStandardItemModel* game_list; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <height>165</height> | ||||
|     <height>211</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|  | @ -131,6 +131,34 @@ | |||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout_3"> | ||||
|      <item> | ||||
|       <widget class="QLabel" name="label_7"> | ||||
|        <property name="text"> | ||||
|         <string>Room Description</string> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <widget class="QTextEdit" name="room_description"/> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout"> | ||||
|      <item> | ||||
|       <widget class="QCheckBox" name="load_ban_list"> | ||||
|        <property name="text"> | ||||
|         <string>Load Previous Ban List</string> | ||||
|        </property> | ||||
|        <property name="checked"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|      <property name="rightMargin"> | ||||
|  |  | |||
|  | @ -15,8 +15,12 @@ | |||
| #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" | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/web_backend.h" | ||||
| #endif | ||||
| 
 | ||||
| Lobby::Lobby(QWidget* parent, QStandardItemModel* list, | ||||
|              std::shared_ptr<Core::AnnounceMultiplayerSession> session) | ||||
|  | @ -105,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; | ||||
|  | @ -135,11 +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<void> f = QtConcurrent::run([nickname, ip, port, password] { | ||||
|     QFuture<void> 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, 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, token); | ||||
|         } | ||||
|     }); | ||||
|     watcher->setFuture(f); | ||||
|  | @ -191,7 +211,8 @@ void Lobby::OnRefreshLobby() { | |||
|         QList<QVariant> 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); | ||||
|         } | ||||
|  | @ -203,14 +224,18 @@ 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); | ||||
|         // 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)); | ||||
|         } | ||||
|     } | ||||
|  | @ -226,8 +251,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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -95,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 { | ||||
|  | @ -121,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; | ||||
|  | @ -137,6 +169,7 @@ public: | |||
| 
 | ||||
| private: | ||||
|     QString username; | ||||
|     QString nickname; | ||||
|     u64 title_id; | ||||
|     QString game_name; | ||||
| }; | ||||
|  | @ -195,10 +228,9 @@ public: | |||
|                 out += '\n'; | ||||
|             const auto& m = member.value<LobbyMember>(); | ||||
|             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; | ||||
|         } | ||||
|  |  | |||
|  | @ -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( | ||||
|  | @ -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( | ||||
|  | @ -34,8 +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()), | ||||
|  |  | |||
|  | @ -20,20 +20,27 @@ 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; | ||||
| 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; | ||||
| 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 | ||||
|  |  | |||
							
								
								
									
										113
									
								
								src/citra_qt/multiplayer/moderation_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/citra_qt/multiplayer/moderation_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| // Copyright 2018 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QStandardItem> | ||||
| #include <QStandardItemModel> | ||||
| #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::ModerationDialog>()) { | ||||
|     ui->setupUi(this); | ||||
| 
 | ||||
|     qRegisterMetaType<Network::Room::BanList>(); | ||||
| 
 | ||||
|     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(); | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/citra_qt/multiplayer/moderation_dialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/citra_qt/multiplayer/moderation_dialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <memory> | ||||
| #include <optional> | ||||
| #include <QDialog> | ||||
| #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::ModerationDialog> ui; | ||||
|     QStandardItemModel* model; | ||||
|     Network::RoomMember::CallbackHandle<Network::StatusMessageEntry> callback_handle_status_message; | ||||
|     Network::RoomMember::CallbackHandle<Network::Room::BanList> callback_handle_ban_list; | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(Network::Room::BanList); | ||||
							
								
								
									
										84
									
								
								src/citra_qt/multiplayer/moderation_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/citra_qt/multiplayer/moderation_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ModerationDialog</class> | ||||
|  <widget class="QDialog" name="ModerationDialog"> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Moderation</string> | ||||
|   </property> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>500</width> | ||||
|     <height>300</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout"> | ||||
|    <item> | ||||
|     <widget class="QGroupBox" name="ban_list_group_box"> | ||||
|      <property name="title"> | ||||
|       <string>Ban List</string> | ||||
|      </property> | ||||
|      <layout class="QVBoxLayout"> | ||||
|       <item> | ||||
|        <layout class="QHBoxLayout"> | ||||
|         <item> | ||||
|          <spacer name="horizontalSpacer"> | ||||
|           <property name="orientation"> | ||||
|            <enum>Qt::Horizontal</enum> | ||||
|           </property> | ||||
|           <property name="sizeHint" stdset="0"> | ||||
|            <size> | ||||
|             <width>40</width> | ||||
|             <height>20</height> | ||||
|            </size> | ||||
|           </property> | ||||
|          </spacer> | ||||
|         </item> | ||||
|         <item> | ||||
|          <widget class="QPushButton" name="refresh"> | ||||
|           <property name="text"> | ||||
|            <string>Refreshing</string> | ||||
|           </property> | ||||
|           <property name="enabled"> | ||||
|            <bool>false</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item> | ||||
|          <widget class="QPushButton" name="unban"> | ||||
|           <property name="text"> | ||||
|            <string>Unban</string> | ||||
|           </property> | ||||
|           <property name="enabled"> | ||||
|            <bool>false</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|        </layout> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QTreeView" name="ban_list_view"/> | ||||
|       </item> | ||||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <widget class="QDialogButtonBox" name="buttonBox"> | ||||
|      <property name="standardButtons"> | ||||
|       <set>QDialogButtonBox::Ok</set> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <connections> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>accepted()</signal> | ||||
|    <receiver>ModerationDialog</receiver> | ||||
|    <slot>accept()</slot> | ||||
|   </connection> | ||||
|  </connections> | ||||
|  <resources/> | ||||
| </ui> | ||||
|  | @ -3,6 +3,7 @@ | |||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QAction> | ||||
| #include <QApplication> | ||||
| #include <QIcon> | ||||
| #include <QMessageBox> | ||||
| #include <QStandardItemModel> | ||||
|  | @ -13,6 +14,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" | ||||
|  | @ -27,9 +29,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<Network::RoomMember::State>(); | ||||
|     qRegisterMetaType<Network::RoomMember::Error>(); | ||||
|     qRegisterMetaType<Common::WebResult>(); | ||||
|     announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(); | ||||
|     announce_multiplayer_session->BindErrorCallback( | ||||
|  | @ -44,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*>(QApplication::instance()), &QApplication::focusChanged, this, | ||||
|             [this](QWidget* /*old*/, QWidget* now) { | ||||
|                 if (client_room && client_room->isAncestorOf(now)) { | ||||
|                     HideNotification(); | ||||
|                 } | ||||
|             }); | ||||
| } | ||||
| 
 | ||||
| MultiplayerState::~MultiplayerState() { | ||||
|  | @ -52,6 +65,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() { | ||||
|  | @ -70,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")); | ||||
|  | @ -88,35 +109,10 @@ 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_IN_USE); | ||||
|         break; | ||||
|     case Network::RoomMember::State::MacCollision: | ||||
|         NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); | ||||
|         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 || | ||||
|         state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         OnOpenNetworkRoom(); | ||||
|         break; | ||||
|     } | ||||
|     if (is_connected) { | ||||
|         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||
|         status_text->setText(tr("Connected")); | ||||
|         leave_room->setEnabled(true); | ||||
|  | @ -131,6 +127,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( | ||||
|  | @ -144,7 +185,11 @@ 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 || | ||||
|                current_state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||
|     } else { | ||||
|         status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); | ||||
|  | @ -185,6 +230,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)"); | ||||
|  | @ -192,11 +241,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); | ||||
|             } | ||||
|             BringWidgetToFront(client_room); | ||||
|             return; | ||||
|  |  | |||
|  | @ -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(); | ||||
|  | @ -47,9 +48,12 @@ public slots: | |||
|     void OnDirectConnectToRoom(); | ||||
|     void OnAnnounceFailed(const Common::WebResult&); | ||||
|     void UpdateThemedIcons(); | ||||
|     void ShowNotification(); | ||||
|     void HideNotification(); | ||||
| 
 | ||||
| signals: | ||||
|     void NetworkStateChanged(const Network::RoomMember::State&); | ||||
|     void NetworkError(const Network::RoomMember::Error&); | ||||
|     void AnnounceFailed(const Common::WebResult&); | ||||
| 
 | ||||
| private: | ||||
|  | @ -64,7 +68,11 @@ private: | |||
|     QAction* show_room; | ||||
|     std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||
|     Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; | ||||
|     bool has_mod_perms = false; | ||||
|     Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle; | ||||
|     Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle; | ||||
| 
 | ||||
|     bool show_notification = false; | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(Common::WebResult); | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <string> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #include <QByteArray> | ||||
| #include <QMetaType> | ||||
|  | @ -109,6 +111,8 @@ struct Values { | |||
|     QString room_port; | ||||
|     uint host_type; | ||||
|     qulonglong game_id; | ||||
|     QString room_description; | ||||
|     std::pair<std::vector<std::string>, std::vector<std::string>> ban_list; | ||||
| 
 | ||||
|     // logging
 | ||||
|     bool show_console; | ||||
|  |  | |||
|  | @ -17,13 +17,17 @@ using MacAddress = std::array<u8, 6>; | |||
| 
 | ||||
| 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 UID; | ||||
|     std::string description; | ||||
|     std::string owner; | ||||
|     std::string ip; | ||||
|     u16 port; | ||||
|  | @ -49,14 +53,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, | ||||
|                                     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; | ||||
|     /**
 | ||||
|  | @ -66,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 | ||||
|  | @ -99,15 +111,19 @@ public: | |||
| class NullBackend : public Backend { | ||||
| public: | ||||
|     ~NullBackend() = default; | ||||
|     void SetRoomInformation(const std::string& /*uid*/, const std::string& /*name*/, | ||||
|     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{}; | ||||
|  |  | |||
|  | @ -29,6 +29,21 @@ AnnounceMultiplayerSession::AnnounceMultiplayerSession() { | |||
| #endif | ||||
| } | ||||
| 
 | ||||
| void AnnounceMultiplayerSession::Register() { | ||||
|     std::shared_ptr<Network::Room> 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<Network::Room> room) { | ||||
|     Network::RoomInformation room_information = room->GetRoomInformation(); | ||||
|     std::vector<Network::Room::Member> 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<Common::WebResult> future; | ||||
|     while (!shutdown_event.WaitUntil(update_time)) { | ||||
|  | @ -76,24 +109,19 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { | |||
|         if (room->GetState() != Network::Room::State::Open) { | ||||
|             break; | ||||
|         } | ||||
|         Network::RoomInformation room_information = room->GetRoomInformation(); | ||||
|         std::vector<Network::Room::Member> 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->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<std::mutex> lock(callback_mutex); | ||||
|             for (auto callback : error_callbacks) { | ||||
|                 (*callback)(result); | ||||
|             } | ||||
|         } | ||||
|         if (result.result_string == "404") { | ||||
|             registered = false; | ||||
|             // Needs to register the room again
 | ||||
|             Register(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <atomic> | ||||
| #include <functional> | ||||
| #include <memory> | ||||
| #include <mutex> | ||||
|  | @ -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<AnnounceMultiplayerRoom::Backend> backend; | ||||
| 
 | ||||
|     std::atomic_bool registered = false; ///< Whether the room has been registered
 | ||||
| 
 | ||||
|     void UpdateBackendData(std::shared_ptr<Network::Room> room); | ||||
|     void AnnounceMultiplayerLoop(); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -734,4 +734,21 @@ void InstallInterfaces(Core::System& system) { | |||
|     std::make_shared<CFG_NOR>()->InstallAsService(service_manager); | ||||
| } | ||||
| 
 | ||||
| std::string GetConsoleIdHash(Core::System& system) { | ||||
|     u64_le console_id{}; | ||||
|     std::array<u8, sizeof(console_id)> buffer; | ||||
|     if (system.IsPoweredOn()) { | ||||
|         auto cfg = GetModule(system); | ||||
|         ASSERT_MSG(cfg, "CFG Module missing!"); | ||||
|         console_id = cfg->GetConsoleUniqueId(); | ||||
|     } else { | ||||
|         console_id = std::make_unique<Service::CFG::Module>()->GetConsoleUniqueId(); | ||||
|     } | ||||
|     std::memcpy(buffer.data(), &console_id, sizeof(console_id)); | ||||
| 
 | ||||
|     std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash; | ||||
|     CryptoPP::SHA256().CalculateDigest(hash.data(), buffer.data(), sizeof(buffer)); | ||||
|     return fmt::format("{:02x}", fmt::join(hash.begin(), hash.end(), "")); | ||||
| } | ||||
| 
 | ||||
| } // namespace Service::CFG
 | ||||
|  |  | |||
|  | @ -415,4 +415,7 @@ std::shared_ptr<Module> 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
 | ||||
|  |  | |||
|  | @ -140,7 +140,9 @@ std::list<Network::WifiPacket> 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); | ||||
|         } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <fstream> | ||||
| #include <iostream> | ||||
| #include <memory> | ||||
| #include <regex> | ||||
|  | @ -27,15 +28,23 @@ | |||
| #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 | ||||
| #include "web_service/verify_user_jwt.h" | ||||
| #endif | ||||
| 
 | ||||
| static void PrintHelp(const char* argv0) { | ||||
|     std::cout << "Usage: " << argv0 | ||||
|               << " [options] <filename>\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" | ||||
|  | @ -44,6 +53,8 @@ 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" | ||||
|                  "--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"; | ||||
| } | ||||
|  | @ -53,6 +64,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; | ||||
|  | @ -63,17 +139,21 @@ 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; | ||||
|     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; | ||||
|     bool enable_citra_mods = false; | ||||
| 
 | ||||
|     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'}, | ||||
|  | @ -82,18 +162,23 @@ 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'}, | ||||
|         {"enable-citra-mods", no_argument, 0, 'e'}, | ||||
|         {"help", no_argument, 0, 'h'}, | ||||
|         {"version", no_argument, 0, 'v'}, | ||||
|         {0, 0, 0, 0}, | ||||
|     }; | ||||
| 
 | ||||
|     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; | ||||
|  | @ -118,6 +203,12 @@ int main(int argc, char** argv) { | |||
|             case 'a': | ||||
|                 web_api_url.assign(optarg); | ||||
|                 break; | ||||
|             case 'b': | ||||
|                 ban_list_file.assign(optarg); | ||||
|                 break; | ||||
|             case 'e': | ||||
|                 enable_citra_mods = true; | ||||
|                 break; | ||||
|             case 'h': | ||||
|                 PrintHelp(argv[0]); | ||||
|                 return 0; | ||||
|  | @ -153,6 +244,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 <file>\n\n"; | ||||
|     } | ||||
|     bool announce = true; | ||||
|     if (username.empty()) { | ||||
|         announce = false; | ||||
|  | @ -172,11 +267,35 @@ 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; | ||||
|     if (!ban_list_file.empty()) { | ||||
|         ban_list = LoadBanList(ban_list_file); | ||||
|     } | ||||
| 
 | ||||
|     std::unique_ptr<Network::VerifyUser::Backend> verify_backend; | ||||
|     if (announce) { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|         verify_backend = std::make_unique<WebService::VerifyUserJWT>(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<Network::VerifyUser::NullBackend>(); | ||||
| #endif | ||||
|     } else { | ||||
|         verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); | ||||
|     } | ||||
| 
 | ||||
|     Network::Init(); | ||||
|     if (std::shared_ptr<Network::Room> 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, username, | ||||
|                           preferred_game, preferred_game_id, std::move(verify_backend), ban_list, | ||||
|                           enable_citra_mods)) { | ||||
|             std::cout << "Failed to create room: \n\n"; | ||||
|             return -1; | ||||
|         } | ||||
|  | @ -193,6 +312,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; | ||||
|  | @ -203,6 +326,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(); | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -126,7 +126,7 @@ Packet& Packet::operator>>(std::vector<T>& 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<T>& out_data) { | |||
| template <typename T, std::size_t S> | ||||
| Packet& Packet::operator>>(std::array<T, S>& out_data) { | ||||
|     for (std::size_t i = 0; i < out_data.size(); ++i) { | ||||
|         T character = 0; | ||||
|         T character; | ||||
|         *this >> character; | ||||
|         out_data[i] = character; | ||||
|     } | ||||
|  |  | |||
|  | @ -7,12 +7,14 @@ | |||
| #include <iomanip> | ||||
| #include <mutex> | ||||
| #include <random> | ||||
| #include <regex> | ||||
| #include <sstream> | ||||
| #include <thread> | ||||
| #include "common/logging/log.h" | ||||
| #include "enet/enet.h" | ||||
| #include "network/packet.h" | ||||
| #include "network/room.h" | ||||
| #include "network/verify_user.h" | ||||
| 
 | ||||
| namespace Network { | ||||
| 
 | ||||
|  | @ -27,12 +29,18 @@ public: | |||
|     std::atomic<State> 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 { | ||||
|         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.
 | ||||
|         /// Data of the user, often including authenticated forum username.
 | ||||
|         VerifyUser::UserData user_data; | ||||
|         ENetPeer* peer; ///< The remote peer.
 | ||||
|     }; | ||||
|     using MemberList = std::vector<Member>; | ||||
|  | @ -40,12 +48,19 @@ 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} {} | ||||
| 
 | ||||
|     /// Thread that receives and dispatches network packets
 | ||||
|     std::unique_ptr<std::thread> room_thread; | ||||
| 
 | ||||
|     /// Verification backend of the room
 | ||||
|     std::unique_ptr<VerifyUser::Backend> verify_backend; | ||||
| 
 | ||||
|     /// Thread function that will receive and dispatch messages until the room is destroyed.
 | ||||
|     void ServerLoop(); | ||||
|     void StartLoop(); | ||||
|  | @ -57,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. | ||||
|      */ | ||||
|  | @ -68,6 +107,22 @@ 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; | ||||
| 
 | ||||
|     /**
 | ||||
|      * 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. | ||||
|      */ | ||||
|     void SendRoomIsFull(ENetPeer* client); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. | ||||
|      */ | ||||
|  | @ -78,6 +133,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. | ||||
|      */ | ||||
|  | @ -94,17 +155,56 @@ 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. | ||||
|      */ | ||||
|     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, | ||||
|      */ | ||||
|     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. | ||||
|      * The packet has the structure: | ||||
|      * <MessageID>ID_ROOM_INFORMATION | ||||
|      * <String> room_name | ||||
|      * <String> room_description | ||||
|      * <u32> member_slots: The max number of clients allowed in this room | ||||
|      * <String> uid | ||||
|      * <u16> port | ||||
|  | @ -145,11 +245,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
 | ||||
|  | @ -172,6 +267,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; | ||||
|  | @ -193,12 +301,22 @@ void Room::RoomImpl::StartLoop() { | |||
| } | ||||
| 
 | ||||
| void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { | ||||
|     { | ||||
|         std::lock_guard<std::mutex> 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
 | ||||
|     std::string nickname; | ||||
|     packet >> nickname; | ||||
| 
 | ||||
|     std::string console_id_hash; | ||||
|     packet >> console_id_hash; | ||||
| 
 | ||||
|     MacAddress preferred_mac; | ||||
|     packet >> preferred_mac; | ||||
| 
 | ||||
|  | @ -208,6 +326,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; | ||||
|  | @ -229,6 +350,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; | ||||
|  | @ -237,9 +363,43 @@ 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; | ||||
| 
 | ||||
|     std::string uid; | ||||
|     { | ||||
|         std::lock_guard<std::mutex> lock(verify_UID_mutex); | ||||
|         uid = verify_UID; | ||||
|     } | ||||
|     member.user_data = verify_backend->LoadUserData(uid, token); | ||||
| 
 | ||||
|     { | ||||
|         std::lock_guard<std::mutex> 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); | ||||
| 
 | ||||
|     { | ||||
|         std::lock_guard<std::mutex> lock(member_mutex); | ||||
|         members.push_back(std::move(member)); | ||||
|  | @ -247,12 +407,167 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { | |||
| 
 | ||||
|     // Notify everyone that the room information has changed.
 | ||||
|     BroadcastRoomInformation(); | ||||
|     if (HasModPermission(event->peer)) { | ||||
|         SendJoinSuccessAsMod(event->peer, preferred_mac); | ||||
|     } else { | ||||
|         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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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 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<std::mutex> lock(member_mutex); | ||||
|     return std::all_of(members.begin(), members.end(), | ||||
|                        [&nickname](const auto& member) { return member.nickname != nickname; }); | ||||
|  | @ -265,6 +580,35 @@ 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<std::mutex> 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; | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { | ||||
|     std::lock_guard<std::mutex> 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 (room_information.enable_citra_mods && | ||||
|         sending_member->user_data.moderator) { // Community moderator
 | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
|     if (!room_information.host_username.empty() && | ||||
|         sending_member->user_data.username == room_information.host_username) { // Room host
 | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| void Room::RoomImpl::SendNameCollision(ENetPeer* client) { | ||||
|     Packet packet; | ||||
|     packet << static_cast<u8>(IdNameCollision); | ||||
|  | @ -285,6 +629,16 @@ void Room::RoomImpl::SendMacCollision(ENetPeer* client) { | |||
|     enet_host_flush(server); | ||||
| } | ||||
| 
 | ||||
| void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { | ||||
|     Packet packet; | ||||
|     packet << static_cast<u8>(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<u8>(IdWrongPassword); | ||||
|  | @ -295,6 +649,16 @@ void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { | |||
|     enet_host_flush(server); | ||||
| } | ||||
| 
 | ||||
| void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { | ||||
|     Packet packet; | ||||
|     packet << static_cast<u8>(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<u8>(IdVersionMismatch); | ||||
|  | @ -316,6 +680,71 @@ 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<u8>(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<u8>(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<u8>(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<u8>(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<u8>(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<u8>(IdModBanListResponse); | ||||
|     { | ||||
|         std::lock_guard<std::mutex> 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<u8>(IdCloseRoom); | ||||
|  | @ -333,14 +762,33 @@ void Room::RoomImpl::SendCloseMessage() { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, | ||||
|                                        const std::string& username) { | ||||
|     Packet packet; | ||||
|     packet << static_cast<u8>(IdStatusMessage); | ||||
|     packet << static_cast<u8>(type); | ||||
|     packet << nickname; | ||||
|     packet << username; | ||||
|     std::lock_guard<std::mutex> 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<u8>(IdRoomInformation); | ||||
|     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; | ||||
|     packet << room_information.host_username; | ||||
| 
 | ||||
|     packet << static_cast<u32>(members.size()); | ||||
|     { | ||||
|  | @ -350,6 +798,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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -442,6 +893,7 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { | |||
|     Packet out_packet; | ||||
|     out_packet << static_cast<u8>(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(), | ||||
|  | @ -485,41 +937,37 @@ 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<std::mutex> 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(); | ||||
| } | ||||
| 
 | ||||
| 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<RoomImpl>()} {} | ||||
| 
 | ||||
| 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& host_username, | ||||
|                   const std::string& preferred_game, u64 preferred_game_id, | ||||
|                   std::unique_ptr<VerifyUser::Backend> verify_backend, | ||||
|                   const Room::BanList& ban_list, bool enable_citra_mods) { | ||||
|     ENetAddress address; | ||||
|     address.host = ENET_HOST_ANY; | ||||
|     if (!server_address.empty()) { | ||||
|  | @ -527,19 +975,26 @@ 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; | ||||
|     } | ||||
|     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; | ||||
|     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->CreateUniqueID(); | ||||
|     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; | ||||
|  | @ -553,12 +1008,25 @@ const RoomInformation& Room::GetRoomInformation() const { | |||
|     return room_impl->room_information; | ||||
| } | ||||
| 
 | ||||
| std::string Room::GetVerifyUID() const { | ||||
|     std::lock_guard<std::mutex> lock(room_impl->verify_UID_mutex); | ||||
|     return room_impl->verify_UID; | ||||
| } | ||||
| 
 | ||||
| Room::BanList Room::GetBanList() const { | ||||
|     std::lock_guard<std::mutex> lock(room_impl->ban_list_mutex); | ||||
|     return {room_impl->username_ban_list, room_impl->ip_ban_list}; | ||||
| } | ||||
| 
 | ||||
| std::vector<Room::Member> Room::GetRoomMemberList() const { | ||||
|     std::vector<Room::Member> member_list; | ||||
|     std::lock_guard<std::mutex> 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); | ||||
|  | @ -570,6 +1038,11 @@ bool Room::HasPassword() const { | |||
|     return !room_impl->password.empty(); | ||||
| } | ||||
| 
 | ||||
| void Room::SetVerifyUID(const std::string& uid) { | ||||
|     std::lock_guard<std::mutex> lock(room_impl->verify_UID_mutex); | ||||
|     room_impl->verify_UID = uid; | ||||
| } | ||||
| 
 | ||||
| void Room::Destroy() { | ||||
|     room_impl->state = State::Closed; | ||||
|     room_impl->room_thread->join(); | ||||
|  |  | |||
|  | @ -9,10 +9,11 @@ | |||
| #include <string> | ||||
| #include <vector> | ||||
| #include "common/common_types.h" | ||||
| #include "network/verify_user.h" | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|  | @ -25,11 +26,13 @@ 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
 | ||||
|     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 { | ||||
|  | @ -57,7 +60,31 @@ enum RoomMessageTypes : u8 { | |||
|     IdMacCollision, | ||||
|     IdVersionMismatch, | ||||
|     IdWrongPassword, | ||||
|     IdCloseRoom | ||||
|     IdCloseRoom, | ||||
|     IdRoomIsFull, | ||||
|     IdConsoleIdCollision, | ||||
|     IdStatusMessage, | ||||
|     IdHostKicked, | ||||
|     IdHostBanned, | ||||
|     /// Moderation requests
 | ||||
|     IdModKick, | ||||
|     IdModBan, | ||||
|     IdModUnban, | ||||
|     IdModGetBanList, | ||||
|     // Moderation responses
 | ||||
|     IdModBanListResponse, | ||||
|     IdModPermissionDenied, | ||||
|     IdModNoSuchUser, | ||||
|     IdJoinSuccessAsMod, | ||||
| }; | ||||
| 
 | ||||
| /// Types of system status messages
 | ||||
| enum StatusMessageTypes : u8 { | ||||
|     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.
 | ||||
|  | @ -70,6 +97,9 @@ public: | |||
| 
 | ||||
|     struct 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.
 | ||||
|     }; | ||||
|  | @ -87,6 +117,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. | ||||
|      */ | ||||
|  | @ -97,14 +132,33 @@ public: | |||
|      */ | ||||
|     bool HasPassword() const; | ||||
| 
 | ||||
|     using UsernameBanList = std::vector<std::string>; | ||||
|     using IPBanList = std::vector<std::string>; | ||||
| 
 | ||||
|     using BanList = std::pair<UsernameBanList, IPBanList>; | ||||
| 
 | ||||
|     /**
 | ||||
|      * 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); | ||||
|                 const std::string& host_username = "", const std::string& preferred_game = "", | ||||
|                 u64 preferred_game_id = 0, | ||||
|                 std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr, | ||||
|                 const BanList& ban_list = {}, bool enable_citra_mods = false); | ||||
| 
 | ||||
|     /**
 | ||||
|      * 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 | ||||
|  |  | |||
|  | @ -31,9 +31,14 @@ public: | |||
| 
 | ||||
|     std::atomic<State> 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.
 | ||||
| 
 | ||||
|     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.
 | ||||
|  | @ -54,8 +59,11 @@ public: | |||
|     private: | ||||
|         CallbackSet<WifiPacket> callback_set_wifi_packet; | ||||
|         CallbackSet<ChatEntry> callback_set_chat_messages; | ||||
|         CallbackSet<StatusMessageEntry> callback_set_status_messages; | ||||
|         CallbackSet<RoomInformation> callback_set_room_information; | ||||
|         CallbackSet<State> callback_set_state; | ||||
|         CallbackSet<Error> callback_set_error; | ||||
|         CallbackSet<Room::BanList> callback_set_ban_list; | ||||
|     }; | ||||
|     Callbacks callbacks; ///< All CallbackSets to all events
 | ||||
| 
 | ||||
|  | @ -73,13 +81,14 @@ 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 = ""); | ||||
|                          const std::string& password = "", const std::string& token = ""); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Extracts a MAC Address from a received ENet packet. | ||||
|  | @ -104,6 +113,19 @@ 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); | ||||
| 
 | ||||
|     /**
 | ||||
|      * 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 | ||||
|      */ | ||||
|  | @ -124,8 +146,12 @@ void RoomMember::RoomMemberImpl::SetState(const State new_state) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void RoomMember::RoomMemberImpl::SetError(const Error new_error) { | ||||
|     Invoke<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() { | ||||
|  | @ -143,37 +169,78 @@ void RoomMember::RoomMemberImpl::MemberLoop() { | |||
|                 case IdChatMessage: | ||||
|                     HandleChatPacket(&event); | ||||
|                     break; | ||||
|                 case IdStatusMessage: | ||||
|                     HandleStatusMessagePacket(&event); | ||||
|                     break; | ||||
|                 case IdRoomInformation: | ||||
|                     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
 | ||||
|                     if (event.packet->data[0] == IdJoinSuccessAsMod) { | ||||
|                         SetState(State::Moderator); | ||||
|                     } else { | ||||
|                         SetState(State::Joined); | ||||
|                     } | ||||
|                     break; | ||||
|                 case IdModBanListResponse: | ||||
|                     HandleModBanListResponsePacket(&event); | ||||
|                     break; | ||||
|                 case IdRoomIsFull: | ||||
|                     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::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 || state == State::Moderator) { | ||||
|                     SetState(State::Idle); | ||||
|                     SetError(Error::LostConnection); | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | @ -201,14 +268,18 @@ 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<u8>(IdJoinRequest); | ||||
|     packet << nickname; | ||||
|     packet << console_id_hash; | ||||
|     packet << preferred_mac; | ||||
|     packet << network_version; | ||||
|     packet << password; | ||||
|     packet << token; | ||||
|     Send(std::move(packet)); | ||||
| } | ||||
| 
 | ||||
|  | @ -221,14 +292,17 @@ 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; | ||||
|     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; | ||||
|  | @ -239,6 +313,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<std::mutex> lock(username_mutex); | ||||
|             if (member.nickname == nickname) { | ||||
|                 username = member.username; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Invoke(room_information); | ||||
| } | ||||
|  | @ -252,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) { | ||||
|  | @ -286,10 +369,40 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { | |||
| 
 | ||||
|     ChatEntry chat_entry{}; | ||||
|     packet >> chat_entry.nickname; | ||||
|     packet >> chat_entry.username; | ||||
|     packet >> chat_entry.message; | ||||
|     Invoke<ChatEntry>(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<StatusMessageTypes>(type); | ||||
|     packet >> status_message_entry.nickname; | ||||
|     packet >> status_message_entry.username; | ||||
|     Invoke<StatusMessageEntry>(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<Room::BanList>(ban_list); | ||||
| } | ||||
| 
 | ||||
| void RoomMember::RoomMemberImpl::Disconnect() { | ||||
|     member_information.clear(); | ||||
|     room_information.member_slots = 0; | ||||
|  | @ -329,6 +442,12 @@ RoomMember::RoomMemberImpl::Callbacks::Get() { | |||
|     return callback_set_state; | ||||
| } | ||||
| 
 | ||||
| template <> | ||||
| RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>& | ||||
| RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||
|     return callback_set_error; | ||||
| } | ||||
| 
 | ||||
| template <> | ||||
| RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& | ||||
| RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||
|  | @ -340,6 +459,18 @@ RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl:: | |||
|     return callback_set_chat_messages; | ||||
| } | ||||
| 
 | ||||
| template <> | ||||
| RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>& | ||||
| RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||
|     return callback_set_status_messages; | ||||
| } | ||||
| 
 | ||||
| template <> | ||||
| RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>& | ||||
| RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||
|     return callback_set_ban_list; | ||||
| } | ||||
| 
 | ||||
| template <typename T> | ||||
| void RoomMember::RoomMemberImpl::Invoke(const T& data) { | ||||
|     std::lock_guard<std::mutex> lock(callback_mutex); | ||||
|  | @ -380,6 +511,11 @@ const std::string& RoomMember::GetNickname() const { | |||
|     return room_member_impl->nickname; | ||||
| } | ||||
| 
 | ||||
| const std::string& RoomMember::GetUsername() const { | ||||
|     std::lock_guard<std::mutex> 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; | ||||
|  | @ -389,9 +525,10 @@ 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, | ||||
|                       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(); | ||||
|  | @ -415,7 +552,8 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv | |||
|         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; | ||||
|     } | ||||
| 
 | ||||
|  | @ -424,11 +562,12 @@ 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, token); | ||||
|         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); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -466,11 +605,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<u8>(type); | ||||
|     packet << nickname; | ||||
|     room_member_impl->Send(std::move(packet)); | ||||
| } | ||||
| 
 | ||||
| void RoomMember::RequestBanList() { | ||||
|     if (!IsConnected()) | ||||
|         return; | ||||
| 
 | ||||
|     Packet packet; | ||||
|     packet << static_cast<u8>(IdModGetBanList); | ||||
|     room_member_impl->Send(std::move(packet)); | ||||
| } | ||||
| 
 | ||||
| RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged( | ||||
|     std::function<void(const RoomMember::State&)> callback) { | ||||
|     return room_member_impl->Bind(callback); | ||||
| } | ||||
| 
 | ||||
| RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError( | ||||
|     std::function<void(const RoomMember::Error&)> callback) { | ||||
|     return room_member_impl->Bind(callback); | ||||
| } | ||||
| 
 | ||||
| RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived( | ||||
|     std::function<void(const WifiPacket&)> callback) { | ||||
|     return room_member_impl->Bind(callback); | ||||
|  | @ -486,6 +651,16 @@ RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved( | |||
|     return room_member_impl->Bind(callback); | ||||
| } | ||||
| 
 | ||||
| RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived( | ||||
|     std::function<void(const StatusMessageEntry&)> callback) { | ||||
|     return room_member_impl->Bind(callback); | ||||
| } | ||||
| 
 | ||||
| RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived( | ||||
|     std::function<void(const Room::BanList&)> callback) { | ||||
|     return room_member_impl->Bind(callback); | ||||
| } | ||||
| 
 | ||||
| template <typename T> | ||||
| void RoomMember::Unbind(CallbackHandle<T> handle) { | ||||
|     std::lock_guard<std::mutex> lock(room_member_impl->callback_mutex); | ||||
|  | @ -503,7 +678,10 @@ void RoomMember::Leave() { | |||
| 
 | ||||
| template void RoomMember::Unbind(CallbackHandle<WifiPacket>); | ||||
| template void RoomMember::Unbind(CallbackHandle<RoomMember::State>); | ||||
| template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>); | ||||
| template void RoomMember::Unbind(CallbackHandle<RoomInformation>); | ||||
| template void RoomMember::Unbind(CallbackHandle<ChatEntry>); | ||||
| template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>); | ||||
| template void RoomMember::Unbind(CallbackHandle<Room::BanList>); | ||||
| 
 | ||||
| } // namespace Network
 | ||||
|  |  | |||
|  | @ -35,9 +35,19 @@ struct WifiPacket { | |||
| /// Represents a chat message.
 | ||||
| struct ChatEntry { | ||||
|     std::string nickname; ///< Nickname of the client who sent this message.
 | ||||
|     /// Web services username of the client who sent this message, can be empty.
 | ||||
|     std::string username; | ||||
|     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 | ||||
|  | @ -47,22 +57,38 @@ 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.
 | ||||
|         Moderator, ///< The client is connnected to the room and is granted mod permissions.
 | ||||
|     }; | ||||
| 
 | ||||
|     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
 | ||||
|         CouldNotConnect,    ///< The room is not responding to a connection attempt
 | ||||
|         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 { | ||||
|         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.
 | ||||
|  | @ -98,6 +124,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. | ||||
|      */ | ||||
|  | @ -115,11 +146,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 = "", const std::string& token = ""); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Sends a WiFi packet to the room. | ||||
|  | @ -139,6 +172,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 | ||||
|  | @ -148,6 +194,15 @@ public: | |||
|      */ | ||||
|     CallbackHandle<State> BindOnStateChanged(std::function<void(const State&)> 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<Error> BindOnError(std::function<void(const Error&)> 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. | ||||
|  | @ -178,6 +233,26 @@ public: | |||
|     CallbackHandle<ChatEntry> BindOnChatMessageRecieved( | ||||
|         std::function<void(const ChatEntry&)> 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<StatusMessageEntry> BindOnStatusMessageReceived( | ||||
|         std::function<void(const StatusMessageEntry&)> 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<Room::BanList> BindOnBanListReceived( | ||||
|         std::function<void(const Room::BanList&)> callback); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Leaves the current room. | ||||
|      */ | ||||
|  | @ -192,24 +267,44 @@ 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: | ||||
|     case RoomMember::State::Moderator: | ||||
|         return "Moderator"; | ||||
|     } | ||||
|     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"; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										18
									
								
								src/network/verify_user.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/network/verify_user.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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
 | ||||
							
								
								
									
										46
									
								
								src/network/verify_user.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/network/verify_user.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| // Copyright 2018 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <string> | ||||
| #include "common/logging/log.h" | ||||
| 
 | ||||
| namespace Network::VerifyUser { | ||||
| 
 | ||||
| struct UserData { | ||||
|     std::string username; | ||||
|     std::string display_name; | ||||
|     std::string avatar_url; | ||||
|     bool moderator = false; ///< Whether the user is a Citra Moderator.
 | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * 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
 | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -12,21 +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<std::string>(); | ||||
|     member.nickname = json.at("nickname").get<std::string>(); | ||||
|     member.game_name = json.at("gameName").get<std::string>(); | ||||
|     member.game_id = json.at("gameId").get<u64>(); | ||||
|     try { | ||||
|         member.username = json.at("username").get<std::string>(); | ||||
|         member.avatar_url = json.at("avatarUrl").get<std::string>(); | ||||
|     } 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; | ||||
|     if (!room.description.empty()) { | ||||
|         json["description"] = room.description; | ||||
|     } | ||||
|     json["preferredGameName"] = room.preferred_game; | ||||
|     json["preferredGameId"] = room.preferred_game_id; | ||||
|     json["maxPlayers"] = room.max_player; | ||||
|  | @ -39,8 +54,15 @@ 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<std::string>(); | ||||
|     room.ip = json.at("address").get<std::string>(); | ||||
|     room.name = json.at("name").get<std::string>(); | ||||
|     try { | ||||
|         room.description = json.at("description").get<std::string>(); | ||||
|     } 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<std::string>(); | ||||
|     room.port = json.at("port").get<u16>(); | ||||
|     room.preferred_game = json.at("preferredGameName").get<std::string>(); | ||||
|  | @ -59,12 +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 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.UID = uid; | ||||
|     room.description = description; | ||||
|     room.port = port; | ||||
|     room.max_player = max_player; | ||||
|     room.net_version = net_version; | ||||
|  | @ -72,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("/lobby/{}", room_id), json.dump(), false); | ||||
| } | ||||
| 
 | ||||
| std::string RoomJson::Register() { | ||||
|     nlohmann::json json = room; | ||||
|     return client.PostJson("/lobby", json.dump(), false); | ||||
|     auto reply = client.PostJson("/lobby", json.dump(), false).returned_data; | ||||
|     if (reply.empty()) { | ||||
|         return ""; | ||||
|     } | ||||
|     auto reply_json = nlohmann::json::parse(reply); | ||||
|     room = reply_json.get<AnnounceMultiplayerRoom::Room>(); | ||||
|     room_id = reply_json.at("id").get<std::string>(); | ||||
|     return room.verify_UID; | ||||
| } | ||||
| 
 | ||||
| void RoomJson::ClearPlayers() { | ||||
|  | @ -101,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("/lobby/{}", room_id), "", false); | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,14 +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 u16 port, | ||||
|     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; | ||||
|  | @ -38,6 +40,7 @@ private: | |||
|     std::string host; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
|     std::string room_id; | ||||
| }; | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
							
								
								
									
										60
									
								
								src/web_service/verify_user_jwt.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/web_service/verify_user_jwt.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| // Copyright 2018 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <system_error> | ||||
| #include <jwt/jwt.hpp> | ||||
| #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.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 { | ||||
|             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<std::string>("username"); | ||||
|     } | ||||
|     if (decoded.payload().has_claim("displayName")) { | ||||
|         user_data.display_name = decoded.payload().get_claim_value<std::string>("displayName"); | ||||
|     } | ||||
|     if (decoded.payload().has_claim("avatarUrl")) { | ||||
|         user_data.avatar_url = decoded.payload().get_claim_value<std::string>("avatarUrl"); | ||||
|     } | ||||
|     if (decoded.payload().has_claim("roles")) { | ||||
|         auto roles = decoded.payload().get_claim_value<std::vector<std::string>>("roles"); | ||||
|         user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end(); | ||||
|     } | ||||
|     return user_data; | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
							
								
								
									
										25
									
								
								src/web_service/verify_user_jwt.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/web_service/verify_user_jwt.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <fmt/format.h> | ||||
| #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
 | ||||
|  | @ -7,6 +7,7 @@ | |||
| #include <mutex> | ||||
| #include <string> | ||||
| #include <LUrlParser.h> | ||||
| #include <fmt/format.h> | ||||
| #include <httplib.h> | ||||
| #include "common/common_types.h" | ||||
| #include "common/logging/log.h" | ||||
|  | @ -32,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(); | ||||
|         } | ||||
|  | @ -44,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; | ||||
|  | @ -60,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; | ||||
|  | @ -133,8 +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) { | ||||
|         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"}; | ||||
|  | @ -148,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 { | ||||
|  | @ -181,16 +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 impl->GenericRequest("POST", fmt::format("/jwt/external/{}", audience), "", false, | ||||
|                                 "text/html"); | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -46,6 +46,29 @@ 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. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult GetExternalJWT(const std::string& audience); | ||||
| 
 | ||||
| private: | ||||
|     struct Impl; | ||||
|     std::unique_ptr<Impl> impl; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue