mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +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"] | [submodule "externals/cppzmq"] | ||||||
| 	path = externals/cppzmq | 	path = externals/cppzmq | ||||||
| 	url = https://github.com/zeromq/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/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.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/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/failed.png | Free for non-commercial use | ||||||
| qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com | 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/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/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/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/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/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/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.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/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/failed.png | Free for non-commercial use | ||||||
| qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com | 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/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/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/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/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/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.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/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/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 | 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"> |     <qresource prefix="icons/colorful"> | ||||||
|         <file alias="index.theme">icons/index.theme</file> |         <file alias="index.theme">icons/index.theme</file> | ||||||
|         <file alias="16x16/connected.png">icons/16x16/connected.png</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/disconnected.png">icons/16x16/disconnected.png</file> | ||||||
|         <file alias="16x16/lock.png">icons/16x16/lock.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/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"> |     <qresource prefix="icons/colorful_dark"> | ||||||
|         <file alias="index.theme">icons/index.theme</file> |         <file alias="index.theme">icons/index.theme</file> | ||||||
|         <file alias="16x16/connected.png">../colorful/icons/16x16/connected.png</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/disconnected.png">../colorful/icons/16x16/disconnected.png</file> | ||||||
|         <file alias="16x16/lock.png">icons/16x16/lock.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/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file> | ||||||
|         <file alias="48x48/chip.png">../colorful/icons/48x48/chip.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/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/plus.png">../colorful/icons/48x48/plus.png</file> | ||||||
|         <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.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> |         <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/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="16x16/lock.png">icons/16x16/lock.png</file> | ||||||
| 
 | 
 | ||||||
|         <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.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/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/plus.png">icons/48x48/plus.png</file> | ||||||
|        |        | ||||||
|         <file alias="48x48/sd_card.png">icons/48x48/sd_card.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="index.theme">icons/index.theme</file> | ||||||
|     <file alias="16x16/connected.png">icons/16x16/connected.png</file> |     <file alias="16x16/connected.png">icons/16x16/connected.png</file> | ||||||
|     <file alias="16x16/disconnected.png">icons/16x16/disconnected.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="16x16/lock.png">icons/16x16/lock.png</file> | ||||||
|     <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.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/chip.png">icons/48x48/chip.png</file> | ||||||
|     <file alias="48x48/folder.png">icons/48x48/folder.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/plus.png">icons/48x48/plus.png</file> | ||||||
|     <file alias="48x48/sd_card.png">icons/48x48/sd_card.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> |     <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_include_directories(ssl INTERFACE ./libressl/include) | ||||||
|     target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP) |     target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP) | ||||||
| 
 | 
 | ||||||
|  |     # JSON | ||||||
|  |     add_library(json-headers INTERFACE) | ||||||
|  |     target_include_directories(json-headers INTERFACE ./json) | ||||||
|  | 
 | ||||||
|     # lurlparser |     # lurlparser | ||||||
|     add_subdirectory(lurlparser EXCLUDE_FROM_ALL) |     add_subdirectory(lurlparser EXCLUDE_FROM_ALL) | ||||||
| 
 | 
 | ||||||
|  | @ -89,9 +93,9 @@ if (ENABLE_WEB_SERVICE) | ||||||
|     add_library(httplib INTERFACE) |     add_library(httplib INTERFACE) | ||||||
|     target_include_directories(httplib INTERFACE ./httplib) |     target_include_directories(httplib INTERFACE ./httplib) | ||||||
| 
 | 
 | ||||||
|     # JSON |     # cpp-jwt | ||||||
|     add_library(json-headers INTERFACE) |     add_library(cpp-jwt INTERFACE) | ||||||
|     target_include_directories(json-headers INTERFACE ./json) |     target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) | ||||||
| endif() | endif() | ||||||
| 
 | 
 | ||||||
| if (ENABLE_SCRIPTING) | 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 | checked.png                        | Free for non-commercial use | ||||||
| connected.png                      | CC BY-ND 3.0  | https://icons8.com | 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 | disconnected.png                   | CC BY-ND 3.0  | https://icons8.com | ||||||
| failed.png                         | Free for non-commercial use | failed.png                         | Free for non-commercial use | ||||||
| lock.png                           | CC BY-ND 3.0  | https://icons8.com | lock.png                           | CC BY-ND 3.0  | https://icons8.com | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ | ||||||
| #include "core/frontend/applets/default_applets.h" | #include "core/frontend/applets/default_applets.h" | ||||||
| #include "core/gdbstub/gdbstub.h" | #include "core/gdbstub/gdbstub.h" | ||||||
| #include "core/hle/service/am/am.h" | #include "core/hle/service/am/am.h" | ||||||
|  | #include "core/hle/service/cfg/cfg.h" | ||||||
| #include "core/loader/loader.h" | #include "core/loader/loader.h" | ||||||
| #include "core/movie.h" | #include "core/movie.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
|  | @ -80,42 +81,87 @@ static void OnStateChanged(const Network::RoomMember::State& state) { | ||||||
|     case Network::RoomMember::State::Joined: |     case Network::RoomMember::State::Joined: | ||||||
|         LOG_DEBUG(Network, "Successfully joined to the room"); |         LOG_DEBUG(Network, "Successfully joined to the room"); | ||||||
|         break; |         break; | ||||||
|     case Network::RoomMember::State::LostConnection: |     case Network::RoomMember::State::Moderator: | ||||||
|         LOG_DEBUG(Network, "Lost connection to the room"); |         LOG_DEBUG(Network, "Successfully joined the room as a moderator"); | ||||||
|         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); |  | ||||||
|         break; |         break; | ||||||
|     default: |     default: | ||||||
|         break; |         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) { | static void OnMessageReceived(const Network::ChatEntry& msg) { | ||||||
|     std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; |     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() { | static void InitializeLogging() { | ||||||
|     Log::Filter log_filter(Log::Level::Debug); |     Log::Filter log_filter(Log::Level::Debug); | ||||||
|     log_filter.ParseFilterString(Settings::values.log_filter); |     log_filter.ParseFilterString(Settings::values.log_filter); | ||||||
|  | @ -333,10 +379,13 @@ int main(int argc, char** argv) { | ||||||
|     if (use_multiplayer) { |     if (use_multiplayer) { | ||||||
|         if (auto member = Network::GetRoomMember().lock()) { |         if (auto member = Network::GetRoomMember().lock()) { | ||||||
|             member->BindOnChatMessageRecieved(OnMessageReceived); |             member->BindOnChatMessageRecieved(OnMessageReceived); | ||||||
|  |             member->BindOnStatusMessageReceived(OnStatusMessageReceived); | ||||||
|             member->BindOnStateChanged(OnStateChanged); |             member->BindOnStateChanged(OnStateChanged); | ||||||
|  |             member->BindOnError(OnNetworkError); | ||||||
|             LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, |             LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, | ||||||
|                       nickname); |                       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 { |         } else { | ||||||
|             LOG_ERROR(Network, "Could not access RoomMember"); |             LOG_ERROR(Network, "Could not access RoomMember"); | ||||||
|             return 0; |             return 0; | ||||||
|  |  | ||||||
|  | @ -99,6 +99,8 @@ add_executable(citra-qt | ||||||
|     multiplayer/lobby.cpp |     multiplayer/lobby.cpp | ||||||
|     multiplayer/message.h |     multiplayer/message.h | ||||||
|     multiplayer/message.cpp |     multiplayer/message.cpp | ||||||
|  |     multiplayer/moderation_dialog.cpp | ||||||
|  |     multiplayer/moderation_dialog.h | ||||||
|     multiplayer/state.cpp |     multiplayer/state.cpp | ||||||
|     multiplayer/state.h |     multiplayer/state.h | ||||||
|     multiplayer/validation.h |     multiplayer/validation.h | ||||||
|  | @ -135,6 +137,7 @@ set(UIS | ||||||
|     multiplayer/chat_room.ui |     multiplayer/chat_room.ui | ||||||
|     multiplayer/client_room.ui |     multiplayer/client_room.ui | ||||||
|     multiplayer/host_room.ui |     multiplayer/host_room.ui | ||||||
|  |     multiplayer/moderation_dialog.ui | ||||||
|     aboutdialog.ui |     aboutdialog.ui | ||||||
|     cheats.ui |     cheats.ui | ||||||
|     hotkeys.ui |     hotkeys.ui | ||||||
|  | @ -228,6 +231,10 @@ if (USE_DISCORD_PRESENCE) | ||||||
|     target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE) |     target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE) | ||||||
| endif() | endif() | ||||||
| 
 | 
 | ||||||
|  | if (ENABLE_WEB_SERVICE) | ||||||
|  |     target_compile_definitions(citra-qt PRIVATE -DENABLE_WEB_SERVICE) | ||||||
|  | endif() | ||||||
|  | 
 | ||||||
| if(UNIX AND NOT APPLE) | if(UNIX AND NOT APPLE) | ||||||
|     install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") |     install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") | ||||||
| endif() | endif() | ||||||
|  |  | ||||||
|  | @ -329,6 +329,22 @@ void Config::ReadValues() { | ||||||
|     } |     } | ||||||
|     UISettings::values.max_player = ReadSetting("max_player", 8).toUInt(); |     UISettings::values.max_player = ReadSetting("max_player", 8).toUInt(); | ||||||
|     UISettings::values.game_id = ReadSetting("game_id", 0).toULongLong(); |     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(); | ||||||
| 
 | 
 | ||||||
|     qt_config->endGroup(); |     qt_config->endGroup(); | ||||||
|  | @ -533,6 +549,20 @@ void Config::SaveValues() { | ||||||
|     WriteSetting("host_type", UISettings::values.host_type, 0); |     WriteSetting("host_type", UISettings::values.host_type, 0); | ||||||
|     WriteSetting("max_player", UISettings::values.max_player, 8); |     WriteSetting("max_player", UISettings::values.max_player, 8); | ||||||
|     WriteSetting("game_id", UISettings::values.game_id, 0); |     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(); | ||||||
| 
 | 
 | ||||||
|     qt_config->endGroup(); |     qt_config->endGroup(); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| #include <array> | #include <array> | ||||||
| #include <future> | #include <future> | ||||||
| #include <QColor> | #include <QColor> | ||||||
|  | #include <QDesktopServices> | ||||||
|  | #include <QFutureWatcher> | ||||||
| #include <QImage> | #include <QImage> | ||||||
| #include <QList> | #include <QList> | ||||||
| #include <QLocale> | #include <QLocale> | ||||||
|  | @ -12,6 +14,7 @@ | ||||||
| #include <QMessageBox> | #include <QMessageBox> | ||||||
| #include <QMetaType> | #include <QMetaType> | ||||||
| #include <QTime> | #include <QTime> | ||||||
|  | #include <QUrl> | ||||||
| #include <QtConcurrent/QtConcurrentRun> | #include <QtConcurrent/QtConcurrentRun> | ||||||
| #include "citra_qt/game_list_p.h" | #include "citra_qt/game_list_p.h" | ||||||
| #include "citra_qt/multiplayer/chat_room.h" | #include "citra_qt/multiplayer/chat_room.h" | ||||||
|  | @ -19,6 +22,9 @@ | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "core/announce_multiplayer_session.h" | #include "core/announce_multiplayer_session.h" | ||||||
| #include "ui_chat_room.h" | #include "ui_chat_room.h" | ||||||
|  | #ifdef ENABLE_WEB_SERVICE | ||||||
|  | #include "web_service/web_backend.h" | ||||||
|  | #endif | ||||||
| 
 | 
 | ||||||
| class ChatMessage { | class ChatMessage { | ||||||
| public: | public: | ||||||
|  | @ -27,24 +33,60 @@ public: | ||||||
|         QLocale locale; |         QLocale locale; | ||||||
|         timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); |         timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); | ||||||
|         nickname = QString::fromStdString(chat.nickname); |         nickname = QString::fromStdString(chat.nickname); | ||||||
|  |         username = QString::fromStdString(chat.username); | ||||||
|         message = QString::fromStdString(chat.message); |         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
 |     /// Format the message using the players color
 | ||||||
|     QString GetPlayerChatMessage(u16 player) const { |     QString GetPlayerChatMessage(u16 player) const { | ||||||
|         auto color = player_color[player % 16]; |         auto color = player_color[player % 16]; | ||||||
|         return QString("[%1] <font color='%2'><%3></font> %4") |         QString name; | ||||||
|             .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped()); |         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: | private: | ||||||
|     static constexpr std::array<const char*, 16> player_color = { |     static constexpr std::array<const char*, 16> player_color = { | ||||||
|         {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", |         {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", | ||||||
|          "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; |          "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; | ||||||
|  |     static constexpr char ping_color[] = "#FFFF00"; | ||||||
| 
 | 
 | ||||||
|     QString timestamp; |     QString timestamp; | ||||||
|     QString nickname; |     QString nickname; | ||||||
|  |     QString username; | ||||||
|     QString message; |     QString message; | ||||||
|  |     bool contains_ping; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class StatusMessage { | class StatusMessage { | ||||||
|  | @ -57,37 +99,69 @@ public: | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     QString GetSystemChatMessage() const { |     QString GetSystemChatMessage() const { | ||||||
|         return QString("[%1] <font color='%2'><i>%3</i></font>") |         return QString("[%1] <font color='%2'>* %3</font>").arg(timestamp, system_color, message); | ||||||
|             .arg(timestamp, system_color, message); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     static constexpr const char system_color[] = "#888888"; |     static constexpr const char system_color[] = "#FF8C00"; | ||||||
|     QString timestamp; |     QString timestamp; | ||||||
|     QString message; |     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>()) { | ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) { | ||||||
|     ui->setupUi(this); |     ui->setupUi(this); | ||||||
| 
 | 
 | ||||||
|     // set the item_model for player_view
 |     // set the item_model for player_view
 | ||||||
|     enum { |  | ||||||
|         COLUMN_NAME, |  | ||||||
|         COLUMN_GAME, |  | ||||||
|         COLUMN_COUNT, // Number of columns
 |  | ||||||
|     }; |  | ||||||
| 
 | 
 | ||||||
|     player_list = new QStandardItemModel(ui->player_view); |     player_list = new QStandardItemModel(ui->player_view); | ||||||
|     ui->player_view->setModel(player_list); |     ui->player_view->setModel(player_list); | ||||||
|     ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); |     ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); | ||||||
|     player_list->insertColumns(0, COLUMN_COUNT); |     // set a header to make it look better though there is only one column
 | ||||||
|     player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); |     player_list->insertColumns(0, 1); | ||||||
|     player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game")); |     player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); | ||||||
| 
 | 
 | ||||||
|     ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); |     ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); | ||||||
| 
 | 
 | ||||||
|     // register the network structs to use in slots and signals
 |     // register the network structs to use in slots and signals
 | ||||||
|     qRegisterMetaType<Network::ChatEntry>(); |     qRegisterMetaType<Network::ChatEntry>(); | ||||||
|  |     qRegisterMetaType<Network::StatusMessageEntry>(); | ||||||
|     qRegisterMetaType<Network::RoomInformation>(); |     qRegisterMetaType<Network::RoomInformation>(); | ||||||
|     qRegisterMetaType<Network::RoomMember::State>(); |     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()) { |     if (auto member = Network::GetRoomMember().lock()) { | ||||||
|         member->BindOnChatMessageRecieved( |         member->BindOnChatMessageRecieved( | ||||||
|             [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); |             [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::ChatReceived, this, &ChatRoom::OnChatReceive); | ||||||
|  |         connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); | ||||||
|     } else { |     } else { | ||||||
|         // TODO (jroweboy) network was not initialized?
 |         // 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; | ChatRoom::~ChatRoom() = default; | ||||||
| 
 | 
 | ||||||
|  | void ChatRoom::SetModPerms(bool is_mod) { | ||||||
|  |     has_mod_perms = is_mod; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void ChatRoom::RetranslateUi() { | void ChatRoom::RetranslateUi() { | ||||||
|     ui->retranslateUi(this); |     ui->retranslateUi(this); | ||||||
| } | } | ||||||
|  | @ -127,6 +210,21 @@ void ChatRoom::AppendChatMessage(const QString& msg) { | ||||||
|     ui->chat_history->append(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) { | bool ChatRoom::ValidateMessage(const std::string& msg) { | ||||||
|     return !msg.empty(); |     return !msg.empty(); | ||||||
| } | } | ||||||
|  | @ -157,7 +255,8 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { | ||||||
|         auto members = room->GetMemberInformation(); |         auto members = room->GetMemberInformation(); | ||||||
|         auto it = std::find_if(members.begin(), members.end(), |         auto it = std::find_if(members.begin(), members.end(), | ||||||
|                                [&chat](const Network::RoomMember::MemberInformation& member) { |                                [&chat](const Network::RoomMember::MemberInformation& member) { | ||||||
|                                    return member.nickname == chat.nickname; |                                    return member.nickname == chat.nickname && | ||||||
|  |                                           member.username == chat.username; | ||||||
|                                }); |                                }); | ||||||
|         if (it == members.end()) { |         if (it == members.end()) { | ||||||
|             LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); |             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); |         auto player = std::distance(members.begin(), it); | ||||||
|         ChatMessage m(chat); |         ChatMessage m(chat); | ||||||
|  |         if (m.ContainsPing()) { | ||||||
|  |             emit UserPinged(); | ||||||
|  |         } | ||||||
|         AppendChatMessage(m.GetPlayerChatMessage(player)); |         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() { | void ChatRoom::OnSendChat() { | ||||||
|     if (auto room = Network::GetRoomMember().lock()) { |     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; |             return; | ||||||
|         } |         } | ||||||
|         auto message = ui->chat_message->text().toStdString(); |         auto message = ui->chat_message->text().toStdString(); | ||||||
|  | @ -184,12 +318,14 @@ void ChatRoom::OnSendChat() { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         auto nick = room->GetNickname(); |         auto nick = room->GetNickname(); | ||||||
|         Network::ChatEntry chat{nick, message}; |         auto username = room->GetUsername(); | ||||||
|  |         Network::ChatEntry chat{nick, username, message}; | ||||||
| 
 | 
 | ||||||
|         auto members = room->GetMemberInformation(); |         auto members = room->GetMemberInformation(); | ||||||
|         auto it = std::find_if(members.begin(), members.end(), |         auto it = std::find_if(members.begin(), members.end(), | ||||||
|                                [&chat](const Network::RoomMember::MemberInformation& member) { |                                [&chat](const Network::RoomMember::MemberInformation& member) { | ||||||
|                                    return member.nickname == chat.nickname; |                                    return member.nickname == chat.nickname && | ||||||
|  |                                           member.username == chat.username; | ||||||
|                                }); |                                }); | ||||||
|         if (it == members.end()) { |         if (it == members.end()) { | ||||||
|             LOG_INFO(Network, "Cannot find self in the player list when sending a message."); |             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) { | void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) { | ||||||
|     // TODO(B3N30): Remember which row is selected
 |     // TODO(B3N30): Remember which row is selected
 | ||||||
|     player_list->removeRows(0, player_list->rowCount()); |     player_list->removeRows(0, player_list->rowCount()); | ||||||
|     for (const auto& member : member_list) { |     for (const auto& member : member_list) { | ||||||
|         if (member.nickname.empty()) |         if (member.nickname.empty()) | ||||||
|             continue; |             continue; | ||||||
|         QList<QStandardItem*> l; |         QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, | ||||||
|         std::vector<std::string> elements = {member.nickname, member.game_info.name}; |                                                       member.avatar_url, member.game_info.name); | ||||||
|         for (const auto& item : elements) { | 
 | ||||||
|             QStandardItem* child = new QStandardItem(QString::fromStdString(item)); |         if (!icon_cache.count(member.avatar_url)) { | ||||||
|             child->setEditable(false); |             // Emplace a default question mark icon as avatar
 | ||||||
|             l.append(child); |             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
 |     // TODO(B3N30): Restore row selection
 | ||||||
| } | } | ||||||
|  | @ -230,14 +410,26 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { | ||||||
|     if (!item.isValid()) |     if (!item.isValid()) | ||||||
|         return; |         return; | ||||||
| 
 | 
 | ||||||
|     std::string nickname = player_list->item(item.row())->text().toStdString(); |     std::string nickname = | ||||||
|     if (auto room = Network::GetRoomMember().lock()) { |         player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); | ||||||
|         // You can't block yourself
 |  | ||||||
|         if (nickname == room->GetNickname()) |  | ||||||
|             return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     QMenu context_menu; |     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")); |         QAction* block_action = context_menu.addAction(tr("Block Player")); | ||||||
| 
 | 
 | ||||||
|         block_action->setCheckable(true); |         block_action->setCheckable(true); | ||||||
|  | @ -257,6 +449,34 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { | ||||||
|                     block_list.emplace(nickname); |                     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)); |     context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,9 +36,12 @@ public: | ||||||
|     void AppendStatusMessage(const QString& msg); |     void AppendStatusMessage(const QString& msg); | ||||||
|     ~ChatRoom(); |     ~ChatRoom(); | ||||||
| 
 | 
 | ||||||
|  |     void SetModPerms(bool is_mod); | ||||||
|  | 
 | ||||||
| public slots: | public slots: | ||||||
|     void OnRoomUpdate(const Network::RoomInformation& info); |     void OnRoomUpdate(const Network::RoomInformation& info); | ||||||
|     void OnChatReceive(const Network::ChatEntry&); |     void OnChatReceive(const Network::ChatEntry&); | ||||||
|  |     void OnStatusMessageReceive(const Network::StatusMessageEntry&); | ||||||
|     void OnSendChat(); |     void OnSendChat(); | ||||||
|     void OnChatTextChanged(); |     void OnChatTextChanged(); | ||||||
|     void PopupContextMenu(const QPoint& menu_location); |     void PopupContextMenu(const QPoint& menu_location); | ||||||
|  | @ -47,16 +50,25 @@ public slots: | ||||||
| 
 | 
 | ||||||
| signals: | signals: | ||||||
|     void ChatReceived(const Network::ChatEntry&); |     void ChatReceived(const Network::ChatEntry&); | ||||||
|  |     void StatusMessageReceived(const Network::StatusMessageEntry&); | ||||||
|  |     void UserPinged(); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     static constexpr u32 max_chat_lines = 1000; |     static constexpr u32 max_chat_lines = 1000; | ||||||
|     void AppendChatMessage(const QString&); |     void AppendChatMessage(const QString&); | ||||||
|     bool ValidateMessage(const std::string&); |     bool ValidateMessage(const std::string&); | ||||||
|  |     void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); | ||||||
|  |     void UpdateIconDisplay(); | ||||||
|  | 
 | ||||||
|  |     bool has_mod_perms = false; | ||||||
|     QStandardItemModel* player_list; |     QStandardItemModel* player_list; | ||||||
|     std::unique_ptr<Ui::ChatRoom> ui; |     std::unique_ptr<Ui::ChatRoom> ui; | ||||||
|     std::unordered_set<std::string> block_list; |     std::unordered_set<std::string> block_list; | ||||||
|  |     std::unordered_map<std::string, QPixmap> icon_cache; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Q_DECLARE_METATYPE(Network::ChatEntry); | Q_DECLARE_METATYPE(Network::ChatEntry); | ||||||
|  | Q_DECLARE_METATYPE(Network::StatusMessageEntry); | ||||||
| Q_DECLARE_METATYPE(Network::RoomInformation); | Q_DECLARE_METATYPE(Network::RoomInformation); | ||||||
| Q_DECLARE_METATYPE(Network::RoomMember::State); | Q_DECLARE_METATYPE(Network::RoomMember::State); | ||||||
|  | Q_DECLARE_METATYPE(Network::RoomMember::Error); | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
|    <rect> |    <rect> | ||||||
|     <x>0</x> |     <x>0</x> | ||||||
|     <y>0</y> |     <y>0</y> | ||||||
|     <width>607</width> |     <width>807</width> | ||||||
|     <height>432</height> |     <height>432</height> | ||||||
|    </rect> |    </rect> | ||||||
|   </property> |   </property> | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
| #include "citra_qt/game_list_p.h" | #include "citra_qt/game_list_p.h" | ||||||
| #include "citra_qt/multiplayer/client_room.h" | #include "citra_qt/multiplayer/client_room.h" | ||||||
| #include "citra_qt/multiplayer/message.h" | #include "citra_qt/multiplayer/message.h" | ||||||
|  | #include "citra_qt/multiplayer/moderation_dialog.h" | ||||||
| #include "citra_qt/multiplayer/state.h" | #include "citra_qt/multiplayer/state.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "core/announce_multiplayer_session.h" | #include "core/announce_multiplayer_session.h" | ||||||
|  | @ -33,6 +34,8 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) | ||||||
|         connect(this, &ClientRoomWindow::RoomInformationChanged, this, |         connect(this, &ClientRoomWindow::RoomInformationChanged, this, | ||||||
|                 &ClientRoomWindow::OnRoomUpdate); |                 &ClientRoomWindow::OnRoomUpdate); | ||||||
|         connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); |         connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); | ||||||
|  |         // Update the state
 | ||||||
|  |         OnStateChange(member->GetState()); | ||||||
|     } else { |     } else { | ||||||
|         // TODO (jroweboy) network was not initialized?
 |         // TODO (jroweboy) network was not initialized?
 | ||||||
|     } |     } | ||||||
|  | @ -40,11 +43,25 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) | ||||||
|     connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); }); |     connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); }); | ||||||
|     ui->disconnect->setDefault(false); |     ui->disconnect->setDefault(false); | ||||||
|     ui->disconnect->setAutoDefault(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(); |     UpdateView(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ClientRoomWindow::~ClientRoomWindow() = default; | 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() { | void ClientRoomWindow::RetranslateUi() { | ||||||
|     ui->retranslateUi(this); |     ui->retranslateUi(this); | ||||||
|     ui->chat->RetranslateUi(); |     ui->chat->RetranslateUi(); | ||||||
|  | @ -55,9 +72,12 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { | 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->Clear(); | ||||||
|         ui->chat->AppendStatusMessage(tr("Connected")); |         ui->chat->AppendStatusMessage(tr("Connected")); | ||||||
|  |         SetModPerms(state == Network::RoomMember::State::Moderator); | ||||||
|     } |     } | ||||||
|     UpdateView(); |     UpdateView(); | ||||||
| } | } | ||||||
|  | @ -82,6 +102,7 @@ void ClientRoomWindow::UpdateView() { | ||||||
|                                .arg(QString::fromStdString(information.name)) |                                .arg(QString::fromStdString(information.name)) | ||||||
|                                .arg(memberlist.size()) |                                .arg(memberlist.size()) | ||||||
|                                .arg(information.member_slots)); |                                .arg(information.member_slots)); | ||||||
|  |             ui->description->setText(QString::fromStdString(information.description)); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -26,10 +26,12 @@ public slots: | ||||||
| signals: | signals: | ||||||
|     void RoomInformationChanged(const Network::RoomInformation&); |     void RoomInformationChanged(const Network::RoomInformation&); | ||||||
|     void StateChanged(const Network::RoomMember::State&); |     void StateChanged(const Network::RoomMember::State&); | ||||||
|  |     void ShowNotification(); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     void Disconnect(); |     void Disconnect(); | ||||||
|     void UpdateView(); |     void UpdateView(); | ||||||
|  |     void SetModPerms(bool is_mod); | ||||||
| 
 | 
 | ||||||
|     QStandardItemModel* player_list; |     QStandardItemModel* player_list; | ||||||
|     std::unique_ptr<Ui::ClientRoom> ui; |     std::unique_ptr<Ui::ClientRoom> ui; | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
|    <rect> |    <rect> | ||||||
|     <x>0</x> |     <x>0</x> | ||||||
|     <y>0</y> |     <y>0</y> | ||||||
|     <width>607</width> |     <width>807</width> | ||||||
|     <height>432</height> |     <height>432</height> | ||||||
|    </rect> |    </rect> | ||||||
|   </property> |   </property> | ||||||
|  | @ -21,6 +21,13 @@ | ||||||
|        <property name="rightMargin"> |        <property name="rightMargin"> | ||||||
|         <number>0</number> |         <number>0</number> | ||||||
|        </property> |        </property> | ||||||
|  |        <item> | ||||||
|  |         <widget class="QLabel" name="description"> | ||||||
|  |          <property name="text"> | ||||||
|  |           <string>Room Description</string> | ||||||
|  |          </property> | ||||||
|  |         </widget> | ||||||
|  |        </item> | ||||||
|        <item> |        <item> | ||||||
|         <spacer name="horizontalSpacer"> |         <spacer name="horizontalSpacer"> | ||||||
|          <property name="orientation"> |          <property name="orientation"> | ||||||
|  | @ -34,6 +41,16 @@ | ||||||
|          </property> |          </property> | ||||||
|         </spacer> |         </spacer> | ||||||
|        </item> |        </item> | ||||||
|  |        <item> | ||||||
|  |         <widget class="QPushButton" name="moderation"> | ||||||
|  |          <property name="text"> | ||||||
|  |           <string>Moderation...</string> | ||||||
|  |          </property> | ||||||
|  |          <property name="visible"> | ||||||
|  |           <bool>false</bool> | ||||||
|  |          </property> | ||||||
|  |         </widget> | ||||||
|  |        </item> | ||||||
|        <item> |        <item> | ||||||
|         <widget class="QPushButton" name="disconnect"> |         <widget class="QPushButton" name="disconnect"> | ||||||
|          <property name="text"> |          <property name="text"> | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| #include "citra_qt/multiplayer/state.h" | #include "citra_qt/multiplayer/state.h" | ||||||
| #include "citra_qt/multiplayer/validation.h" | #include "citra_qt/multiplayer/validation.h" | ||||||
| #include "citra_qt/ui_settings.h" | #include "citra_qt/ui_settings.h" | ||||||
|  | #include "core/hle/service/cfg/cfg.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
| #include "network/network.h" | #include "network/network.h" | ||||||
| #include "ui_direct_connect.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.
 |         // Prevent the user from trying to join a room while they are already joining.
 | ||||||
|         if (member->GetState() == Network::RoomMember::State::Joining) { |         if (member->GetState() == Network::RoomMember::State::Joining) { | ||||||
|             return; |             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.
 |             // And ask if they want to leave the room if they are already in one.
 | ||||||
|             if (!NetworkMessage::WarnDisconnect()) { |             if (!NetworkMessage::WarnDisconnect()) { | ||||||
|                 return; |                 return; | ||||||
|  | @ -97,6 +98,7 @@ void DirectConnectWindow::Connect() { | ||||||
|         if (auto room_member = Network::GetRoomMember().lock()) { |         if (auto room_member = Network::GetRoomMember().lock()) { | ||||||
|             auto port = UISettings::values.port.toUInt(); |             auto port = UISettings::values.port.toUInt(); | ||||||
|             room_member->Join(ui->nickname->text().toStdString(), |             room_member->Join(ui->nickname->text().toStdString(), | ||||||
|  |                               Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), | ||||||
|                               ui->ip->text().toStdString().c_str(), port, 0, |                               ui->ip->text().toStdString().c_str(), port, 0, | ||||||
|                               Network::NoPreferredMac, ui->password->text().toStdString().c_str()); |                               Network::NoPreferredMac, ui->password->text().toStdString().c_str()); | ||||||
|         } |         } | ||||||
|  | @ -120,7 +122,9 @@ void DirectConnectWindow::OnConnection() { | ||||||
|     EndConnecting(); |     EndConnecting(); | ||||||
| 
 | 
 | ||||||
|     if (auto room_member = Network::GetRoomMember().lock()) { |     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(); |             close(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -19,8 +19,12 @@ | ||||||
| #include "citra_qt/ui_settings.h" | #include "citra_qt/ui_settings.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "core/announce_multiplayer_session.h" | #include "core/announce_multiplayer_session.h" | ||||||
|  | #include "core/hle/service/cfg/cfg.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
| #include "ui_host_room.h" | #include "ui_host_room.h" | ||||||
|  | #ifdef ENABLE_WEB_SERVICE | ||||||
|  | #include "web_service/verify_user_jwt.h" | ||||||
|  | #endif | ||||||
| 
 | 
 | ||||||
| HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | ||||||
|                                std::shared_ptr<Core::AnnounceMultiplayerSession> session) |                                std::shared_ptr<Core::AnnounceMultiplayerSession> session) | ||||||
|  | @ -69,6 +73,7 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | ||||||
|     if (index != -1) { |     if (index != -1) { | ||||||
|         ui->game_list->setCurrentIndex(index); |         ui->game_list->setCurrentIndex(index); | ||||||
|     } |     } | ||||||
|  |     ui->room_description->setText(UISettings::values.room_description); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| HostRoomWindow::~HostRoomWindow() = default; | HostRoomWindow::~HostRoomWindow() = default; | ||||||
|  | @ -77,6 +82,21 @@ void HostRoomWindow::RetranslateUi() { | ||||||
|     ui->retranslateUi(this); |     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() { | void HostRoomWindow::Host() { | ||||||
|     if (!ui->username->hasAcceptableInput()) { |     if (!ui->username->hasAcceptableInput()) { | ||||||
|         NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); |         NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); | ||||||
|  | @ -93,7 +113,7 @@ void HostRoomWindow::Host() { | ||||||
|     if (auto member = Network::GetRoomMember().lock()) { |     if (auto member = Network::GetRoomMember().lock()) { | ||||||
|         if (member->GetState() == Network::RoomMember::State::Joining) { |         if (member->GetState() == Network::RoomMember::State::Joining) { | ||||||
|             return; |             return; | ||||||
|         } else if (member->GetState() == Network::RoomMember::State::Joined) { |         } else if (member->IsConnected()) { | ||||||
|             auto parent = static_cast<MultiplayerState*>(parentWidget()); |             auto parent = static_cast<MultiplayerState*>(parentWidget()); | ||||||
|             if (!parent->OnCloseRoom()) { |             if (!parent->OnCloseRoom()) { | ||||||
|                 close(); |                 close(); | ||||||
|  | @ -106,9 +126,17 @@ void HostRoomWindow::Host() { | ||||||
|         auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); |         auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); | ||||||
|         auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; |         auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; | ||||||
|         auto password = ui->password->text().toStdString(); |         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()) { |         if (auto room = Network::GetRoom().lock()) { | ||||||
|             bool created = room->Create(ui->room_name->text().toStdString(), "", port, password, |             bool created = room->Create(ui->room_name->text().toStdString(), | ||||||
|                                         ui->max_player->value(), game_name.toStdString(), game_id); |                                         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) { |             if (!created) { | ||||||
|                 NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); |                 NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); | ||||||
|                 LOG_ERROR(Network, "Could not create room!"); |                 LOG_ERROR(Network, "Could not create room!"); | ||||||
|  | @ -116,8 +144,34 @@ void HostRoomWindow::Host() { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, |         // Start the announce session if they chose Public
 | ||||||
|                      Network::NoPreferredMac, password); |         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
 |         // Store settings
 | ||||||
|         UISettings::values.room_nickname = ui->username->text(); |         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()) |         UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) | ||||||
|                                            ? ui->port->text() |                                            ? ui->port->text() | ||||||
|                                            : QString::number(Network::DefaultRoomPort); |                                            : QString::number(Network::DefaultRoomPort); | ||||||
|  |         UISettings::values.room_description = ui->room_description->toPlainText(); | ||||||
|         Settings::Apply(); |         Settings::Apply(); | ||||||
|         OnConnection(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void HostRoomWindow::OnConnection() { |  | ||||||
|         ui->host->setEnabled(true); |         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(); |         close(); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { | QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { | ||||||
|  |  | ||||||
|  | @ -26,6 +26,10 @@ class ComboBoxProxyModel; | ||||||
| 
 | 
 | ||||||
| class ChatMessage; | class ChatMessage; | ||||||
| 
 | 
 | ||||||
|  | namespace Network::VerifyUser { | ||||||
|  | class Backend; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| class HostRoomWindow : public QDialog { | class HostRoomWindow : public QDialog { | ||||||
|     Q_OBJECT |     Q_OBJECT | ||||||
| 
 | 
 | ||||||
|  | @ -36,15 +40,9 @@ public: | ||||||
| 
 | 
 | ||||||
|     void RetranslateUi(); |     void RetranslateUi(); | ||||||
| 
 | 
 | ||||||
| private slots: |  | ||||||
|     /**
 |  | ||||||
|      * Handler for connection status changes. Launches the chat window if successful or |  | ||||||
|      * displays an error |  | ||||||
|      */ |  | ||||||
|     void OnConnection(); |  | ||||||
| 
 |  | ||||||
| private: | private: | ||||||
|     void Host(); |     void Host(); | ||||||
|  |     std::unique_ptr<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const; | ||||||
| 
 | 
 | ||||||
|     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; |     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||||
|     QStandardItemModel* game_list; |     QStandardItemModel* game_list; | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
|     <x>0</x> |     <x>0</x> | ||||||
|     <y>0</y> |     <y>0</y> | ||||||
|     <width>607</width> |     <width>607</width> | ||||||
|     <height>165</height> |     <height>211</height> | ||||||
|    </rect> |    </rect> | ||||||
|   </property> |   </property> | ||||||
|   <property name="windowTitle"> |   <property name="windowTitle"> | ||||||
|  | @ -131,6 +131,34 @@ | ||||||
|      </layout> |      </layout> | ||||||
|     </widget> |     </widget> | ||||||
|    </item> |    </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> |    <item> | ||||||
|     <layout class="QHBoxLayout" name="horizontalLayout"> |     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|      <property name="rightMargin"> |      <property name="rightMargin"> | ||||||
|  |  | ||||||
|  | @ -15,8 +15,12 @@ | ||||||
| #include "citra_qt/multiplayer/validation.h" | #include "citra_qt/multiplayer/validation.h" | ||||||
| #include "citra_qt/ui_settings.h" | #include "citra_qt/ui_settings.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | #include "core/hle/service/cfg/cfg.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
| #include "network/network.h" | #include "network/network.h" | ||||||
|  | #ifdef ENABLE_WEB_SERVICE | ||||||
|  | #include "web_service/web_backend.h" | ||||||
|  | #endif | ||||||
| 
 | 
 | ||||||
| Lobby::Lobby(QWidget* parent, QStandardItemModel* list, | Lobby::Lobby(QWidget* parent, QStandardItemModel* list, | ||||||
|              std::shared_ptr<Core::AnnounceMultiplayerSession> session) |              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.
 |         // Prevent the user from trying to join a room while they are already joining.
 | ||||||
|         if (member->GetState() == Network::RoomMember::State::Joining) { |         if (member->GetState() == Network::RoomMember::State::Joining) { | ||||||
|             return; |             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.
 |             // And ask if they want to leave the room if they are already in one.
 | ||||||
|             if (!NetworkMessage::WarnDisconnect()) { |             if (!NetworkMessage::WarnDisconnect()) { | ||||||
|                 return; |                 return; | ||||||
|  | @ -135,11 +139,27 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { | ||||||
|     const std::string ip = |     const std::string ip = | ||||||
|         proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); |         proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); | ||||||
|     int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); |     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
 |     // 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()) { |         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); |     watcher->setFuture(f); | ||||||
|  | @ -191,7 +211,8 @@ void Lobby::OnRefreshLobby() { | ||||||
|         QList<QVariant> members; |         QList<QVariant> members; | ||||||
|         for (auto member : room.members) { |         for (auto member : room.members) { | ||||||
|             QVariant var; |             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)}); |                                      QString::fromStdString(member.game_name)}); | ||||||
|             members.append(var); |             members.append(var); | ||||||
|         } |         } | ||||||
|  | @ -203,14 +224,18 @@ void Lobby::OnRefreshLobby() { | ||||||
|             new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), |             new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), | ||||||
|                               smdh_icon), |                               smdh_icon), | ||||||
|             new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip), |             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), |             new LobbyItemMemberList(members, room.max_player), | ||||||
|         }); |         }); | ||||||
|         model->appendRow(row); |         model->appendRow(row); | ||||||
|         // To make the rows expandable, add the member data as a child of the first column of the
 |         // 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
 |         // rows with people in them and have qt set them to colspan after the model is finished
 | ||||||
|         // resetting
 |         // 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)); |             first_item->appendRow(new LobbyItemExpandedMemberList(members)); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -226,8 +251,8 @@ void Lobby::OnRefreshLobby() { | ||||||
|     // Set the member list child items to span all columns
 |     // Set the member list child items to span all columns
 | ||||||
|     for (int i = 0; i < proxy->rowCount(); i++) { |     for (int i = 0; i < proxy->rowCount(); i++) { | ||||||
|         auto parent = model->item(i, 0); |         auto parent = model->item(i, 0); | ||||||
|         if (parent->hasChildren()) { |         for (int j = 0; j < parent->rowCount(); j++) { | ||||||
|             ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true); |             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 { | class LobbyItemGame : public LobbyItem { | ||||||
| public: | public: | ||||||
|     static const int TitleIDRole = Qt::UserRole + 1; |     static const int TitleIDRole = Qt::UserRole + 1; | ||||||
|  | @ -95,12 +120,14 @@ public: | ||||||
|     static const int HostUsernameRole = Qt::UserRole + 1; |     static const int HostUsernameRole = Qt::UserRole + 1; | ||||||
|     static const int HostIPRole = Qt::UserRole + 2; |     static const int HostIPRole = Qt::UserRole + 2; | ||||||
|     static const int HostPortRole = Qt::UserRole + 3; |     static const int HostPortRole = Qt::UserRole + 3; | ||||||
|  |     static const int HostVerifyUIDRole = Qt::UserRole + 4; | ||||||
| 
 | 
 | ||||||
|     LobbyItemHost() = default; |     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(username, HostUsernameRole); | ||||||
|         setData(ip, HostIPRole); |         setData(ip, HostIPRole); | ||||||
|         setData(port, HostPortRole); |         setData(port, HostPortRole); | ||||||
|  |         setData(verify_UID, HostVerifyUIDRole); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     QVariant data(int role) const override { |     QVariant data(int role) const override { | ||||||
|  | @ -121,12 +148,17 @@ class LobbyMember { | ||||||
| public: | public: | ||||||
|     LobbyMember() = default; |     LobbyMember() = default; | ||||||
|     LobbyMember(const LobbyMember& other) = default; |     LobbyMember(const LobbyMember& other) = default; | ||||||
|     explicit LobbyMember(QString username, u64 title_id, QString game_name) |     explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) | ||||||
|         : username(std::move(username)), title_id(title_id), game_name(std::move(game_name)) {} |         : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), | ||||||
|  |           game_name(std::move(game_name)) {} | ||||||
|     ~LobbyMember() = default; |     ~LobbyMember() = default; | ||||||
| 
 | 
 | ||||||
|     QString GetUsername() const { |     QString GetName() const { | ||||||
|         return username; |         if (username.isEmpty() || username == nickname) { | ||||||
|  |             return nickname; | ||||||
|  |         } else { | ||||||
|  |             return QString("%1 (%2)").arg(nickname, username); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     u64 GetTitleId() const { |     u64 GetTitleId() const { | ||||||
|         return title_id; |         return title_id; | ||||||
|  | @ -137,6 +169,7 @@ public: | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     QString username; |     QString username; | ||||||
|  |     QString nickname; | ||||||
|     u64 title_id; |     u64 title_id; | ||||||
|     QString game_name; |     QString game_name; | ||||||
| }; | }; | ||||||
|  | @ -195,10 +228,9 @@ public: | ||||||
|                 out += '\n'; |                 out += '\n'; | ||||||
|             const auto& m = member.value<LobbyMember>(); |             const auto& m = member.value<LobbyMember>(); | ||||||
|             if (m.GetGameName().isEmpty()) { |             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 { |             } else { | ||||||
|                 out += |                 out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); | ||||||
|                     QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName()); |  | ||||||
|             } |             } | ||||||
|             first = false; |             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.")); |     QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); | ||||||
| const ConnectionError ROOMNAME_NOT_VALID( | const ConnectionError ROOMNAME_NOT_VALID( | ||||||
|     QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); |     QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); | ||||||
| const ConnectionError USERNAME_IN_USE( | const ConnectionError USERNAME_NOT_VALID_SERVER( | ||||||
|     QT_TR_NOOP("Username is already in use. Please choose another.")); |     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 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 PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535.")); | ||||||
| const ConnectionError NO_INTERNET( | 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 " |     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 " |                "you still cannot connect, contact the room host and verify that the host is " | ||||||
|                "properly configured with the external port forwarded.")); |                "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( | const ConnectionError COULD_NOT_CREATE_ROOM( | ||||||
|     QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); |     QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); | ||||||
| const ConnectionError HOST_BANNED( | const ConnectionError HOST_BANNED( | ||||||
|  | @ -34,8 +36,16 @@ const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); | ||||||
| const ConnectionError GENERIC_ERROR( | const ConnectionError GENERIC_ERROR( | ||||||
|     QT_TR_NOOP("An unknown error occured. If this error continues to occur, please open an issue")); |     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 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( | const ConnectionError MAC_COLLISION( | ||||||
|     QT_TR_NOOP("MAC address is already in use. Please choose another.")); |     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) { | static bool WarnMessage(const std::string& title, const std::string& text) { | ||||||
|     return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), |     return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), | ||||||
|  |  | ||||||
|  | @ -20,20 +20,27 @@ private: | ||||||
|     std::string err; |     std::string err; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /// When the nickname is considered invalid by the client
 | ||||||
| extern const ConnectionError USERNAME_NOT_VALID; | extern const ConnectionError USERNAME_NOT_VALID; | ||||||
| extern const ConnectionError ROOMNAME_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 IP_ADDRESS_NOT_VALID; | ||||||
| extern const ConnectionError PORT_NOT_VALID; | extern const ConnectionError PORT_NOT_VALID; | ||||||
| extern const ConnectionError NO_INTERNET; | extern const ConnectionError NO_INTERNET; | ||||||
| extern const ConnectionError UNABLE_TO_CONNECT; | extern const ConnectionError UNABLE_TO_CONNECT; | ||||||
|  | extern const ConnectionError ROOM_IS_FULL; | ||||||
| extern const ConnectionError COULD_NOT_CREATE_ROOM; | extern const ConnectionError COULD_NOT_CREATE_ROOM; | ||||||
| extern const ConnectionError HOST_BANNED; | extern const ConnectionError HOST_BANNED; | ||||||
| extern const ConnectionError WRONG_VERSION; | extern const ConnectionError WRONG_VERSION; | ||||||
| extern const ConnectionError WRONG_PASSWORD; | extern const ConnectionError WRONG_PASSWORD; | ||||||
| extern const ConnectionError GENERIC_ERROR; | extern const ConnectionError GENERIC_ERROR; | ||||||
| extern const ConnectionError LOST_CONNECTION; | extern const ConnectionError LOST_CONNECTION; | ||||||
|  | extern const ConnectionError HOST_KICKED; | ||||||
| extern const ConnectionError MAC_COLLISION; | 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 |  *  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.
 | // Refer to the license.txt file included.
 | ||||||
| 
 | 
 | ||||||
| #include <QAction> | #include <QAction> | ||||||
|  | #include <QApplication> | ||||||
| #include <QIcon> | #include <QIcon> | ||||||
| #include <QMessageBox> | #include <QMessageBox> | ||||||
| #include <QStandardItemModel> | #include <QStandardItemModel> | ||||||
|  | @ -13,6 +14,7 @@ | ||||||
| #include "citra_qt/multiplayer/lobby.h" | #include "citra_qt/multiplayer/lobby.h" | ||||||
| #include "citra_qt/multiplayer/message.h" | #include "citra_qt/multiplayer/message.h" | ||||||
| #include "citra_qt/multiplayer/state.h" | #include "citra_qt/multiplayer/state.h" | ||||||
|  | #include "citra_qt/ui_settings.h" | ||||||
| #include "citra_qt/util/clickable_label.h" | #include "citra_qt/util/clickable_label.h" | ||||||
| #include "common/announce_multiplayer_room.h" | #include "common/announce_multiplayer_room.h" | ||||||
| #include "common/logging/log.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); }); |             [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); | ||||||
|         connect(this, &MultiplayerState::NetworkStateChanged, this, |         connect(this, &MultiplayerState::NetworkStateChanged, this, | ||||||
|                 &MultiplayerState::OnNetworkStateChanged); |                 &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::State>(); | ||||||
|  |     qRegisterMetaType<Network::RoomMember::Error>(); | ||||||
|     qRegisterMetaType<Common::WebResult>(); |     qRegisterMetaType<Common::WebResult>(); | ||||||
|     announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(); |     announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(); | ||||||
|     announce_multiplayer_session->BindErrorCallback( |     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_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); | ||||||
|     connect(status_icon, &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() { | MultiplayerState::~MultiplayerState() { | ||||||
|  | @ -52,6 +65,12 @@ MultiplayerState::~MultiplayerState() { | ||||||
|             member->Unbind(state_callback_handle); |             member->Unbind(state_callback_handle); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     if (error_callback_handle) { | ||||||
|  |         if (auto member = Network::GetRoomMember().lock()) { | ||||||
|  |             member->Unbind(error_callback_handle); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void MultiplayerState::Close() { | void MultiplayerState::Close() { | ||||||
|  | @ -70,7 +89,9 @@ void MultiplayerState::retranslateUi() { | ||||||
| 
 | 
 | ||||||
|     if (current_state == Network::RoomMember::State::Uninitialized) { |     if (current_state == Network::RoomMember::State::Uninitialized) { | ||||||
|         status_text->setText(tr("Not Connected. Click here to find a room!")); |         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")); |         status_text->setText(tr("Connected")); | ||||||
|     } else { |     } else { | ||||||
|         status_text->setText(tr("Not Connected")); |         status_text->setText(tr("Not Connected")); | ||||||
|  | @ -88,35 +109,10 @@ void MultiplayerState::retranslateUi() { | ||||||
| 
 | 
 | ||||||
| void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { | void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { | ||||||
|     LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); |     LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); | ||||||
|     bool is_connected = false; |     if (state == Network::RoomMember::State::Joined || | ||||||
|     switch (state) { |         state == Network::RoomMember::State::Moderator) { | ||||||
|     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; |  | ||||||
|         OnOpenNetworkRoom(); |         OnOpenNetworkRoom(); | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|     if (is_connected) { |  | ||||||
|         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); |         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||||
|         status_text->setText(tr("Connected")); |         status_text->setText(tr("Connected")); | ||||||
|         leave_room->setEnabled(true); |         leave_room->setEnabled(true); | ||||||
|  | @ -131,6 +127,51 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s | ||||||
|     current_state = state; |     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) { | void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { | ||||||
|     announce_multiplayer_session->Stop(); |     announce_multiplayer_session->Stop(); | ||||||
|     QMessageBox::warning( |     QMessageBox::warning( | ||||||
|  | @ -144,7 +185,11 @@ void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void MultiplayerState::UpdateThemedIcons() { | 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)); |         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||||
|     } else { |     } else { | ||||||
|         status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); |         status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); | ||||||
|  | @ -185,6 +230,10 @@ bool MultiplayerState::OnCloseRoom() { | ||||||
|         if (room->GetState() != Network::Room::State::Open) { |         if (room->GetState() != Network::Room::State::Open) { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |         // Save ban list
 | ||||||
|  |         if (auto room = Network::GetRoom().lock()) { | ||||||
|  |             UISettings::values.ban_list = std::move(room->GetBanList()); | ||||||
|  |         } | ||||||
|         room->Destroy(); |         room->Destroy(); | ||||||
|         announce_multiplayer_session->Stop(); |         announce_multiplayer_session->Stop(); | ||||||
|         LOG_DEBUG(Frontend, "Closed the room (as a server)"); |         LOG_DEBUG(Frontend, "Closed the room (as a server)"); | ||||||
|  | @ -192,11 +241,28 @@ bool MultiplayerState::OnCloseRoom() { | ||||||
|     return true; |     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() { | void MultiplayerState::OnOpenNetworkRoom() { | ||||||
|     if (auto member = Network::GetRoomMember().lock()) { |     if (auto member = Network::GetRoomMember().lock()) { | ||||||
|         if (member->IsConnected()) { |         if (member->IsConnected()) { | ||||||
|             if (client_room == nullptr) { |             if (client_room == nullptr) { | ||||||
|                 client_room = new ClientRoomWindow(this); |                 client_room = new ClientRoomWindow(this); | ||||||
|  |                 connect(client_room, &ClientRoomWindow::ShowNotification, this, | ||||||
|  |                         &MultiplayerState::ShowNotification); | ||||||
|             } |             } | ||||||
|             BringWidgetToFront(client_room); |             BringWidgetToFront(client_room); | ||||||
|             return; |             return; | ||||||
|  |  | ||||||
|  | @ -40,6 +40,7 @@ public: | ||||||
| 
 | 
 | ||||||
| public slots: | public slots: | ||||||
|     void OnNetworkStateChanged(const Network::RoomMember::State& state); |     void OnNetworkStateChanged(const Network::RoomMember::State& state); | ||||||
|  |     void OnNetworkError(const Network::RoomMember::Error& error); | ||||||
|     void OnViewLobby(); |     void OnViewLobby(); | ||||||
|     void OnCreateRoom(); |     void OnCreateRoom(); | ||||||
|     bool OnCloseRoom(); |     bool OnCloseRoom(); | ||||||
|  | @ -47,9 +48,12 @@ public slots: | ||||||
|     void OnDirectConnectToRoom(); |     void OnDirectConnectToRoom(); | ||||||
|     void OnAnnounceFailed(const Common::WebResult&); |     void OnAnnounceFailed(const Common::WebResult&); | ||||||
|     void UpdateThemedIcons(); |     void UpdateThemedIcons(); | ||||||
|  |     void ShowNotification(); | ||||||
|  |     void HideNotification(); | ||||||
| 
 | 
 | ||||||
| signals: | signals: | ||||||
|     void NetworkStateChanged(const Network::RoomMember::State&); |     void NetworkStateChanged(const Network::RoomMember::State&); | ||||||
|  |     void NetworkError(const Network::RoomMember::Error&); | ||||||
|     void AnnounceFailed(const Common::WebResult&); |     void AnnounceFailed(const Common::WebResult&); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  | @ -64,7 +68,11 @@ private: | ||||||
|     QAction* show_room; |     QAction* show_room; | ||||||
|     std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; |     std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||||
|     Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; |     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::State> state_callback_handle; | ||||||
|  |     Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle; | ||||||
|  | 
 | ||||||
|  |     bool show_notification = false; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Q_DECLARE_METATYPE(Common::WebResult); | Q_DECLARE_METATYPE(Common::WebResult); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
| #include <array> | #include <array> | ||||||
|  | #include <string> | ||||||
|  | #include <utility> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <QByteArray> | #include <QByteArray> | ||||||
| #include <QMetaType> | #include <QMetaType> | ||||||
|  | @ -109,6 +111,8 @@ struct Values { | ||||||
|     QString room_port; |     QString room_port; | ||||||
|     uint host_type; |     uint host_type; | ||||||
|     qulonglong game_id; |     qulonglong game_id; | ||||||
|  |     QString room_description; | ||||||
|  |     std::pair<std::vector<std::string>, std::vector<std::string>> ban_list; | ||||||
| 
 | 
 | ||||||
|     // logging
 |     // logging
 | ||||||
|     bool show_console; |     bool show_console; | ||||||
|  |  | ||||||
|  | @ -17,13 +17,17 @@ using MacAddress = std::array<u8, 6>; | ||||||
| 
 | 
 | ||||||
| struct Room { | struct Room { | ||||||
|     struct Member { |     struct Member { | ||||||
|         std::string name; |         std::string username; | ||||||
|  |         std::string nickname; | ||||||
|  |         std::string avatar_url; | ||||||
|         MacAddress mac_address; |         MacAddress mac_address; | ||||||
|         std::string game_name; |         std::string game_name; | ||||||
|         u64 game_id; |         u64 game_id; | ||||||
|     }; |     }; | ||||||
|  |     std::string id; | ||||||
|  |     std::string verify_UID; ///< UID used for verification
 | ||||||
|     std::string name; |     std::string name; | ||||||
|     std::string UID; |     std::string description; | ||||||
|     std::string owner; |     std::string owner; | ||||||
|     std::string ip; |     std::string ip; | ||||||
|     u16 port; |     u16 port; | ||||||
|  | @ -49,14 +53,15 @@ public: | ||||||
|      * Sets the Information that gets used for the announce |      * Sets the Information that gets used for the announce | ||||||
|      * @param uid The Id of the room |      * @param uid The Id of the room | ||||||
|      * @param name The name of the room |      * @param name The name of the room | ||||||
|  |      * @param description The room description | ||||||
|      * @param port The port of the room |      * @param port The port of the room | ||||||
|      * @param net_version The version of the libNetwork that gets used |      * @param net_version The version of the libNetwork that gets used | ||||||
|      * @param has_password True if the room is passowrd protected |      * @param has_password True if the room is passowrd protected | ||||||
|      * @param preferred_game The preferred game of the room |      * @param preferred_game The preferred game of the room | ||||||
|      * @param preferred_game_id The title id of the preferred game |      * @param preferred_game_id The title id of the preferred game | ||||||
|      */ |      */ | ||||||
|     virtual void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, |     virtual void SetRoomInformation(const std::string& name, const std::string& description, | ||||||
|                                     const u32 max_player, const u32 net_version, |                                     const u16 port, const u32 max_player, const u32 net_version, | ||||||
|                                     const bool has_password, const std::string& preferred_game, |                                     const bool has_password, const std::string& preferred_game, | ||||||
|                                     const u64 preferred_game_id) = 0; |                                     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_id The title id of the game the player plays | ||||||
|      * @param game_name The name 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; |                            const u64 game_id, const std::string& game_name) = 0; | ||||||
| 
 | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Send the data to the announce service |      * Updates the data in the announce service. Re-register the room when required. | ||||||
|      * @result The result of the announce attempt |      * @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 |      * Empties the stored players | ||||||
|  | @ -99,15 +111,19 @@ public: | ||||||
| class NullBackend : public Backend { | class NullBackend : public Backend { | ||||||
| public: | public: | ||||||
|     ~NullBackend() = default; |     ~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 u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, | ||||||
|                             const bool /*has_password*/, const std::string& /*preferred_game*/, |                             const bool /*has_password*/, const std::string& /*preferred_game*/, | ||||||
|                             const u64 /*preferred_game_id*/) override {} |                             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 {} |                    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"}; |         return Common::WebResult{Common::WebResult::Code::NoWebservice, "WebService is missing"}; | ||||||
|     } |     } | ||||||
|  |     std::string Register() override { | ||||||
|  |         return ""; | ||||||
|  |     } | ||||||
|     void ClearPlayers() override {} |     void ClearPlayers() override {} | ||||||
|     RoomList GetRoomList() override { |     RoomList GetRoomList() override { | ||||||
|         return RoomList{}; |         return RoomList{}; | ||||||
|  |  | ||||||
|  | @ -29,6 +29,21 @@ AnnounceMultiplayerSession::AnnounceMultiplayerSession() { | ||||||
| #endif | #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() { | void AnnounceMultiplayerSession::Start() { | ||||||
|     if (announce_multiplayer_thread) { |     if (announce_multiplayer_thread) { | ||||||
|         Stop(); |         Stop(); | ||||||
|  | @ -44,6 +59,7 @@ void AnnounceMultiplayerSession::Stop() { | ||||||
|         announce_multiplayer_thread->join(); |         announce_multiplayer_thread->join(); | ||||||
|         announce_multiplayer_thread.reset(); |         announce_multiplayer_thread.reset(); | ||||||
|         backend->Delete(); |         backend->Delete(); | ||||||
|  |         registered = false; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -64,7 +80,24 @@ AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { | ||||||
|     Stop(); |     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() { | void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { | ||||||
|  |     if (!registered) { | ||||||
|  |         Register(); | ||||||
|  |     } | ||||||
|     auto update_time = std::chrono::steady_clock::now(); |     auto update_time = std::chrono::steady_clock::now(); | ||||||
|     std::future<Common::WebResult> future; |     std::future<Common::WebResult> future; | ||||||
|     while (!shutdown_event.WaitUntil(update_time)) { |     while (!shutdown_event.WaitUntil(update_time)) { | ||||||
|  | @ -76,24 +109,19 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { | ||||||
|         if (room->GetState() != Network::Room::State::Open) { |         if (room->GetState() != Network::Room::State::Open) { | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|         Network::RoomInformation room_information = room->GetRoomInformation(); |         UpdateBackendData(room); | ||||||
|         std::vector<Network::Room::Member> memberlist = room->GetRoomMemberList(); |         Common::WebResult result = backend->Update(); | ||||||
|         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(); |  | ||||||
|         if (result.result_code != Common::WebResult::Code::Success) { |         if (result.result_code != Common::WebResult::Code::Success) { | ||||||
|             std::lock_guard<std::mutex> lock(callback_mutex); |             std::lock_guard<std::mutex> lock(callback_mutex); | ||||||
|             for (auto callback : error_callbacks) { |             for (auto callback : error_callbacks) { | ||||||
|                 (*callback)(result); |                 (*callback)(result); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         if (result.result_string == "404") { | ||||||
|  |             registered = false; | ||||||
|  |             // Needs to register the room again
 | ||||||
|  |             Register(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include <atomic> | ||||||
| #include <functional> | #include <functional> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <mutex> | #include <mutex> | ||||||
|  | @ -13,6 +14,10 @@ | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/thread.h" | #include "common/thread.h" | ||||||
| 
 | 
 | ||||||
|  | namespace Network { | ||||||
|  | class Room; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| namespace Core { | namespace Core { | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  | @ -39,6 +44,9 @@ public: | ||||||
|      */ |      */ | ||||||
|     void UnbindErrorCallback(CallbackHandle handle); |     void UnbindErrorCallback(CallbackHandle handle); | ||||||
| 
 | 
 | ||||||
|  |     /// Registers a room to web services
 | ||||||
|  |     void Register(); | ||||||
|  | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Starts the announce of a room to web services |      * Starts the announce of a room to web services | ||||||
|      */ |      */ | ||||||
|  | @ -65,6 +73,9 @@ private: | ||||||
|     /// Backend interface that logs fields
 |     /// Backend interface that logs fields
 | ||||||
|     std::unique_ptr<AnnounceMultiplayerRoom::Backend> backend; |     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(); |     void AnnounceMultiplayerLoop(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -734,4 +734,21 @@ void InstallInterfaces(Core::System& system) { | ||||||
|     std::make_shared<CFG_NOR>()->InstallAsService(service_manager); |     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
 | } // namespace Service::CFG
 | ||||||
|  |  | ||||||
|  | @ -415,4 +415,7 @@ std::shared_ptr<Module> GetModule(Core::System& system); | ||||||
| 
 | 
 | ||||||
| void InstallInterfaces(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
 | } // 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.
 | /// Sends a WifiPacket to the room we're currently connected to.
 | ||||||
| void SendPacket(Network::WifiPacket& packet) { | void SendPacket(Network::WifiPacket& packet) { | ||||||
|     if (auto room_member = Network::GetRoomMember().lock()) { |     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(); |             packet.transmitter_address = room_member->GetMacAddress(); | ||||||
|             room_member->SendWifiPacket(packet); |             room_member->SendWifiPacket(packet); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,11 @@ add_executable(citra-room | ||||||
| create_target_directory_groups(citra-room) | create_target_directory_groups(citra-room) | ||||||
| 
 | 
 | ||||||
| target_link_libraries(citra-room PRIVATE common core network) | 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) | target_link_libraries(citra-room PRIVATE glad) | ||||||
| if (MSVC) | if (MSVC) | ||||||
|     target_link_libraries(citra-room PRIVATE getopt) |     target_link_libraries(citra-room PRIVATE getopt) | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
| // Refer to the license.txt file included.
 | // Refer to the license.txt file included.
 | ||||||
| 
 | 
 | ||||||
| #include <chrono> | #include <chrono> | ||||||
|  | #include <fstream> | ||||||
| #include <iostream> | #include <iostream> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <regex> | #include <regex> | ||||||
|  | @ -27,15 +28,23 @@ | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/detached_tasks.h" | #include "common/detached_tasks.h" | ||||||
| #include "common/scm_rev.h" | #include "common/scm_rev.h" | ||||||
|  | #include "common/string_util.h" | ||||||
| #include "core/announce_multiplayer_session.h" | #include "core/announce_multiplayer_session.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
| #include "network/network.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) { | static void PrintHelp(const char* argv0) { | ||||||
|     std::cout << "Usage: " << argv0 |     std::cout << "Usage: " << argv0 | ||||||
|               << " [options] <filename>\n" |               << " [options] <filename>\n" | ||||||
|                  "--room-name         The name of the room\n" |                  "--room-name         The name of the room\n" | ||||||
|  |                  "--room-description  The room description\n" | ||||||
|                  "--port              The port used for the room\n" |                  "--port              The port used for the room\n" | ||||||
|                  "--max_members       The maximum number of players for this room\n" |                  "--max_members       The maximum number of players for this room\n" | ||||||
|                  "--password          The password for the 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" |                  "--username          The username used for announce\n" | ||||||
|                  "--token             The token used for announce\n" |                  "--token             The token used for announce\n" | ||||||
|                  "--web-api-url       Citra Web API url\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" |                  "-h, --help          Display this help and exit\n" | ||||||
|                  "-v, --version       Output version information and exit\n"; |                  "-v, --version       Output version information and exit\n"; | ||||||
| } | } | ||||||
|  | @ -53,6 +64,71 @@ static void PrintVersion() { | ||||||
|               << " Libnetwork: " << Network::network_version << std::endl; |               << " 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
 | /// Application entry point
 | ||||||
| int main(int argc, char** argv) { | int main(int argc, char** argv) { | ||||||
|     Common::DetachedTasks detached_tasks; |     Common::DetachedTasks detached_tasks; | ||||||
|  | @ -63,17 +139,21 @@ int main(int argc, char** argv) { | ||||||
|     gladLoadGL(); |     gladLoadGL(); | ||||||
| 
 | 
 | ||||||
|     std::string room_name; |     std::string room_name; | ||||||
|  |     std::string room_description; | ||||||
|     std::string password; |     std::string password; | ||||||
|     std::string preferred_game; |     std::string preferred_game; | ||||||
|     std::string username; |     std::string username; | ||||||
|     std::string token; |     std::string token; | ||||||
|     std::string web_api_url; |     std::string web_api_url; | ||||||
|  |     std::string ban_list_file; | ||||||
|     u64 preferred_game_id = 0; |     u64 preferred_game_id = 0; | ||||||
|     u32 port = Network::DefaultRoomPort; |     u32 port = Network::DefaultRoomPort; | ||||||
|     u32 max_members = 16; |     u32 max_members = 16; | ||||||
|  |     bool enable_citra_mods = false; | ||||||
| 
 | 
 | ||||||
|     static struct option long_options[] = { |     static struct option long_options[] = { | ||||||
|         {"room-name", required_argument, 0, 'n'}, |         {"room-name", required_argument, 0, 'n'}, | ||||||
|  |         {"room-description", required_argument, 0, 'd'}, | ||||||
|         {"port", required_argument, 0, 'p'}, |         {"port", required_argument, 0, 'p'}, | ||||||
|         {"max_members", required_argument, 0, 'm'}, |         {"max_members", required_argument, 0, 'm'}, | ||||||
|         {"password", required_argument, 0, 'w'}, |         {"password", required_argument, 0, 'w'}, | ||||||
|  | @ -82,18 +162,23 @@ int main(int argc, char** argv) { | ||||||
|         {"username", required_argument, 0, 'u'}, |         {"username", required_argument, 0, 'u'}, | ||||||
|         {"token", required_argument, 0, 't'}, |         {"token", required_argument, 0, 't'}, | ||||||
|         {"web-api-url", required_argument, 0, 'a'}, |         {"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'}, |         {"help", no_argument, 0, 'h'}, | ||||||
|         {"version", no_argument, 0, 'v'}, |         {"version", no_argument, 0, 'v'}, | ||||||
|         {0, 0, 0, 0}, |         {0, 0, 0, 0}, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     while (optind < argc) { |     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) { |         if (arg != -1) { | ||||||
|             switch (arg) { |             switch (arg) { | ||||||
|             case 'n': |             case 'n': | ||||||
|                 room_name.assign(optarg); |                 room_name.assign(optarg); | ||||||
|                 break; |                 break; | ||||||
|  |             case 'd': | ||||||
|  |                 room_description.assign(optarg); | ||||||
|  |                 break; | ||||||
|             case 'p': |             case 'p': | ||||||
|                 port = strtoul(optarg, &endarg, 0); |                 port = strtoul(optarg, &endarg, 0); | ||||||
|                 break; |                 break; | ||||||
|  | @ -118,6 +203,12 @@ int main(int argc, char** argv) { | ||||||
|             case 'a': |             case 'a': | ||||||
|                 web_api_url.assign(optarg); |                 web_api_url.assign(optarg); | ||||||
|                 break; |                 break; | ||||||
|  |             case 'b': | ||||||
|  |                 ban_list_file.assign(optarg); | ||||||
|  |                 break; | ||||||
|  |             case 'e': | ||||||
|  |                 enable_citra_mods = true; | ||||||
|  |                 break; | ||||||
|             case 'h': |             case 'h': | ||||||
|                 PrintHelp(argv[0]); |                 PrintHelp(argv[0]); | ||||||
|                 return 0; |                 return 0; | ||||||
|  | @ -153,6 +244,10 @@ int main(int argc, char** argv) { | ||||||
|         PrintHelp(argv[0]); |         PrintHelp(argv[0]); | ||||||
|         return -1; |         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; |     bool announce = true; | ||||||
|     if (username.empty()) { |     if (username.empty()) { | ||||||
|         announce = false; |         announce = false; | ||||||
|  | @ -172,11 +267,35 @@ int main(int argc, char** argv) { | ||||||
|         Settings::values.citra_username = username; |         Settings::values.citra_username = username; | ||||||
|         Settings::values.citra_token = token; |         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(); |     Network::Init(); | ||||||
|     if (std::shared_ptr<Network::Room> room = Network::GetRoom().lock()) { |     if (std::shared_ptr<Network::Room> room = Network::GetRoom().lock()) { | ||||||
|         if (!room->Create(room_name, "", port, password, max_members, preferred_game, |         if (!room->Create(room_name, room_description, "", port, password, max_members, username, | ||||||
|                           preferred_game_id)) { |                           preferred_game, preferred_game_id, std::move(verify_backend), ban_list, | ||||||
|  |                           enable_citra_mods)) { | ||||||
|             std::cout << "Failed to create room: \n\n"; |             std::cout << "Failed to create room: \n\n"; | ||||||
|             return -1; |             return -1; | ||||||
|         } |         } | ||||||
|  | @ -193,6 +312,10 @@ int main(int argc, char** argv) { | ||||||
|                     announce_session->Stop(); |                     announce_session->Stop(); | ||||||
|                 } |                 } | ||||||
|                 announce_session.reset(); |                 announce_session.reset(); | ||||||
|  |                 // Save the ban list
 | ||||||
|  |                 if (!ban_list_file.empty()) { | ||||||
|  |                     SaveBanList(room->GetBanList(), ban_list_file); | ||||||
|  |                 } | ||||||
|                 room->Destroy(); |                 room->Destroy(); | ||||||
|                 Network::Shutdown(); |                 Network::Shutdown(); | ||||||
|                 return 0; |                 return 0; | ||||||
|  | @ -203,6 +326,10 @@ int main(int argc, char** argv) { | ||||||
|             announce_session->Stop(); |             announce_session->Stop(); | ||||||
|         } |         } | ||||||
|         announce_session.reset(); |         announce_session.reset(); | ||||||
|  |         // Save the ban list
 | ||||||
|  |         if (!ban_list_file.empty()) { | ||||||
|  |             SaveBanList(room->GetBanList(), ban_list_file); | ||||||
|  |         } | ||||||
|         room->Destroy(); |         room->Destroy(); | ||||||
|     } |     } | ||||||
|     Network::Shutdown(); |     Network::Shutdown(); | ||||||
|  |  | ||||||
|  | @ -7,8 +7,10 @@ add_library(network STATIC | ||||||
|     room.h |     room.h | ||||||
|     room_member.cpp |     room_member.cpp | ||||||
|     room_member.h |     room_member.h | ||||||
|  |     verify_user.cpp | ||||||
|  |     verify_user.h | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| create_target_directory_groups(network) | 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
 |     // Then extract the data
 | ||||||
|     for (std::size_t i = 0; i < out_data.size(); ++i) { |     for (std::size_t i = 0; i < out_data.size(); ++i) { | ||||||
|         T character = 0; |         T character; | ||||||
|         *this >> character; |         *this >> character; | ||||||
|         out_data[i] = character; |         out_data[i] = character; | ||||||
|     } |     } | ||||||
|  | @ -136,7 +136,7 @@ Packet& Packet::operator>>(std::vector<T>& out_data) { | ||||||
| template <typename T, std::size_t S> | template <typename T, std::size_t S> | ||||||
| Packet& Packet::operator>>(std::array<T, S>& out_data) { | Packet& Packet::operator>>(std::array<T, S>& out_data) { | ||||||
|     for (std::size_t i = 0; i < out_data.size(); ++i) { |     for (std::size_t i = 0; i < out_data.size(); ++i) { | ||||||
|         T character = 0; |         T character; | ||||||
|         *this >> character; |         *this >> character; | ||||||
|         out_data[i] = character; |         out_data[i] = character; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -7,12 +7,14 @@ | ||||||
| #include <iomanip> | #include <iomanip> | ||||||
| #include <mutex> | #include <mutex> | ||||||
| #include <random> | #include <random> | ||||||
|  | #include <regex> | ||||||
| #include <sstream> | #include <sstream> | ||||||
| #include <thread> | #include <thread> | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "enet/enet.h" | #include "enet/enet.h" | ||||||
| #include "network/packet.h" | #include "network/packet.h" | ||||||
| #include "network/room.h" | #include "network/room.h" | ||||||
|  | #include "network/verify_user.h" | ||||||
| 
 | 
 | ||||||
| namespace Network { | namespace Network { | ||||||
| 
 | 
 | ||||||
|  | @ -27,12 +29,18 @@ public: | ||||||
|     std::atomic<State> state{State::Closed}; ///< Current state of the room.
 |     std::atomic<State> state{State::Closed}; ///< Current state of the room.
 | ||||||
|     RoomInformation room_information;        ///< Information about this 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.
 |     std::string password; ///< The password required to connect to this room.
 | ||||||
| 
 | 
 | ||||||
|     struct Member { |     struct Member { | ||||||
|         std::string nickname;        ///< The nickname of the 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
 |         GameInfo game_info;          ///< The current game of the member
 | ||||||
|         MacAddress mac_address;      ///< The assigned mac address 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.
 |         ENetPeer* peer; ///< The remote peer.
 | ||||||
|     }; |     }; | ||||||
|     using MemberList = std::vector<Member>; |     using MemberList = std::vector<Member>; | ||||||
|  | @ -40,12 +48,19 @@ public: | ||||||
|     mutable std::mutex member_mutex; ///< Mutex for locking the members list
 |     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
 |     /// 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() |     RoomImpl() | ||||||
|         : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} |         : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} | ||||||
| 
 | 
 | ||||||
|     /// Thread that receives and dispatches network packets
 |     /// Thread that receives and dispatches network packets
 | ||||||
|     std::unique_ptr<std::thread> room_thread; |     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.
 |     /// Thread function that will receive and dispatch messages until the room is destroyed.
 | ||||||
|     void ServerLoop(); |     void ServerLoop(); | ||||||
|     void StartLoop(); |     void StartLoop(); | ||||||
|  | @ -57,6 +72,30 @@ public: | ||||||
|      */ |      */ | ||||||
|     void HandleJoinRequest(const ENetEvent* event); |     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. |      * 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; |     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. |      * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. | ||||||
|      */ |      */ | ||||||
|  | @ -78,6 +133,12 @@ public: | ||||||
|      */ |      */ | ||||||
|     void SendMacCollision(ENetPeer* client); |     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. |      * 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); |     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, |      * Notifies the members that the room is closed, | ||||||
|      */ |      */ | ||||||
|     void SendCloseMessage(); |     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 |      * Sends the information about the room, along with the list of members | ||||||
|      * to every connected client in the room. |      * to every connected client in the room. | ||||||
|      * The packet has the structure: |      * The packet has the structure: | ||||||
|      * <MessageID>ID_ROOM_INFORMATION |      * <MessageID>ID_ROOM_INFORMATION | ||||||
|      * <String> room_name |      * <String> room_name | ||||||
|  |      * <String> room_description | ||||||
|      * <u32> member_slots: The max number of clients allowed in this room |      * <u32> member_slots: The max number of clients allowed in this room | ||||||
|      * <String> uid |      * <String> uid | ||||||
|      * <u16> port |      * <u16> port | ||||||
|  | @ -145,11 +245,6 @@ public: | ||||||
|      * to all other clients. |      * to all other clients. | ||||||
|      */ |      */ | ||||||
|     void HandleClientDisconnection(ENetPeer* client); |     void HandleClientDisconnection(ENetPeer* client); | ||||||
| 
 |  | ||||||
|     /**
 |  | ||||||
|      * Creates a random ID in the form 12345678-1234-1234-1234-123456789012 |  | ||||||
|      */ |  | ||||||
|     void CreateUniqueID(); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // RoomImpl
 | // RoomImpl
 | ||||||
|  | @ -172,6 +267,19 @@ void Room::RoomImpl::ServerLoop() { | ||||||
|                 case IdChatMessage: |                 case IdChatMessage: | ||||||
|                     HandleChatPacket(&event); |                     HandleChatPacket(&event); | ||||||
|                     break; |                     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); |                 enet_packet_destroy(event.packet); | ||||||
|                 break; |                 break; | ||||||
|  | @ -193,12 +301,22 @@ void Room::RoomImpl::StartLoop() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { | 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 packet; | ||||||
|     packet.Append(event->packet->data, event->packet->dataLength); |     packet.Append(event->packet->data, event->packet->dataLength); | ||||||
|     packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
 |     packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
 | ||||||
|     std::string nickname; |     std::string nickname; | ||||||
|     packet >> nickname; |     packet >> nickname; | ||||||
| 
 | 
 | ||||||
|  |     std::string console_id_hash; | ||||||
|  |     packet >> console_id_hash; | ||||||
|  | 
 | ||||||
|     MacAddress preferred_mac; |     MacAddress preferred_mac; | ||||||
|     packet >> preferred_mac; |     packet >> preferred_mac; | ||||||
| 
 | 
 | ||||||
|  | @ -208,6 +326,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { | ||||||
|     std::string pass; |     std::string pass; | ||||||
|     packet >> pass; |     packet >> pass; | ||||||
| 
 | 
 | ||||||
|  |     std::string token; | ||||||
|  |     packet >> token; | ||||||
|  | 
 | ||||||
|     if (pass != password) { |     if (pass != password) { | ||||||
|         SendWrongPassword(event->peer); |         SendWrongPassword(event->peer); | ||||||
|         return; |         return; | ||||||
|  | @ -229,6 +350,11 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { | ||||||
|         preferred_mac = GenerateMacAddress(); |         preferred_mac = GenerateMacAddress(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (!IsValidConsoleId(console_id_hash)) { | ||||||
|  |         SendConsoleIdCollision(event->peer); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (client_version != network_version) { |     if (client_version != network_version) { | ||||||
|         SendVersionMismatch(event->peer); |         SendVersionMismatch(event->peer); | ||||||
|         return; |         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.
 |     // At this point the client is ready to be added to the room.
 | ||||||
|     Member member{}; |     Member member{}; | ||||||
|     member.mac_address = preferred_mac; |     member.mac_address = preferred_mac; | ||||||
|  |     member.console_id_hash = console_id_hash; | ||||||
|     member.nickname = nickname; |     member.nickname = nickname; | ||||||
|     member.peer = event->peer; |     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); |         std::lock_guard<std::mutex> lock(member_mutex); | ||||||
|         members.push_back(std::move(member)); |         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.
 |     // Notify everyone that the room information has changed.
 | ||||||
|     BroadcastRoomInformation(); |     BroadcastRoomInformation(); | ||||||
|  |     if (HasModPermission(event->peer)) { | ||||||
|  |         SendJoinSuccessAsMod(event->peer, preferred_mac); | ||||||
|  |     } else { | ||||||
|         SendJoinSuccess(event->peer, preferred_mac); |         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 { | 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.
 |     // A nickname is valid if it matches the regex and is not already taken by anybody else in the
 | ||||||
|     // TODO(B3N30): Check for empty names, spaces, etc.
 |     // 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); |     std::lock_guard<std::mutex> lock(member_mutex); | ||||||
|     return std::all_of(members.begin(), members.end(), |     return std::all_of(members.begin(), members.end(), | ||||||
|                        [&nickname](const auto& member) { return member.nickname != nickname; }); |                        [&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; }); |                        [&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) { | void Room::RoomImpl::SendNameCollision(ENetPeer* client) { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdNameCollision); |     packet << static_cast<u8>(IdNameCollision); | ||||||
|  | @ -285,6 +629,16 @@ void Room::RoomImpl::SendMacCollision(ENetPeer* client) { | ||||||
|     enet_host_flush(server); |     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) { | void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdWrongPassword); |     packet << static_cast<u8>(IdWrongPassword); | ||||||
|  | @ -295,6 +649,16 @@ void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { | ||||||
|     enet_host_flush(server); |     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) { | void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdVersionMismatch); |     packet << static_cast<u8>(IdVersionMismatch); | ||||||
|  | @ -316,6 +680,71 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { | ||||||
|     enet_host_flush(server); |     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() { | void Room::RoomImpl::SendCloseMessage() { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdCloseRoom); |     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() { | void Room::RoomImpl::BroadcastRoomInformation() { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdRoomInformation); |     packet << static_cast<u8>(IdRoomInformation); | ||||||
|     packet << room_information.name; |     packet << room_information.name; | ||||||
|  |     packet << room_information.description; | ||||||
|     packet << room_information.member_slots; |     packet << room_information.member_slots; | ||||||
|     packet << room_information.uid; |  | ||||||
|     packet << room_information.port; |     packet << room_information.port; | ||||||
|     packet << room_information.preferred_game; |     packet << room_information.preferred_game; | ||||||
|  |     packet << room_information.host_username; | ||||||
| 
 | 
 | ||||||
|     packet << static_cast<u32>(members.size()); |     packet << static_cast<u32>(members.size()); | ||||||
|     { |     { | ||||||
|  | @ -350,6 +798,9 @@ void Room::RoomImpl::BroadcastRoomInformation() { | ||||||
|             packet << member.mac_address; |             packet << member.mac_address; | ||||||
|             packet << member.game_info.name; |             packet << member.game_info.name; | ||||||
|             packet << member.game_info.id; |             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; |     Packet out_packet; | ||||||
|     out_packet << static_cast<u8>(IdChatMessage); |     out_packet << static_cast<u8>(IdChatMessage); | ||||||
|     out_packet << sending_member->nickname; |     out_packet << sending_member->nickname; | ||||||
|  |     out_packet << sending_member->user_data.username; | ||||||
|     out_packet << message; |     out_packet << message; | ||||||
| 
 | 
 | ||||||
|     ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), |     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) { | void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { | ||||||
|     // Remove the client from the members list.
 |     // Remove the client from the members list.
 | ||||||
|  |     std::string nickname, username; | ||||||
|     { |     { | ||||||
|         std::lock_guard<std::mutex> lock(member_mutex); |         std::lock_guard<std::mutex> lock(member_mutex); | ||||||
|         members.erase( |         auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) { | ||||||
|             std::remove_if(members.begin(), members.end(), |             return member.peer == client; | ||||||
|                            [client](const Member& member) { return member.peer == client; }), |         }); | ||||||
|             members.end()); |         if (member != members.end()) { | ||||||
|  |             nickname = member->nickname; | ||||||
|  |             username = member->user_data.username; | ||||||
|  |             members.erase(member); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Announce the change to all clients.
 |     // Announce the change to all clients.
 | ||||||
|     enet_peer_disconnect(client, 0); |     enet_peer_disconnect(client, 0); | ||||||
|  |     if (!nickname.empty()) | ||||||
|  |         SendStatusMessage(IdMemberLeave, nickname, username); | ||||||
|     BroadcastRoomInformation(); |     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() : room_impl{std::make_unique<RoomImpl>()} {} | Room::Room() : room_impl{std::make_unique<RoomImpl>()} {} | ||||||
| 
 | 
 | ||||||
| Room::~Room() = default; | Room::~Room() = default; | ||||||
| 
 | 
 | ||||||
| bool Room::Create(const std::string& name, const std::string& server_address, u16 server_port, | bool Room::Create(const std::string& name, const std::string& description, | ||||||
|                   const std::string& password, const u32 max_connections, |                   const std::string& server_address, u16 server_port, const std::string& password, | ||||||
|                   const std::string& preferred_game, u64 preferred_game_id) { |                   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; |     ENetAddress address; | ||||||
|     address.host = ENET_HOST_ANY; |     address.host = ENET_HOST_ANY; | ||||||
|     if (!server_address.empty()) { |     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; |     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) { |     if (!room_impl->server) { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|     room_impl->state = State::Open; |     room_impl->state = State::Open; | ||||||
| 
 | 
 | ||||||
|     room_impl->room_information.name = name; |     room_impl->room_information.name = name; | ||||||
|  |     room_impl->room_information.description = description; | ||||||
|     room_impl->room_information.member_slots = max_connections; |     room_impl->room_information.member_slots = max_connections; | ||||||
|     room_impl->room_information.port = server_port; |     room_impl->room_information.port = server_port; | ||||||
|     room_impl->room_information.preferred_game = preferred_game; |     room_impl->room_information.preferred_game = preferred_game; | ||||||
|     room_impl->room_information.preferred_game_id = preferred_game_id; |     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->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(); |     room_impl->StartLoop(); | ||||||
|     return true; |     return true; | ||||||
|  | @ -553,12 +1008,25 @@ const RoomInformation& Room::GetRoomInformation() const { | ||||||
|     return room_impl->room_information; |     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> Room::GetRoomMemberList() const { | ||||||
|     std::vector<Room::Member> member_list; |     std::vector<Room::Member> member_list; | ||||||
|     std::lock_guard<std::mutex> lock(room_impl->member_mutex); |     std::lock_guard<std::mutex> lock(room_impl->member_mutex); | ||||||
|     for (const auto& member_impl : room_impl->members) { |     for (const auto& member_impl : room_impl->members) { | ||||||
|         Member member; |         Member member; | ||||||
|         member.nickname = member_impl.nickname; |         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.mac_address = member_impl.mac_address; | ||||||
|         member.game_info = member_impl.game_info; |         member.game_info = member_impl.game_info; | ||||||
|         member_list.push_back(member); |         member_list.push_back(member); | ||||||
|  | @ -570,6 +1038,11 @@ bool Room::HasPassword() const { | ||||||
|     return !room_impl->password.empty(); |     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() { | void Room::Destroy() { | ||||||
|     room_impl->state = State::Closed; |     room_impl->state = State::Closed; | ||||||
|     room_impl->room_thread->join(); |     room_impl->room_thread->join(); | ||||||
|  |  | ||||||
|  | @ -9,10 +9,11 @@ | ||||||
| #include <string> | #include <string> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
|  | #include "network/verify_user.h" | ||||||
| 
 | 
 | ||||||
| namespace Network { | 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; | constexpr u16 DefaultRoomPort = 24872; | ||||||
| 
 | 
 | ||||||
|  | @ -25,11 +26,13 @@ constexpr std::size_t NumChannels = 1; // Number of channels used for the connec | ||||||
| 
 | 
 | ||||||
| struct RoomInformation { | struct RoomInformation { | ||||||
|     std::string name;           ///< Name of the server
 |     std::string name;           ///< Name of the server
 | ||||||
|  |     std::string description;    ///< Server description
 | ||||||
|     u32 member_slots;           ///< Maximum number of members in this room
 |     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
 |     u16 port;                   ///< The port of this room
 | ||||||
|     std::string preferred_game; ///< Game to advertise that you want to play
 |     std::string preferred_game; ///< Game to advertise that you want to play
 | ||||||
|     u64 preferred_game_id;      ///< Title ID for the advertised game
 |     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 { | struct GameInfo { | ||||||
|  | @ -57,7 +60,31 @@ enum RoomMessageTypes : u8 { | ||||||
|     IdMacCollision, |     IdMacCollision, | ||||||
|     IdVersionMismatch, |     IdVersionMismatch, | ||||||
|     IdWrongPassword, |     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.
 | /// This is what a server [person creating a server] would use.
 | ||||||
|  | @ -70,6 +97,9 @@ public: | ||||||
| 
 | 
 | ||||||
|     struct Member { |     struct Member { | ||||||
|         std::string nickname;     ///< The nickname of the member.
 |         std::string nickname;     ///< The nickname of the member.
 | ||||||
|  |         std::string username;     ///< The web services username of the member. Can be empty.
 | ||||||
|  |         std::string display_name; ///< The web services display name of the member. Can be empty.
 | ||||||
|  |         std::string avatar_url;   ///< Url to the member's avatar. Can be empty.
 | ||||||
|         GameInfo game_info;       ///< The current game of the member
 |         GameInfo game_info;       ///< The current game of the member
 | ||||||
|         MacAddress mac_address;   ///< The assigned mac address of the member.
 |         MacAddress mac_address;   ///< The assigned mac address of the member.
 | ||||||
|     }; |     }; | ||||||
|  | @ -87,6 +117,11 @@ public: | ||||||
|      */ |      */ | ||||||
|     const RoomInformation& GetRoomInformation() const; |     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. |      * Gets a list of the mbmers connected to the room. | ||||||
|      */ |      */ | ||||||
|  | @ -97,14 +132,33 @@ public: | ||||||
|      */ |      */ | ||||||
|     bool HasPassword() const; |     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 |      * Creates the socket for this room. Will bind to default address if | ||||||
|      * server is empty string. |      * server is empty string. | ||||||
|      */ |      */ | ||||||
|     bool Create(const std::string& name, const std::string& server = "", |     bool Create(const std::string& name, const std::string& description = "", | ||||||
|                 u16 server_port = DefaultRoomPort, const std::string& password = "", |                 const std::string& server = "", u16 server_port = DefaultRoomPort, | ||||||
|  |                 const std::string& password = "", | ||||||
|                 const u32 max_connections = MaxConcurrentConnections, |                 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 |      * Destroys the socket | ||||||
|  |  | ||||||
|  | @ -31,9 +31,14 @@ public: | ||||||
| 
 | 
 | ||||||
|     std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
 |     std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
 | ||||||
|     void SetState(const State new_state); |     void SetState(const State new_state); | ||||||
|  |     void SetError(const Error new_error); | ||||||
|     bool IsConnected() const; |     bool IsConnected() const; | ||||||
| 
 | 
 | ||||||
|     std::string nickname; ///< The nickname of this member.
 |     std::string nickname; ///< The nickname of this member.
 | ||||||
|  | 
 | ||||||
|  |     std::string username;              ///< The username of this member.
 | ||||||
|  |     mutable std::mutex username_mutex; ///< Mutex for locking username.
 | ||||||
|  | 
 | ||||||
|     MacAddress mac_address; ///< The mac_address of this member.
 |     MacAddress mac_address; ///< The mac_address of this member.
 | ||||||
| 
 | 
 | ||||||
|     std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
 |     std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
 | ||||||
|  | @ -54,8 +59,11 @@ public: | ||||||
|     private: |     private: | ||||||
|         CallbackSet<WifiPacket> callback_set_wifi_packet; |         CallbackSet<WifiPacket> callback_set_wifi_packet; | ||||||
|         CallbackSet<ChatEntry> callback_set_chat_messages; |         CallbackSet<ChatEntry> callback_set_chat_messages; | ||||||
|  |         CallbackSet<StatusMessageEntry> callback_set_status_messages; | ||||||
|         CallbackSet<RoomInformation> callback_set_room_information; |         CallbackSet<RoomInformation> callback_set_room_information; | ||||||
|         CallbackSet<State> callback_set_state; |         CallbackSet<State> callback_set_state; | ||||||
|  |         CallbackSet<Error> callback_set_error; | ||||||
|  |         CallbackSet<Room::BanList> callback_set_ban_list; | ||||||
|     }; |     }; | ||||||
|     Callbacks callbacks; ///< All CallbackSets to all events
 |     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 |      * Sends a request to the server, asking for permission to join a room with the specified | ||||||
|      * nickname and preferred mac. |      * nickname and preferred mac. | ||||||
|      * @params nickname The desired nickname. |      * @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 preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells | ||||||
|      * @params password The password for the room |      * @params password The password for the room | ||||||
|      * the server to assign one for us. |      * 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 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. |      * Extracts a MAC Address from a received ENet packet. | ||||||
|  | @ -104,6 +113,19 @@ public: | ||||||
|      */ |      */ | ||||||
|     void HandleChatPacket(const ENetEvent* event); |     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 |      * 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 { | 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() { | void RoomMember::RoomMemberImpl::MemberLoop() { | ||||||
|  | @ -143,37 +169,78 @@ void RoomMember::RoomMemberImpl::MemberLoop() { | ||||||
|                 case IdChatMessage: |                 case IdChatMessage: | ||||||
|                     HandleChatPacket(&event); |                     HandleChatPacket(&event); | ||||||
|                     break; |                     break; | ||||||
|  |                 case IdStatusMessage: | ||||||
|  |                     HandleStatusMessagePacket(&event); | ||||||
|  |                     break; | ||||||
|                 case IdRoomInformation: |                 case IdRoomInformation: | ||||||
|                     HandleRoomInformationPacket(&event); |                     HandleRoomInformationPacket(&event); | ||||||
|                     break; |                     break; | ||||||
|                 case IdJoinSuccess: |                 case IdJoinSuccess: | ||||||
|  |                 case IdJoinSuccessAsMod: | ||||||
|                     // The join request was successful, we are now in the room.
 |                     // 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.
 |                     // If we joined successfully, there must be at least one client in the room: us.
 | ||||||
|                     ASSERT_MSG(member_information.size() > 0, |                     ASSERT_MSG(member_information.size() > 0, | ||||||
|                                "We have not yet received member information."); |                                "We have not yet received member information."); | ||||||
|                     HandleJoinPacket(&event); // Get the MAC Address for the client
 |                     HandleJoinPacket(&event); // Get the MAC Address for the client
 | ||||||
|  |                     if (event.packet->data[0] == IdJoinSuccessAsMod) { | ||||||
|  |                         SetState(State::Moderator); | ||||||
|  |                     } else { | ||||||
|                         SetState(State::Joined); |                         SetState(State::Joined); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case IdModBanListResponse: | ||||||
|  |                     HandleModBanListResponsePacket(&event); | ||||||
|  |                     break; | ||||||
|  |                 case IdRoomIsFull: | ||||||
|  |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::RoomIsFull); | ||||||
|                     break; |                     break; | ||||||
|                 case IdNameCollision: |                 case IdNameCollision: | ||||||
|                     SetState(State::NameCollision); |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::NameCollision); | ||||||
|                     break; |                     break; | ||||||
|                 case IdMacCollision: |                 case IdMacCollision: | ||||||
|                     SetState(State::MacCollision); |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::MacCollision); | ||||||
|  |                     break; | ||||||
|  |                 case IdConsoleIdCollision: | ||||||
|  |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::ConsoleIdCollision); | ||||||
|                     break; |                     break; | ||||||
|                 case IdVersionMismatch: |                 case IdVersionMismatch: | ||||||
|                     SetState(State::WrongVersion); |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::WrongVersion); | ||||||
|                     break; |                     break; | ||||||
|                 case IdWrongPassword: |                 case IdWrongPassword: | ||||||
|                     SetState(State::WrongPassword); |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::WrongPassword); | ||||||
|                     break; |                     break; | ||||||
|                 case IdCloseRoom: |                 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; |                     break; | ||||||
|                 } |                 } | ||||||
|                 enet_packet_destroy(event.packet); |                 enet_packet_destroy(event.packet); | ||||||
|                 break; |                 break; | ||||||
|             case ENET_EVENT_TYPE_DISCONNECT: |             case ENET_EVENT_TYPE_DISCONNECT: | ||||||
|                 SetState(State::LostConnection); |                 if (state == State::Joined || state == State::Moderator) { | ||||||
|  |                     SetState(State::Idle); | ||||||
|  |                     SetError(Error::LostConnection); | ||||||
|  |                 } | ||||||
|                 break; |                 break; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -201,14 +268,18 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, | void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, | ||||||
|  |                                                  const std::string& console_id_hash, | ||||||
|                                                  const MacAddress& preferred_mac, |                                                  const MacAddress& preferred_mac, | ||||||
|                                                  const std::string& password) { |                                                  const std::string& password, | ||||||
|  |                                                  const std::string& token) { | ||||||
|     Packet packet; |     Packet packet; | ||||||
|     packet << static_cast<u8>(IdJoinRequest); |     packet << static_cast<u8>(IdJoinRequest); | ||||||
|     packet << nickname; |     packet << nickname; | ||||||
|  |     packet << console_id_hash; | ||||||
|     packet << preferred_mac; |     packet << preferred_mac; | ||||||
|     packet << network_version; |     packet << network_version; | ||||||
|     packet << password; |     packet << password; | ||||||
|  |     packet << token; | ||||||
|     Send(std::move(packet)); |     Send(std::move(packet)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -221,14 +292,17 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev | ||||||
| 
 | 
 | ||||||
|     RoomInformation info{}; |     RoomInformation info{}; | ||||||
|     packet >> info.name; |     packet >> info.name; | ||||||
|  |     packet >> info.description; | ||||||
|     packet >> info.member_slots; |     packet >> info.member_slots; | ||||||
|     packet >> info.uid; |  | ||||||
|     packet >> info.port; |     packet >> info.port; | ||||||
|     packet >> info.preferred_game; |     packet >> info.preferred_game; | ||||||
|  |     packet >> info.host_username; | ||||||
|     room_information.name = info.name; |     room_information.name = info.name; | ||||||
|  |     room_information.description = info.description; | ||||||
|     room_information.member_slots = info.member_slots; |     room_information.member_slots = info.member_slots; | ||||||
|     room_information.port = info.port; |     room_information.port = info.port; | ||||||
|     room_information.preferred_game = info.preferred_game; |     room_information.preferred_game = info.preferred_game; | ||||||
|  |     room_information.host_username = info.host_username; | ||||||
| 
 | 
 | ||||||
|     u32 num_members; |     u32 num_members; | ||||||
|     packet >> num_members; |     packet >> num_members; | ||||||
|  | @ -239,6 +313,16 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev | ||||||
|         packet >> member.mac_address; |         packet >> member.mac_address; | ||||||
|         packet >> member.game_info.name; |         packet >> member.game_info.name; | ||||||
|         packet >> member.game_info.id; |         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); |     Invoke(room_information); | ||||||
| } | } | ||||||
|  | @ -252,7 +336,6 @@ void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { | ||||||
| 
 | 
 | ||||||
|     // Parse the MAC Address from the packet
 |     // Parse the MAC Address from the packet
 | ||||||
|     packet >> mac_address; |     packet >> mac_address; | ||||||
|     SetState(State::Joined); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { | void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { | ||||||
|  | @ -286,10 +369,40 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { | ||||||
| 
 | 
 | ||||||
|     ChatEntry chat_entry{}; |     ChatEntry chat_entry{}; | ||||||
|     packet >> chat_entry.nickname; |     packet >> chat_entry.nickname; | ||||||
|  |     packet >> chat_entry.username; | ||||||
|     packet >> chat_entry.message; |     packet >> chat_entry.message; | ||||||
|     Invoke<ChatEntry>(chat_entry); |     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() { | void RoomMember::RoomMemberImpl::Disconnect() { | ||||||
|     member_information.clear(); |     member_information.clear(); | ||||||
|     room_information.member_slots = 0; |     room_information.member_slots = 0; | ||||||
|  | @ -329,6 +442,12 @@ RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||||
|     return callback_set_state; |     return callback_set_state; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | template <> | ||||||
|  | RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>& | ||||||
|  | RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||||
|  |     return callback_set_error; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| template <> | template <> | ||||||
| RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& | RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& | ||||||
| RoomMember::RoomMemberImpl::Callbacks::Get() { | RoomMember::RoomMemberImpl::Callbacks::Get() { | ||||||
|  | @ -340,6 +459,18 @@ RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl:: | ||||||
|     return callback_set_chat_messages; |     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> | template <typename T> | ||||||
| void RoomMember::RoomMemberImpl::Invoke(const T& data) { | void RoomMember::RoomMemberImpl::Invoke(const T& data) { | ||||||
|     std::lock_guard<std::mutex> lock(callback_mutex); |     std::lock_guard<std::mutex> lock(callback_mutex); | ||||||
|  | @ -380,6 +511,11 @@ const std::string& RoomMember::GetNickname() const { | ||||||
|     return room_member_impl->nickname; |     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 { | const MacAddress& RoomMember::GetMacAddress() const { | ||||||
|     ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); |     ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); | ||||||
|     return room_member_impl->mac_address; |     return room_member_impl->mac_address; | ||||||
|  | @ -389,9 +525,10 @@ RoomInformation RoomMember::GetRoomInformation() const { | ||||||
|     return room_member_impl->room_information; |     return room_member_impl->room_information; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void RoomMember::Join(const std::string& nick, const char* server_addr, u16 server_port, | void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, | ||||||
|                       u16 client_port, const MacAddress& preferred_mac, |                       const char* server_addr, u16 server_port, u16 client_port, | ||||||
|                       const std::string& password) { |                       const MacAddress& preferred_mac, const std::string& password, | ||||||
|  |                       const std::string& token) { | ||||||
|     // If the member is connected, kill the connection first
 |     // If the member is connected, kill the connection first
 | ||||||
|     if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { |     if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { | ||||||
|         Leave(); |         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); |         enet_host_connect(room_member_impl->client, &address, NumChannels, 0); | ||||||
| 
 | 
 | ||||||
|     if (!room_member_impl->server) { |     if (!room_member_impl->server) { | ||||||
|         room_member_impl->SetState(State::Error); |         room_member_impl->SetState(State::Idle); | ||||||
|  |         room_member_impl->SetError(Error::UnknownError); | ||||||
|         return; |         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) { |     if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { | ||||||
|         room_member_impl->nickname = nick; |         room_member_impl->nickname = nick; | ||||||
|         room_member_impl->StartLoop(); |         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); |         SendGameInfo(room_member_impl->current_game_info); | ||||||
|     } else { |     } else { | ||||||
|         enet_peer_disconnect(room_member_impl->server, 0); |         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)); |     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( | RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged( | ||||||
|     std::function<void(const RoomMember::State&)> callback) { |     std::function<void(const RoomMember::State&)> callback) { | ||||||
|     return room_member_impl->Bind(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( | RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived( | ||||||
|     std::function<void(const WifiPacket&)> callback) { |     std::function<void(const WifiPacket&)> callback) { | ||||||
|     return room_member_impl->Bind(callback); |     return room_member_impl->Bind(callback); | ||||||
|  | @ -486,6 +651,16 @@ RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved( | ||||||
|     return room_member_impl->Bind(callback); |     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> | template <typename T> | ||||||
| void RoomMember::Unbind(CallbackHandle<T> handle) { | void RoomMember::Unbind(CallbackHandle<T> handle) { | ||||||
|     std::lock_guard<std::mutex> lock(room_member_impl->callback_mutex); |     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<WifiPacket>); | ||||||
| template void RoomMember::Unbind(CallbackHandle<RoomMember::State>); | 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<RoomInformation>); | ||||||
| template void RoomMember::Unbind(CallbackHandle<ChatEntry>); | template void RoomMember::Unbind(CallbackHandle<ChatEntry>); | ||||||
|  | template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>); | ||||||
|  | template void RoomMember::Unbind(CallbackHandle<Room::BanList>); | ||||||
| 
 | 
 | ||||||
| } // namespace Network
 | } // namespace Network
 | ||||||
|  |  | ||||||
|  | @ -35,9 +35,19 @@ struct WifiPacket { | ||||||
| /// Represents a chat message.
 | /// Represents a chat message.
 | ||||||
| struct ChatEntry { | struct ChatEntry { | ||||||
|     std::string nickname; ///< Nickname of the client who sent this message.
 |     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.
 |     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. |  * 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 |  * 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: | public: | ||||||
|     enum class State : u8 { |     enum class State : u8 { | ||||||
|         Uninitialized, ///< Not initialized
 |         Uninitialized, ///< Not initialized
 | ||||||
|         Idle,          ///< Default state
 |         Idle,          ///< Default state (i.e. not connected)
 | ||||||
|         Error,         ///< Some error [permissions to network device missing or something]
 |  | ||||||
|         Joining,       ///< The client is attempting to join a room.
 |         Joining,       ///< The client is attempting to join a room.
 | ||||||
|         Joined,    ///< The client is connected to the room and is ready to send/receive packets.
 |         Joined,    ///< The client is connected to the room and is ready to send/receive packets.
 | ||||||
|  |         Moderator, ///< The client is connnected to the room and is granted mod permissions.
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     enum class Error : u8 { | ||||||
|  |         // Reasons why connection was closed
 | ||||||
|         LostConnection, ///< Connection closed
 |         LostConnection, ///< Connection closed
 | ||||||
|  |         HostKicked,     ///< Kicked by the host
 | ||||||
| 
 | 
 | ||||||
|         // Reasons why connection was rejected
 |         // Reasons why connection was rejected
 | ||||||
|  |         UnknownError,       ///< Some error [permissions to network device missing or something]
 | ||||||
|         NameCollision,      ///< Somebody is already using this name
 |         NameCollision,      ///< Somebody is already using this name
 | ||||||
|         MacCollision,       ///< Somebody is already using that mac-address
 |         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
 |         WrongVersion,       ///< The room version is not the same as for this RoomMember
 | ||||||
|         WrongPassword,      ///< The password doesn't match the one from the Room
 |         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 { |     struct MemberInformation { | ||||||
|         std::string nickname;     ///< Nickname of the member.
 |         std::string nickname;     ///< Nickname of the member.
 | ||||||
|  |         std::string username;     ///< The web services username of the member. Can be empty.
 | ||||||
|  |         std::string display_name; ///< The web services display name of the member. Can be empty.
 | ||||||
|  |         std::string avatar_url;   ///< Url to the member's avatar. Can be empty.
 | ||||||
|         GameInfo game_info;     ///< Name of the game they're currently playing, or empty if they're
 |         GameInfo game_info;     ///< Name of the game they're currently playing, or empty if they're
 | ||||||
|                                 /// not playing anything.
 |                                 /// not playing anything.
 | ||||||
|         MacAddress mac_address; ///< MAC address associated with this member.
 |         MacAddress mac_address; ///< MAC address associated with this member.
 | ||||||
|  | @ -98,6 +124,11 @@ public: | ||||||
|      */ |      */ | ||||||
|     const std::string& GetNickname() const; |     const std::string& GetNickname() const; | ||||||
| 
 | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Returns the username of the RoomMember. | ||||||
|  |      */ | ||||||
|  |     const std::string& GetUsername() const; | ||||||
|  | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Returns the MAC address of the RoomMember. |      * 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. |      * 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", |     void Join(const std::string& nickname, const std::string& console_id_hash, | ||||||
|               const u16 server_port = DefaultRoomPort, const u16 client_port = 0, |               const char* server_addr = "127.0.0.1", const u16 server_port = DefaultRoomPort, | ||||||
|               const MacAddress& preferred_mac = NoPreferredMac, const std::string& password = ""); |               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. |      * Sends a WiFi packet to the room. | ||||||
|  | @ -139,6 +172,19 @@ public: | ||||||
|      */ |      */ | ||||||
|     void SendGameInfo(const GameInfo& game_info); |     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 |      * 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 |      * 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); |     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. |      * 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. |      * The function wil be called everytime the event is triggered. | ||||||
|  | @ -178,6 +233,26 @@ public: | ||||||
|     CallbackHandle<ChatEntry> BindOnChatMessageRecieved( |     CallbackHandle<ChatEntry> BindOnChatMessageRecieved( | ||||||
|         std::function<void(const ChatEntry&)> callback); |         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. |      * Leaves the current room. | ||||||
|      */ |      */ | ||||||
|  | @ -192,24 +267,44 @@ static const char* GetStateStr(const RoomMember::State& s) { | ||||||
|     switch (s) { |     switch (s) { | ||||||
|     case RoomMember::State::Idle: |     case RoomMember::State::Idle: | ||||||
|         return "Idle"; |         return "Idle"; | ||||||
|     case RoomMember::State::Error: |  | ||||||
|         return "Error"; |  | ||||||
|     case RoomMember::State::Joining: |     case RoomMember::State::Joining: | ||||||
|         return "Joining"; |         return "Joining"; | ||||||
|     case RoomMember::State::Joined: |     case RoomMember::State::Joined: | ||||||
|         return "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"; |         return "LostConnection"; | ||||||
|     case RoomMember::State::NameCollision: |     case RoomMember::Error::HostKicked: | ||||||
|  |         return "HostKicked"; | ||||||
|  |     case RoomMember::Error::UnknownError: | ||||||
|  |         return "UnknownError"; | ||||||
|  |     case RoomMember::Error::NameCollision: | ||||||
|         return "NameCollision"; |         return "NameCollision"; | ||||||
|     case RoomMember::State::MacCollision: |     case RoomMember::Error::MacCollision: | ||||||
|         return "MacCollision"; |         return "MaxCollision"; | ||||||
|     case RoomMember::State::WrongVersion: |     case RoomMember::Error::ConsoleIdCollision: | ||||||
|  |         return "ConsoleIdCollision"; | ||||||
|  |     case RoomMember::Error::WrongVersion: | ||||||
|         return "WrongVersion"; |         return "WrongVersion"; | ||||||
|     case RoomMember::State::WrongPassword: |     case RoomMember::Error::WrongPassword: | ||||||
|         return "WrongPassword"; |         return "WrongPassword"; | ||||||
|     case RoomMember::State::CouldNotConnect: |     case RoomMember::Error::CouldNotConnect: | ||||||
|         return "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"; |     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 |     telemetry_json.h | ||||||
|     verify_login.cpp |     verify_login.cpp | ||||||
|     verify_login.h |     verify_login.h | ||||||
|  |     verify_user_jwt.cpp | ||||||
|  |     verify_user_jwt.h | ||||||
|     web_backend.cpp |     web_backend.cpp | ||||||
|     web_backend.h |     web_backend.h | ||||||
| ) | ) | ||||||
|  | @ -15,4 +17,4 @@ get_directory_property(OPENSSL_LIBS | ||||||
|         DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl |         DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl | ||||||
|         DEFINITION OPENSSL_LIBS) |         DEFINITION OPENSSL_LIBS) | ||||||
| target_compile_definitions(web_service PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT) | 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 { | namespace AnnounceMultiplayerRoom { | ||||||
| 
 | 
 | ||||||
| void to_json(nlohmann::json& json, const Room::Member& member) { | 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["gameName"] = member.game_name; | ||||||
|     json["gameId"] = member.game_id; |     json["gameId"] = member.game_id; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void from_json(const nlohmann::json& json, Room::Member& member) { | 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_name = json.at("gameName").get<std::string>(); | ||||||
|     member.game_id = json.at("gameId").get<u64>(); |     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) { | void to_json(nlohmann::json& json, const Room& room) { | ||||||
|     json["id"] = room.UID; |  | ||||||
|     json["port"] = room.port; |     json["port"] = room.port; | ||||||
|     json["name"] = room.name; |     json["name"] = room.name; | ||||||
|  |     if (!room.description.empty()) { | ||||||
|  |         json["description"] = room.description; | ||||||
|  |     } | ||||||
|     json["preferredGameName"] = room.preferred_game; |     json["preferredGameName"] = room.preferred_game; | ||||||
|     json["preferredGameId"] = room.preferred_game_id; |     json["preferredGameId"] = room.preferred_game_id; | ||||||
|     json["maxPlayers"] = room.max_player; |     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) { | 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.ip = json.at("address").get<std::string>(); | ||||||
|     room.name = json.at("name").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.owner = json.at("owner").get<std::string>(); | ||||||
|     room.port = json.at("port").get<u16>(); |     room.port = json.at("port").get<u16>(); | ||||||
|     room.preferred_game = json.at("preferredGameName").get<std::string>(); |     room.preferred_game = json.at("preferredGameName").get<std::string>(); | ||||||
|  | @ -59,12 +81,12 @@ void from_json(const nlohmann::json& json, Room& room) { | ||||||
| 
 | 
 | ||||||
| namespace WebService { | namespace WebService { | ||||||
| 
 | 
 | ||||||
| void RoomJson::SetRoomInformation(const std::string& uid, const std::string& name, const u16 port, | void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, | ||||||
|                                   const u32 max_player, const u32 net_version, |                                   const u16 port, const u32 max_player, const u32 net_version, | ||||||
|                                   const bool has_password, const std::string& preferred_game, |                                   const bool has_password, const std::string& preferred_game, | ||||||
|                                   const u64 preferred_game_id) { |                                   const u64 preferred_game_id) { | ||||||
|     room.name = name; |     room.name = name; | ||||||
|     room.UID = uid; |     room.description = description; | ||||||
|     room.port = port; |     room.port = port; | ||||||
|     room.max_player = max_player; |     room.max_player = max_player; | ||||||
|     room.net_version = net_version; |     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 = preferred_game; | ||||||
|     room.preferred_game_id = preferred_game_id; |     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 AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, | ||||||
|                          const std::string& game_name) { |                          const std::string& game_name) { | ||||||
|     AnnounceMultiplayerRoom::Room::Member member; |     AnnounceMultiplayerRoom::Room::Member member; | ||||||
|     member.name = nickname; |     member.username = username; | ||||||
|  |     member.nickname = nickname; | ||||||
|  |     member.avatar_url = avatar_url; | ||||||
|     member.mac_address = mac_address; |     member.mac_address = mac_address; | ||||||
|     member.game_id = game_id; |     member.game_id = game_id; | ||||||
|     member.game_name = game_name; |     member.game_name = game_name; | ||||||
|     room.members.push_back(member); |     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; |     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() { | void RoomJson::ClearPlayers() { | ||||||
|  | @ -101,12 +142,14 @@ AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void RoomJson::Delete() { | void RoomJson::Delete() { | ||||||
|     nlohmann::json json; |     if (room_id.empty()) { | ||||||
|     json["id"] = room.UID; |         LOG_ERROR(WebService, "Room must be registered to be deleted"); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|     Common::DetachedTasks::AddTask( |     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.
 |             // 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) |     RoomJson(const std::string& host, const std::string& username, const std::string& token) | ||||||
|         : client(host, username, token), host(host), username(username), token(token) {} |         : client(host, username, token), host(host), username(username), token(token) {} | ||||||
|     ~RoomJson() = default; |     ~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 u32 max_player, const u32 net_version, const bool has_password, | ||||||
|                             const std::string& preferred_game, |                             const std::string& preferred_game, | ||||||
|                             const u64 preferred_game_id) override; |                             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 AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, | ||||||
|                    const std::string& game_name) override; |                    const std::string& game_name) override; | ||||||
|     Common::WebResult Announce() override; |     Common::WebResult Update() override; | ||||||
|  |     std::string Register() override; | ||||||
|     void ClearPlayers() override; |     void ClearPlayers() override; | ||||||
|     AnnounceMultiplayerRoom::RoomList GetRoomList() override; |     AnnounceMultiplayerRoom::RoomList GetRoomList() override; | ||||||
|     void Delete() override; |     void Delete() override; | ||||||
|  | @ -38,6 +40,7 @@ private: | ||||||
|     std::string host; |     std::string host; | ||||||
|     std::string username; |     std::string username; | ||||||
|     std::string token; |     std::string token; | ||||||
|  |     std::string room_id; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace WebService
 | } // 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 <mutex> | ||||||
| #include <string> | #include <string> | ||||||
| #include <LUrlParser.h> | #include <LUrlParser.h> | ||||||
|  | #include <fmt/format.h> | ||||||
| #include <httplib.h> | #include <httplib.h> | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | @ -32,8 +33,9 @@ struct Client::Impl { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// A generic function handles POST, GET and DELETE request together
 |     /// A generic function handles POST, GET and DELETE request together
 | ||||||
|     Common::WebResult GenericJson(const std::string& method, const std::string& path, |     Common::WebResult GenericRequest(const std::string& method, const std::string& path, | ||||||
|                                   const std::string& data, bool allow_anonymous) { |                                      const std::string& data, bool allow_anonymous, | ||||||
|  |                                      const std::string& accept) { | ||||||
|         if (jwt.empty()) { |         if (jwt.empty()) { | ||||||
|             UpdateJWT(); |             UpdateJWT(); | ||||||
|         } |         } | ||||||
|  | @ -44,11 +46,11 @@ struct Client::Impl { | ||||||
|                                      "Credentials needed"}; |                                      "Credentials needed"}; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         auto result = GenericJson(method, path, data, jwt); |         auto result = GenericRequest(method, path, data, accept, jwt); | ||||||
|         if (result.result_string == "401") { |         if (result.result_string == "401") { | ||||||
|             // Try again with new JWT
 |             // Try again with new JWT
 | ||||||
|             UpdateJWT(); |             UpdateJWT(); | ||||||
|             result = GenericJson(method, path, data, jwt); |             result = GenericRequest(method, path, data, accept, jwt); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return result; |         return result; | ||||||
|  | @ -60,9 +62,10 @@ struct Client::Impl { | ||||||
|      * username + token is used if jwt is empty but username and token are |      * 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 |      * not empty anonymous if all of jwt, username and token are empty | ||||||
|      */ |      */ | ||||||
|     Common::WebResult GenericJson(const std::string& method, const std::string& path, |     Common::WebResult GenericRequest(const std::string& method, const std::string& path, | ||||||
|                                   const std::string& data, const std::string& jwt = "", |                                      const std::string& data, const std::string& accept, | ||||||
|                                   const std::string& username = "", const std::string& token = "") { |                                      const std::string& jwt = "", const std::string& username = "", | ||||||
|  |                                      const std::string& token = "") { | ||||||
|         if (cli == nullptr) { |         if (cli == nullptr) { | ||||||
|             auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); |             auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); | ||||||
|             int port; |             int port; | ||||||
|  | @ -133,8 +136,7 @@ struct Client::Impl { | ||||||
|             return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; |             return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (content_type->second.find("application/json") == std::string::npos && |         if (content_type->second.find(accept) == std::string::npos) { | ||||||
|             content_type->second.find("text/html; charset=utf-8") == std::string::npos) { |  | ||||||
|             LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, |             LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, | ||||||
|                       content_type->second); |                       content_type->second); | ||||||
|             return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; |             return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; | ||||||
|  | @ -148,7 +150,7 @@ struct Client::Impl { | ||||||
|             return; |             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) { |         if (result.result_code != Common::WebResult::Code::Success) { | ||||||
|             LOG_ERROR(WebService, "UpdateJWT failed"); |             LOG_ERROR(WebService, "UpdateJWT failed"); | ||||||
|         } else { |         } else { | ||||||
|  | @ -181,16 +183,29 @@ Client::~Client() = default; | ||||||
| 
 | 
 | ||||||
| Common::WebResult Client::PostJson(const std::string& path, const std::string& data, | Common::WebResult Client::PostJson(const std::string& path, const std::string& data, | ||||||
|                                    bool allow_anonymous) { |                                    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) { | 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, | Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data, | ||||||
|                                      bool allow_anonymous) { |                                      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
 | } // namespace WebService
 | ||||||
|  |  | ||||||
|  | @ -46,6 +46,29 @@ public: | ||||||
|     Common::WebResult DeleteJson(const std::string& path, const std::string& data, |     Common::WebResult DeleteJson(const std::string& path, const std::string& data, | ||||||
|                                  bool allow_anonymous); |                                  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: | private: | ||||||
|     struct Impl; |     struct Impl; | ||||||
|     std::unique_ptr<Impl> impl; |     std::unique_ptr<Impl> impl; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue